4. 示例 [nuxt-01]:路由与导航
我们将构建一系列简单示例,逐步了解 [nuxt] 应用程序的工作原理。首先,我们将移植文档《VUE.JS 框架入门示例》中的 [vuejs-11] 应用程序, 首先了解 [nuxt] 应用程序的代码组织结构与 [vue] 应用程序有何不同。
4.1. 项目结构树
[vuejs-11] 项目是一个视图间导航项目:

项目 [vuejs-11] 的源代码结构如下:

- [main.js] 是启动 [vue] 应用程序时执行的脚本;
- [router.js] 定义了路由规则;
- [App.vue] 是应用程序的结构视图,负责组织各视图的布局;
- [Component1, Component2, Component3, Layout, Navigation] 是应用程序各视图中使用的组件;
在将应用程序 [vue] [1] 移植为应用程序 [nuxt] [2] 时:
- 应用程序启动时执行的脚本必须在文件 [nuxt.config.js] 的 [plugins] 键中进行声明。 此外,可以将面向 [nuxt] 服务器的脚本与面向 [nuxt] 客户端的脚本分开;
- 视图 [App.vue] 应安装在文件夹 [layouts] 中,并重命名为 [default.vue];
- 作为路由目标的组件 [Component1, Component2, Component3] 需迁移至文件夹 [pages]。其中一个组件(即用作首页的组件)需重命名为 [index.vue]。我们在此对文件进行了重命名:
- [Component1] --> [index]:显示文本 [Home];
- [Component2] --> [page1]:显示文本 [Page 1];
- [Component3] --> [page2]:显示文本 [Page 2];
[nuxt] 使用文件夹 [pages] 中的内容动态生成以下路线:
因此,在项目 [vue] 中使用的文件 [router.js] 在项目 [nuxt] 中变得不再需要。
配置文件 [nuxt.config.js] 将如下所示:
export default {
mode: 'universal',
/*
** Headers of the page
*/
head: {
title: 'Introduction à [nuxt.js]',
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{
hid: 'description',
name: 'description',
content: 'ssr routing loading asyncdata middleware plugins store'
}
],
link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }]
},
/*
** Customize the progress-bar color
*/
loading: { color: '#fff' },
/*
** Global CSS
*/
css: [],
/*
** Plugins to load before mounting the App
*/
plugins: [],
/*
** Nuxt.js dev-modules
*/
buildModules: [
// 文档:https://github.com/nuxt-community/eslint-module
'@nuxtjs/eslint-module'
],
/*
** Nuxt.js modules
*/
modules: [
// 文档:https://bootstrap-vue.js.org
'bootstrap-vue/nuxt',
// 文档:https://axios.nuxtjs.org/usage
'@nuxtjs/axios'
],
/*
** Axios module configuration
** See https://axios.nuxtjs.org/options
*/
axios: {},
/*
** Build configuration
*/
build: {
/*
** You can extend webpack config here
*/
extend(config, ctx) {}
},
// 源代码目录
srcDir: 'nuxt-01',
// 路由器
router: {
// 应用程序的根目录
base: '/nuxt-01/'
},
// 服务器
server: {
// 服务端口,默认 3000
port: 81,
// 监听的网络地址,默认 localhost:127.0.0.1
// 0.0.0.0 = 该机器的所有网络地址
host: '0.0.0.0'
}
}
- 第 62 行:指定包含项目 [dvp] 源代码的文件夹;
- 第 66 行:指定应用程序 [dvp] 的根目录(可自定义);
- 第 43 行:请注意,配置中引用了库 [bootstrap-vue];
4.2. [main.js] 文件的移植
[vuejs-11] 项目中的 [main.js] 文件内容如下:
// 导入
import Vue from 'vue'
import App from './App.vue'
// 插件
import BootstrapVue from 'bootstrap-vue'
Vue.use(BootstrapVue);
// 引导程序
import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap-vue/dist/bootstrap-vue.css'
// 路由器
import monRouteur from './router'
// 配置
Vue.config.productionTip = false
// 项目实例化[App]
new Vue({
name: "app",
// 主视图
render: h => h(App),
// 路由器
router: monRouteur,
}).$mount('#app')
除 [imports] 外,该代码执行以下操作:
- 第 5-11 行:使用库文件 [bootstrap-vue]。此项工作现由配置文件 [nuxt.config.js] 第 43 行中的模块 [bootstrap-vue/nuxt] 完成;
- 第14行和第25行:使用路由文件[router.js]。此工作现由应用程序[nuxt]根据文件夹[pages]的目录结构自动完成;
- 第 20-26 行:实例化应用程序的主视图。在 [nuxt] 应用程序中,[layouts/default.vue] 视图用作主视图;
文件 [main.js] 现已失去存在意义。如果它原本有其作用,我们本应在配置文件 [nuxt.config.js] 的第 30 行中,通过键 [plugins] 进行声明;
4.3. 主视图 [default.vue]

主视图 [layouts / default.vue] 如下所示:
<template>
<div class="container">
<b-card>
<!-- 一条消息 -->
<b-alert show variant="success" align="center">
<h4>[nuxt-01] : routage et navigation</h4>
</b-alert>
<!-- 当前路由视图 -->
<nuxt />
</b-card>
</div>
</template>
<script>
export default {
name: 'App'
}
</script>
- 在项目 [vuejs-11] 的第 9 行,原本使用的是 <router-view /> 标签,而非此处使用的 <nuxt /> 标签。这两种标签似乎都可以使用。我试用了这两种标签,但未见任何变化。 我保留了推荐使用的 <nuxt /> 标签。它会显示当前视图,即当前路由的目标页面;
4.4. 组件

与项目 [vuejs-11] 相比,组件 [layout, navigation] 没有变化:
[components / layout.vue]
<!-- 视图布局 -->
<template>
<!-- 行 -->
<div>
<b-row>
<!-- 双列区域 -->
<b-col v-if="left" cols="2">
<slot name="left" />
</b-col>
<!-- 十列区域 -->
<b-col v-if="right" cols="10">
<slot name="right" />
</b-col>
</b-row>
</div>
</template>
<script>
export default {
// 参数
props: {
left: {
type: Boolean
},
right: {
type: Boolean
}
}
}
</script>
该组件用于将应用程序页面划分为两栏:
- 第 7-9 行:左侧为 2 个 Bootstrap 列;
- 第11-13行:右侧为10列Bootstrap布局;
[navigation.vue]
<template>
<!-- 三选项 Bootstrap 菜单 -->
<b-nav vertical>
<b-nav-item to="/" exact exact-active-class="active">
Home
</b-nav-item>
<b-nav-item to="/page1" exact exact-active-class="active">
Page 1
</b-nav-item>
<b-nav-item to="/page2" exact exact-active-class="active">
Page 2
</b-nav-item>
</b-nav>
</template>
该组件显示三个导航链接:

要了解第4、7和10行中[to]属性的取值,需查看[pages]和[2]文件夹:
- 页面 [index] 将包含 URL [/];
- 页面 [page1] 将包含 URL 和 [/page1];
- 页面 [page2] 将包含 URL 和 [/page2];
组件 [navigation] 也可以写成如下形式:
<template>
<!-- 三选项 Bootstrap 菜单 -->
<b-nav vertical>
<nuxt-link to="/" exact exact-active-class="active">
Home
</nuxt-link>
<nuxt-link to="/page1" exact exact-active-class="active">
Page 1
</nuxt-link>
<nuxt-link to="/page2" exact exact-active-class="active">
Page 2
</nuxt-link>
</b-nav>
</template>
<b-nav-item> 标签已被 <nuxt-link> 标签取代,后者表示路由链接。在实际运行中,我未观察到显著差异,没有任何因素能使其中一种标签明显优于另一种。
4.5. 页面

页面 [index.vue] 显示如下视图:

该页面的代码如下:
<!-- 主页 -->
<template>
<Layout :left="true" :right="true">
<!-- 导航 -->
<Navigation slot="left" />
<!-- 消息-->
<b-alert slot="right" show variant="warning">
Home
</b-alert>
</Layout>
</template>
<script>
/* eslint-disable no-undef */
/* eslint-disable no-console */
/* eslint-disable nuxt/no-env-in-hooks */
import Navigation from '@/components/navigation'
import Layout from '@/components/layout'
export default {
name: 'Home',
// 使用的组件
components: {
Layout,
Navigation
},
// 生命周期
beforeCreate(...args) {
console.log('[home beforeCreate]', 'process.server=', process.server,
'process.client=', process.client, "参数数量=", args.length)
},
created(...args) {
console.log('[home created]', 'process.server=', process.server,
'process.client=', process.client, "参数个数=", args.length)
},
beforeMount(...args) {
console.log('[home beforeMount]', 'process.server=', process.server,
'process.client=', process.client, "参数个数=", args.length)
},
mounted(...args) {
console.log('[home mounted]', 'process.server=', process.server,
'process.client=', process.client, "参数个数=", args.length)
}
}
</script>
- 第 5 行:导航组件位于左侧栏;
- 第 7-9 行:右侧栏中放置了一个提示;
在 <script> 部分,我们在页面生命周期函数 [beforeCreate, created, beforeMount, beforeMounted] 中添加了代码。我们想知道哪些是由 []nuxt 服务器执行的,哪些是由客户端 [nuxt] 执行的。需要提醒两点:
- 当页面被请求时,无论是应用程序启动时, 例如页面 [index],或是用户手动刷新浏览器页面或直接输入 URL 时,该页面首先由服务器 [nuxt] 返回。 该服务器会解析上述代码并执行其中包含的JavaScript;
- 当由服务器 [nuxt] 发送的页面到达浏览器时,它会携带客户端代码 [nuxt]。客户端会再次解析上述页面;
- 通过日志记录,我们希望了解各方的具体操作,以便更好地理解这一过程;
- 第30-31行:函数中使用了一个全局对象[process],该对象在服务器和客户端均存在:
- 若代码由服务器执行,则 [process.server] 为真,否则为假;
- 若代码由客户端执行,则 [process.client] 为真,否则为假;
- 由于变量 [process] 在代码中未声明,因此必须在第 14 行添加 [eslint]。 第 14 行代码 [16] 是必要的,否则 [eslint] 会因变量 [process] 而报出另一种错误。 第15行是否必要,取决于是否要在生命周期函数中使用[console];
- 第 29 行:我们还想了解生命周期函数是否接收参数。事实上,我们将发现 [nuxt] 会向某些函数传递信息。我们需要确认生命周期函数是否包含在其中;
- 对这四个函数重复相同的代码;
4.6. 文件 [nuxt.config.js]
该文件负责控制项目 [dvp] 的执行。其相关内容已在第 33 页进行过说明。
4.7. 项目执行
我们执行该项目:

显示的页面如下:

在浏览器中安装后,应用程序 [nuxt] 将变为经典的 [vue] 应用程序。 因此,我们将不再对应用程序 [nuxt-01] 的客户端运行机制进行说明。相关内容已在文档《VUE.JS 框架入门示例》中的项目 [vuejs-11] 中进行过说明。
[nuxt] 应用程序与 [vue] 应用程序仅在两个方面有所不同:
- 应用程序的初始启动,此时会显示首页;
- 每次用户以某种方式触发浏览器刷新时;
在这两种情况下:
- 服务器提供所请求的页面;
- 接收到的页面由客户端处理;
让我们查看应用程序启动时的日志(浏览器中的F12):

- 在 [1] 中,服务器日志(process.server=true)。这些日志前缀为 [Nuxt SSR](SSR=服务器端渲染);
- 在 [2] 中,浏览器端的客户端日志(process.client=true);
从这些日志中可以推断出:
- 服务器正在执行生命周期中的 [beforeCreate, created] 函数;
- 客户端执行生命周期中的 [beforeCreate, created, beforeMount, mounted] 函数;
- 服务器在客户端之前处理了该页面;
- 在这两种情况下,执行的函数均未接收任何参数;
现在让我们看看接收到的页面源代码(浏览器中的选项 [Code source de la page]):
<!doctype html>
<html data-n-head-ssr>
<head>
<title>Introduction à [nuxt.js]</title>
<meta data-n-head="ssr" charset="utf-8">
<meta data-n-head="ssr" name="viewport" content="width=device-width, initial-scale=1">
<meta data-n-head="ssr" data-hid="description" name="description" content="ssr routing loading asyncdata middleware plugins store">
<link data-n-head="ssr" rel="icon" type="image/x-icon" href="/favicon.ico">
<base href="/nuxt-01/">
....
<link rel="preload" href="/nuxt-01/_nuxt/runtime.js" as="script">
<link rel="preload" href="/nuxt-01/_nuxt/commons.app.js" as="script">
<link rel="preload" href="/nuxt-01/_nuxt/vendors.app.js" as="script">
<link rel="preload" href="/nuxt-01/_nuxt/app.js" as="script">
</head>
<body>
<div data-server-rendered="true" id="__nuxt">
<div id="__layout">
<div class="container">
<div class="card">
<div class="card-body">
<div role="alert" aria-live="polite" aria-atomic="true" align="center" class="alert alert-success">
<h4>[nuxt-01] : routage et navigation</h4>
</div>
<div>
<div class="row">
<div class="col-2">
<ul class="nav flex-column">
<li class="nav-item">
<a href="/nuxt-01/" target="_self" class="nav-link active nuxt-link-active">
Home
</a>
</li>
<li class="nav-item">
<a href="/nuxt-01/page1" target="_self" class="nav-link">
Page 1
</a>
</li>
<li class="nav-item">
<a href="/nuxt-01/page2" target="_self" class="nav-link">
Page 2
</a>
</li>
</ul>
</div> <div class="col-10">
<div role="alert" aria-live="polite" aria-atomic="true" class="alert alert-warning">
Home
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
window.__NUXT__ = (function (a, b, c, d, e, f, g, h, i, j) {
return {
layout: "default", data: [{}], error: null, serverRendered: true,
logs: [
{ date: new Date(1574069600078), args: [a, b, c, d, e, f, g, "(repeated 1 times)"], type: h, level: i, tag: j },
{ date: new Date(1574070938091), args: [a, b, c, d, e, f, g], type: h, level: i, tag: j }
]
}
}("[home beforeCreate]", "process.server=", "true", "process.client=", "false", "nombre d'arguments=", "0", "log", 2, ""));
</script>
<script src="/nuxt-01/_nuxt/runtime.js" defer></script>
<script src="/nuxt-01/_nuxt/commons.app.js" defer></script>
<script src="/nuxt-01/_nuxt/vendors.app.js" defer></script>
<script src="/nuxt-01/_nuxt/app.js" defer></script>
</body>
</html>
注释
- 首先值得注意的是,收到的 HTML 代码准确反映了用户所见的内容。而 [vue] 应用程序则并非如此,其显示的源代码实际上是一个几乎为空的 HTML 文件的源代码。 这是浏览器接收到的内容。随后,[vue]客户端接管并构建了用户预期的页面。 因此,必须进入浏览器开发工具(F12)中的[inspecteur]标签页,才能查看所显示页面的代码HTML;
- 第57-67行:这是显示了标记为[Nuxt SSR]日志的脚本。这些日志是在服务器端生成的,其结果被嵌入到发送页面中包含的脚本中;
- 第68-71行:构成浏览器端执行客户端的脚本;
第68-71行的脚本被执行并转换了接收到的页面。要查看最终显示给用户的页面,需进入浏览器开发工具(F12)中的[inspecteur]标签页:

展开 <html> 标签([3])后,可见以下内容:
<head>
<title>Introduction à [nuxt.js]</title>
<meta data-n-head="ssr" charset="utf-8">
<meta data-n-head="ssr" name="viewport" content="width=device-width, initial-scale=1">
<meta data-n-head="ssr" data-hid="description" name="description" content="ssr routing loading asyncdata middleware plugins store">
<link data-n-head="ssr" rel="icon" type="image/x-icon" href="/favicon.ico">
<base href="/nuxt-01/">
...
<link rel="preload" href="/nuxt-01/_nuxt/runtime.js" as="script">
<link rel="preload" href="/nuxt-01/_nuxt/commons.app.js" as="script">
<link rel="preload" href="/nuxt-01/_nuxt/vendors.app.js" as="script">
<link rel="preload" href="/nuxt-01/_nuxt/app.js" as="script">
<script charset="utf-8" src="/nuxt-01/_nuxt/pages_index.js"></script>
<script charset="utf-8" src="/nuxt-01/_nuxt/pages_page1.js"></script>
<script charset="utf-8" src="/nuxt-01/_nuxt/pages_page2.js"></script>
</head>
<body>
<div id="__nuxt">
<div id="__layout">
<div class="container">
<div class="card">
<div class="card-body">
<div role="alert" aria-live="polite" aria-atomic="true" class="alert alert-success" align="center">
<h4>[nuxt-01] : routage et navigation</h4>
</div>
<div>
<div class="row">
<div class="col-2">
<ul class="nav flex-column">
<li class="nav-item">
<a href="/nuxt-01/" target="_self" class="nav-link active nuxt-link-active">
Home
</a>
</li>
<li class="nav-item">
<a href="/nuxt-01/page1" target="_self" class="nav-link">
Page 1
</a>
</li>
<li class="nav-item">
<a href="/nuxt-01/page2" target="_self" class="nav-link">
Page 2
</a>
</li>
</ul>
</div>
<div class="col-10">
<div role="alert" aria-live="polite" aria-atomic="true" class="alert alert-warning">
Home
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
window.__NUXT__ = (function (a, b, c, d, e, f, g, h, i) {
return {
layout: "default", data: [{}], error: null, serverRendered: true,
logs: [
{ date: new Date(1574068674481), args: ["[home beforeCreate]", a, b, c, d, e, f], type: g, level: h, tag: i },
{ date: new Date(1574068674482), args: ["[home created]", a, b, c, d, e, f], type: g, level: h, tag: i }
]
}
}("process.server=", "true", "process.client=", "false", "nombre d'arguments=", "0", "log", 2, ""));
</script>
<script src="/nuxt-01/_nuxt/runtime.js" defer=""></script>
<script src="/nuxt-01/_nuxt/commons.app.js" defer=""></script>
<script src="/nuxt-01/_nuxt/vendors.app.js" defer=""></script>
<script src="/nuxt-01/_nuxt/app.js" defer=""></script>
<iframe id="mc-sidebar-container" ...></iframe>
<iframe id="mc-topbar-container"...> </iframe>
<iframe id="mc-toast-container" ...></iframe>
<iframe id="mc-download-overlay-container"...></iframe>
</body>
注释
- 乍看之下,第 19-59 行显示的页面似乎与接收到的页面相同;
- 第14-16行:出现了三个新脚本,应用程序的每一页各有一个;
- 第76-79行:出现了四个[iframe];
第33、37和42行,链接存在问题。它们看似普通链接,点击后应向服务器发送请求。但在执行时,我们发现事实并非如此:并未向服务器发送请求。 要理解原因,需返回浏览器的 [inspecteur] 标签页:

可见在 [1, 2] 中,链接已被附加了事件。正是第 71-74 行的脚本为链接附加了事件处理程序。因此:
- 客户端显示的页面在视觉上与服务器发送的页面完全一致;
- 客户端为页面添加了动态行为;
现在,让我们手动输入 URL 来请求页面 [page1]。日志内容如下:

结果与 [index] 页面相同,但对应的是 [page1]。收到的页面源代码如下:
<body>
<div data-server-rendered="true" id="__nuxt">
<div id="__layout">
<div class="container">
<div class="card">
<div class="card-body">
<div role="alert" aria-live="polite" aria-atomic="true" align="center" class="alert alert-success"><h4>[nuxt-01] : routage et navigation</h4></div> <div>
<div class="row">
<div class="col-2">
<ul class="nav flex-column">
<li class="nav-item">
<a href="/nuxt-01/" target="_self" class="nav-link">
Home
</a>
</li>
<li class="nav-item">
<a href="/nuxt-01/page1" target="_self" class="nav-link active nuxt-link-active">
Page 1
</a>
</li>
<li class="nav-item">
<a href="/nuxt-01/page2" target="_self" class="nav-link">
Page 2
</a>
</li>
</ul>
</div> <div class="col-10">
<div role="alert" aria-live="polite" aria-atomic="true" class="alert alert-primary">
Page 1
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>window.__NUXT__ = { layout: "default", data: [{}], error: null, serverRendered: true, logs: [{ date: new Date(1573917721122), args: ["[page1 beforeCreate]", "process.server=", "true", "process.client=", "false", "nombre d'arguments=", "0"], type: "log", level: 2, tag: "" }] };</script>
<script src="/nuxt-01/_nuxt/runtime.js" defer></script>
<script src="/nuxt-01/_nuxt/commons.app.js" defer></script>
<script src="/nuxt-01/_nuxt/vendors.app.js" defer></script>
<script src="/nuxt-01/_nuxt/app.js" defer></script>
</body>
生成的页面类型与 [index] 页面相同,但显示了 [Page 1] 视图的提示(第 30 行)。第 41-44 行显示,客户端代码已随页面一并返回。 最终,手动请求 URL 与重启应用程序的效果相同。只是显示的页面不一定是首页,而是被请求的页面。一旦收到页面,控制权就交给了客户端。除非用户另有决定,否则服务器将不再被调用。