5. 示例 [nuxt-02]:服务器端与客户端页面
在本项目中,我们将演示:
- 客户端构建的页面可能与从服务器接收到的页面外观不同。这会导致用户察觉到的快速页面切换,从而损害应用的易用性。因此应尽量避免这种做法;
- 一种解决方案,使客户端页面能够重现与服务器发送的页面完全一致的页面;
[nuxt-02] 项目最初是通过克隆 [nuxt-01] 项目创建的。

项目中新增了一个 [store] 文件夹,以及两个新页面。我们稍后会再回来处理这些内容。
5.1. [index] 页面
5.1.1. 页面代码
[index] 页面的代码如下:
<!-- page principale -->
<template>
<Layout :left="true" :right="true">
<!-- navigation -->
<Navigation slot="left" />
<!-- message-->
<b-alert slot="right" show variant="warning"> Home - value= {{ value }} </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 used
components: {
Layout,
Navigation
},
data() {
return {
value: 0
}
},
// life cycle
beforeCreate() {
// client and server
console.log('[home beforeCreate]')
},
created() {
// client and server
console.log('[home created]')
// server only
if (process.server) {
this.value = 10
}
// client and server
console.log('value=', this.value)
},
beforeMount() {
// customer only
console.log('[home beforeMount]')
},
mounted() {
// customer only
console.log('[home mounted]')
}
}
</script>
评论
- 第 7 行:[index] 页面将显示其 [value] 属性的值(第 28 行);
- 第 36–45 行:这里需要记住的是,[created] 函数会在服务器端和客户端两端执行。第 40–42 行:服务器会将 [value] 属性的值设置为 10。然而,客户端并不会修改这个值。我们只是想知道客户端是否保留了这个值。结果我们会发现它并没有保留;
5.1.2. 执行
我们修改 [/nuxt.config.js] 文件以运行 [nuxt-02] 项目:
...
// source code directory
srcDir: 'nuxt-02',
// router
router: {
// application URL root
base: '/nuxt-02/'
},
// server
server: {
// service port, default 3000
port: 81,
// network addresses listened to, default localhost: 127.0.0.1
// 0.0.0.0 = all the machine's network addresses
host: 'localhost'
}
...
我们运行项目 [1]:

随后显示了 [index] 页面 [2-3]。该页面先显示值 [10] 片刻,随后显示值 [0]。发生了什么?
步骤 1
服务器首先运行。它执行[index]页面上的代码:
export default {
name: 'Home',
// components used
components: {
Layout,
Navigation
},
data() {
return {
value: 0
}
},
// life cycle
beforeCreate() {
// client and server
console.log('[home beforeCreate]')
},
created() {
// client and server
console.log('[home created]')
// server only
if (process.server) {
this.value = 10
}
// client and server
console.log('value=', this.value)
},
beforeMount() {
// customer only
console.log('[home beforeMount]')
},
mounted() {
// customer only
console.log('[home mounted]')
}
}
- 由于第 23 行,第 10 行上的 [value] 属性取值为 10;
您可以通过查看浏览器接收到的页面源代码(即浏览器中的[查看源代码]选项)来验证这一点:
<!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-02/">
<link rel="preload" href="/nuxt-02/_nuxt/runtime.js" as="script">
<link rel="preload" href="/nuxt-02/_nuxt/commons.app.js" as="script">
<link rel="preload" href="/nuxt-02/_nuxt/vendors.app.js" as="script">
<link rel="preload" href="/nuxt-02/_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-02] : page serveur, page client</h4>
</div> <div>
<div class="row">
<div class="col-2">
<ul class="nav flex-column">
<li class="nav-item">
<a href="/nuxt-02/" target="_self" class="nav-link active nuxt-link-active">
Home
</a>
</li>
<li class="nav-item">
<a href="/nuxt-02/page1" target="_self" class="nav-link">
Page 1
</a>
</li>
<li class="nav-item">
<a href="/nuxt-02/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 - value= 10
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>window.__NUXT__ = ....;</script>
<script src="/nuxt-02/_nuxt/runtime.js" defer></script>
<script src="/nuxt-02/_nuxt/commons.app.js" defer></script>
<script src="/nuxt-02/_nuxt/vendors.app.js" defer></script>
<script src="/nuxt-02/_nuxt/app.js" defer></script>
</body>
</html>
- 第 46 行:在接收到的页面中,[value] 的值为 10;
步骤 2
我们知道,页面接收后,第 57–60 行的脚本会接管并改变接收到的页面的行为,包括显示的信息,如图所示。这些脚本构成了客户端,它还会执行 [index] 页面的代码——这与服务器端的代码相同:
export default {
name: 'Home',
// components used
components: {
Layout,
Navigation
},
data() {
return {
value: 0
}
},
// life cycle
beforeCreate() {
// client and server
console.log('[home beforeCreate]')
},
created() {
// client and server
console.log('[home created]')
// server only
if (process.server) {
this.value = 10
}
// client and server
console.log('value=', this.value)
},
beforeMount() {
// customer only
console.log('[home beforeMount]')
},
mounted() {
// customer only
console.log('[home mounted]')
}
}
- 要理解发生了什么,你需要明白 [nuxt] 客户端不会执行第 22–24 行(process.server=false);
- 在经典的 [Vue] 应用中,第 10 行中的 [value] 属性会保持为 0。这就是为什么当客户端导航到接收到的页面后,显示的值会变成 [0];
[nuxt] 服务器为 [value] 属性生成的值毫无用处。
5.2. [page1] 页面
5.2.1. [Vuex] 存储
我们在 [nuxt-02] 项目中添加了一个 [store] 文件夹:

该文件夹的存在会导致 [nuxt] 自动实现一个 [Vuex] 存储。该存储由 [index.js] 文件实现。此处的 [index.js] 文件内容如下:
export const state = () => ({
counter: 0
})
export const mutations = {
increment(state, inc) {
state.counter += inc
}
}
[nuxt] 根据 [index.js] 中的内容实现了 [Vuex] 存储:
- 第 1–3 行:定义存储器的 [state]。该状态由一个函数返回。此处,状态仅有一个属性,即第 2 行中的 counter。导出的函数必须命名为 [state];
- 第 5–9 行:对存储状态可执行的操作。这些操作称为 [mutations]。此处的 [increment] 变异会将 [counter] 属性增加 [inc] 的量。导出的对象必须命名为 [mutations];
由 [nuxt] 实现的 Vuex [store] 可在多个位置使用。在视图中,可通过 [this.$store] 属性访问。
5.2.2. 页面代码
与 [index] 页面类似,[page1] 页面将显示一个值:来自 Vuex 存储的计数器:
<!-- page 1 -->
<template>
<Layout :left="true" :right="true">
<!-- navigation -->
<Navigation slot="left" />
<!-- message-->
<b-alert slot="right" show variant="primary"> Page 1 - value = {{ value }} </b-alert>
</Layout>
</template>
<script>
/* eslint-disable no-console */
import Navigation from '@/components/navigation'
import Layout from '@/components/layout'
export default {
name: 'Page1',
// components used
components: {
Layout,
Navigation
},
data() {
return {
value: 0
}
},
// life cycle
beforeCreate() {
// client and server
console.log('[home beforeCreate]')
},
created() {
// client and server
console.log('[home created]')
// server only
if (process.server) {
this.$store.commit('increment', 25)
}
// client and server
this.value = this.$store.state.counter
console.log('value=', this.value)
},
beforeMount() {
// customer only
console.log('[home beforeMount]')
},
mounted() {
// customer only
console.log('[home mounted]')
}
}
</script>
评论
- 第38–40行:服务器将计数器加25;
- 第 42 行:服务器和客户端都会显示计数器的值;
- 第 7 行:显示计数器值;
阅读此代码时,必须理解两点:
- 服务器和客户端执行的代码是相同的;
- [this] 对象并不相同:存在服务器端的 [this] 和客户端的 [this];
我们需要确认服务端的 [this.$store] 是否与客户端的 [this.$store] 相同。由于服务器端代码先执行(应用程序启动时),这实际上等同于询问:服务器端初始化的 [store] 是否被传递给了客户端?
5.2.3. 执行
我们运行 [nuxt-02] 项目,并手动输入 [localhost:81/nuxt-02/page1] 来触发服务器。与启动时的 [index] 页面一样:
- 服务器执行 [page1.vue] 页面;
- 将生成的页面发送至浏览器。页面被显示出来;
- 发送页面中嵌入的客户端脚本接管并再次执行 [page1.vue] 页面;
- 随后显示的页面被修改;
最终结果如下:

这次,显示的值确实是服务器设定的,而且从视觉上看,页面不会因为客户端对服务器显示的值进行了更改而“跳动”。这次发生了什么?
服务器执行了以下 [page1] 页面:
...
<script>
/* eslint-disable no-console */
import Navigation from '@/components/navigation'
import Layout from '@/components/layout'
export default {
name: 'Page1',
// components used
components: {
Layout,
Navigation
},
data() {
return {
value: 0
}
},
// life cycle
beforeCreate() {
// client and server
console.log('[page1 beforeCreate]')
},
created() {
// client and server
console.log('[page1 created]')
// server only
if (process.server) {
this.$store.commit('increment', 25)
}
// client and server
this.value = this.$store.state.counter
console.log('value=', this.value)
},
beforeMount() {
// customer only
console.log('[page1 beforeMount]')
},
mounted() {
// customer only
console.log('[page1 mounted]')
}
}
</script>
- 第 30–32 行已无错误地执行完毕。这意味着在服务器端,[this.$store] 同样指向 [Vuex] 存储。第 31 行将存储的计数器设置为 25;
- 随后页面被发送至客户端;
如果查看客户端接收到的页面,我们会发现以下元素:
<!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-02/">
<link rel="preload" href="/nuxt-02/_nuxt/runtime.js" as="script">
<link rel="preload" href="/nuxt-02/_nuxt/commons.app.js" as="script">
<link rel="preload" href="/nuxt-02/_nuxt/vendors.app.js" as="script">
<link rel="preload" href="/nuxt-02/_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-02] : page serveur, page client</h4>
</div>
<div>
<div class="row">
<div class="col-2">
<ul class="nav flex-column">
<li class="nav-item">
<a href="/nuxt-02/" target="_self" class="nav-link">
Home
</a>
</li>
<li class="nav-item">
<a href="/nuxt-02/page1" target="_self" class="nav-link active nuxt-link-active">
Page 1
</a>
</li>
<li class="nav-item">
<a href="/nuxt-02/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 - value = 25
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
window.__NUXT__ = (function (a, b, c) {
return {
layout: "default", data: [{}], error: null, state: { counter: 25 }, serverRendered: true,
logs: [
{ date: new Date(1574085336802), args: ["[home beforeCreate]"], type: a, level: b, tag: c },
{ date: new Date(1574085336839), args: ["[home created]"], type: a, level: b, tag: c },
{ date: new Date(1574085336869), args: ["value=", "25"], type: a, level: b, tag: c }
]
}
}("log", 2, ""));</script>
<script src="/nuxt-02/_nuxt/runtime.js" defer></script>
<script src="/nuxt-02/_nuxt/commons.app.js" defer></script>
<script src="/nuxt-02/_nuxt/vendors.app.js" defer></script>
<script src="/nuxt-02/_nuxt/app.js" defer></script>
</body>
</html>
- 第 47 行:服务器发送的值;
- 第 60 行:我们可以看到 [Vuex] 存储的状态已被嵌入页面中。这将使客户端(在收到页面后运行)能够重建一个新的 [Vuex] 存储,并将计数器的初始值设为 25;
从服务器接收并显示页面后,客户端接管并依次执行 [page1] 页面:
...
<script>
/* eslint-disable no-console */
import Navigation from '@/components/navigation'
import Layout from '@/components/layout'
export default {
name: 'Page1',
// components used
components: {
Layout,
Navigation
},
data() {
return {
value: 0
}
},
// life cycle
beforeCreate() {
// client and server
console.log('[page1 beforeCreate]')
},
created() {
// client and server
console.log('[page1 created]')
// server only
if (process.server) {
this.$store.commit('increment', 25)
}
// client and server
this.value = this.$store.state.counter
console.log('value=', this.value)
},
beforeMount() {
// customer only
console.log('[page1 beforeMount]')
},
mounted() {
// customer only
console.log('[page1 mounted]')
}
}
</script>
- 第 34 行:将计数器的值 25 赋给第 18 行中的 [value] 属性;
因此,[nuxt] 存储允许服务器在页面初始加载时(即客户端向服务器请求页面时)向客户端传输信息。请注意,一旦获取该页面,服务器便不再参与其中,应用程序将像经典的 [vue] 应用程序一样,以单页模式运行。
5.3. [page2] 页面
在 [page2] 页面上,我们将演示另一种实现方式:
- 服务器将计算结果包含在页面中;
- 客户端不修改这些信息;
5.3.1. 页面代码
页面 [page2] 的代码如下所示:
<!-- page2 -->
<template>
<Layout :left="true" :right="true">
<!-- navigation -->
<Navigation slot="left" />
<!-- message -->
<b-alert slot="right" show variant="secondary"> Page 2 - value = {{ value }} </b-alert>
</Layout>
</template>
<script>
/* eslint-disable no-console */
/* eslint-disable nuxt/no-timing-in-fetch-data */
import Navigation from '@/components/navigation'
import Layout from '@/components/layout'
export default {
name: 'Page2',
// components used
components: {
Layout,
Navigation
},
asyncData(context) {
// who executes this code?
console.log('asyncData, client=', process.client, 'serveur=', process.server)
// only for the server
if (process.server) {
// we return a promise
return new Promise(function(resolve, reject) {
// this is normally an asynchronous function
// we simulate it with a one-second wait
setTimeout(() => {
// this result will be included in the properties of [data]
resolve({ value: 87 })
// log
console.log('asynData terminée')
}, 1000)
})
}
},
// life cycle
beforeCreate() {
// client and server
console.log('[page2 beforeCreate]')
},
created() {
// client and server
console.log('[page2 created]')
},
beforeMount() {
// customer only
console.log('[page2 beforeMount]')
},
mounted() {
// customer only
console.log('[page2 mounted]')
}
}
</script>
- 第 7 行:页面显示了一个名为 [value] 的属性的值;
- [value] 属性并不存在于 [data] 函数返回的对象中。此处,该函数并不存在。第 36 行动态创建了 [value] 属性;
- 第 25 行:[asyncData] 函数是一个 [nuxt] 函数。顾名思义,它通常是一个异步函数。其主要作用是获取外部数据。[nuxt] 确保在 [asyncData] 函数渲染完其异步数据之前,不会将页面发送给客户端浏览器;
- [asyncData] 函数将 [nuxt] 上下文作为参数接收。该对象功能非常丰富,提供了访问 [nuxt] 应用程序大量信息的途径。我们将在后续章节中详细探讨这一点;
- 第 31 行:我们使用 [Promise] 来实现 [asyncData] 函数(参见文档 |通过示例了解 ECMAScript 6|)。该类的构造函数接受一个异步函数作为参数,该函数:
- 通过 [resolve] 函数返回数据来表示成功。该函数返回的对象会自动包含在页面的 [data] 属性中;
- 通过 [reject] 函数返回错误来表示失败;
- 第 34 行:我们使用 [setTimeout] 函数模拟一个异步函数。该函数通过 [resolve] 函数在 1 秒后(第 31 行)返回对象 [{ value: 87 }](第 36 行),这表示 [Promise] 已成功。 该异步函数返回的对象会自动包含在页面的 [data] 属性中。第 7 行显示的正是该属性;
- 第 27 行:我们将看到 [asyncData] 函数由服务器执行,而非客户端;
- 第 29 行:[value] 属性由服务器初始化;
注意:[asyncData] 函数中无法识别 [this] 对象,因为封装 [vue] 组件的对象尚未创建;
5.3.2. 执行
我们运行 [nuxt-02] 项目,并手动输入 [localhost:81/nuxt-02/page2] 来触发服务器。与 [index] 页面的初始加载类似:
- 服务器执行 [page2.vue] 页面;
- 将生成的页面发送至浏览器。页面被显示出来;
- 发送页面中嵌入的客户端脚本接管并再次执行 [page2.vue] 页面;
- 随后显示的页面被修改;
最终结果如下:

这次,显示的值确实是服务器设定的,而且从视觉上看,页面不会因为客户端对服务器显示的值进行了更改而“跳动”。这次发生了什么?
服务器执行了以下 [page2] 页面:
<!-- page2 -->
<template>
<Layout :left="true" :right="true">
<!-- navigation -->
<Navigation slot="left" />
<!-- message -->
<b-alert slot="right" show variant="secondary"> Page 2 - value = {{ value }} </b-alert>
</Layout>
</template>
<script>
/* eslint-disable no-console */
/* eslint-disable nuxt/no-timing-in-fetch-data */
import Navigation from '@/components/navigation'
import Layout from '@/components/layout'
export default {
name: 'Page2',
// components used
components: {
Layout,
Navigation
},
asyncData(context) {
// who executes this code?
console.log('asyncData, client=', process.client, 'serveur=', process.server)
// only for the server
if (process.server) {
// we return a promise
return new Promise(function(resolve, reject) {
// this is normally an asynchronous function
// we simulate it with a one-second wait
setTimeout(() => {
// this result will be included in the properties of [data]
resolve({ value: 87 })
// log
console.log('asynData terminée')
}, 1000)
})
}
},
// life cycle
beforeCreate() {
// client and server
console.log('[page2 beforeCreate]')
},
created() {
// client and server
console.log('[page2 created]')
},
beforeMount() {
// customer only
console.log('[page2 beforeMount]')
},
mounted() {
// customer only
console.log('[page2 mounted]')
}
}
</script>
第 36 行设置了第 7 行显示的值。这就是客户端浏览器接收到的内容。具体来说,它接收到了以下页面:
<!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-02/">
<link rel="preload" href="/nuxt-02/_nuxt/runtime.js" as="script">
<link rel="preload" href="/nuxt-02/_nuxt/commons.app.js" as="script">
<link rel="preload" href="/nuxt-02/_nuxt/vendors.app.js" as="script">
<link rel="preload" href="/nuxt-02/_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-02] : page serveur, page client</h4>
</div>
<div>
<div class="row">
<div class="col-2">
<ul class="nav flex-column">
<li class="nav-item">
<a href="/nuxt-02/" target="_self" class="nav-link">
Home
</a>
</li>
<li class="nav-item">
<a href="/nuxt-02/page1" target="_self" class="nav-link">
Page 1
</a>
</li>
<li class="nav-item">
<a href="/nuxt-02/page2" target="_self" class="nav-link active nuxt-link-active">
Page 2
</a>
</li>
</ul>
</div>
<div class="col-10">
<div role="alert" aria-live="polite" aria-atomic="true" class="alert alert-secondary">
Page 2 - value = 87
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
window.__NUXT__ = (function (a, b, c) {
return {
layout: "default", data: [{ value: 87 }], error: null, state: { counter: 0 }, serverRendered: true,
logs: [
{ date: new Date(1574096608555), args: ["asyncData, client=", "false", "serveur=", "true"], type: a, level: b, tag: c },
{ date: new Date(1574096608575), args: ["[page2 beforeCreate]"], type: a, level: b, tag: c },
{ date: new Date(1574096608599), args: ["[page2 created]"], type: a, level: b, tag: c }
]
}
}("log", 2, ""));</script>
<script src="/nuxt-02/_nuxt/runtime.js" defer></script>
<script src="/nuxt-02/_nuxt/commons.app.js" defer></script>
<script src="/nuxt-02/_nuxt/vendors.app.js" defer></script>
<script src="/nuxt-02/_nuxt/app.js" defer></script>
</body>
</html>
- 第 48 行:我们看到接收到的页面中的值为 87;
- 第 61 行:在服务器响应中,我们看到两个对象:[data] 和 [state]:
- [state] 是 [Vuex] 存储的状态。它是根据 [nuxt-02] 应用程序中 [store] 文件夹的内容实例化而来的;
- [data] 包含服务器通过 [asyncData] 函数创建的属性。我们发现了由服务器创建的 [value: 87] 属性。客户端脚本将把该属性整合到 [page2] 页面的属性中;
让我们回到 [page2] 页面的代码:
<!-- page2 -->
<template>
<Layout :left="true" :right="true">
<!-- navigation -->
<Navigation slot="left" />
<!-- message -->
<b-alert slot="right" show variant="secondary"> Page 2 - value = {{ value }} </b-alert>
</Layout>
</template>
<script>
/* eslint-disable no-console */
/* eslint-disable nuxt/no-timing-in-fetch-data */
import Navigation from '@/components/navigation'
import Layout from '@/components/layout'
export default {
name: 'Page2',
// components used
components: {
Layout,
Navigation
},
asyncData(context) {
// who executes this code?
console.log('asyncData, client=', process.client, 'serveur=', process.server)
// only for the server
if (process.server) {
// we return a promise
return new Promise(function(resolve, reject) {
// this is normally an asynchronous function
// we simulate it with a one-second wait
setTimeout(() => {
// this result will be included in the properties of [data]
resolve({ value: 87 })
// log
console.log('asynData terminée')
}, 1000)
})
}
},
// life cycle
beforeCreate() {
// client and server
console.log('[page2 beforeCreate]')
},
created() {
// client and server
console.log('[page2 created]')
},
beforeMount() {
// customer only
console.log('[page2 beforeMount]')
},
mounted() {
// customer only
console.log('[page2 mounted]')
}
}
</script>
- 第 7 行使用了 [value] 属性。然而,该页面并未定义任何名为 [value] 的属性。客户端脚本使用从服务器接收到的 [data: [{ value: 87 }]] 对象自动创建了此属性;
日志还显示,[asyncData] 函数并未被客户端执行:

[asyncData] 函数由服务器 [1] 执行,而非客户端 [2]。此外,需要注意的是,在 [asyncData] 函数完成之前,服务器不会执行生命周期函数。我们可以通过增加 [asyncData] 函数内的等待时间来验证这一点。
5.4. [page3] 页面
我们正在应用程序中添加一个新页面 [page3]:

5.4.1. [导航]组件
已修改 [navigation] 组件,以便导航至新页面:
<template>
<!-- bootstrap menu with three options -->
<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-item to="/page3" exact exact-active-class="active">
Page 3
</b-nav-item>
</b-nav>
</template>
5.4.2. [page3] 的代码
页面 [page3] 的代码如下:
<!-- page3 -->
<template>
<Layout :left="true" :right="true">
<!-- navigation -->
<Navigation slot="left" />
<!-- message -->
<b-alert slot="right" show variant="secondary"> Page 3 - value = {{ value }} </b-alert>
</Layout>
</template>
<script>
/* eslint-disable no-console */
/* eslint-disable nuxt/no-timing-in-fetch-data */
import Navigation from '@/components/navigation'
import Layout from '@/components/layout'
export default {
name: 'Page3',
// components used
components: {
Layout,
Navigation
},
data() {
return {
value: 0
}
},
fetch(context) {
// who executes this code?
console.log('fetch, client=', process.client, 'serveur=', process.server)
// only for the server
if (process.server) {
// we return a promise
return new Promise(function(resolve, reject) {
// this is normally an asynchronous function
// we simulate it with a one-second wait
setTimeout(() => {
// success
resolve()
}, 1000)
}).then(() => {
// modify the blind
context.store.commit('increment', 28)
})
}
},
// life cycle
beforeCreate() {
// client and server
console.log('[page3 beforeCreate]')
},
created() {
// client and server
this.value = this.$store.state.counter
console.log('[page3 created], value=', this.value)
},
beforeMount() {
// customer only
console.log('[page3 beforeMount]')
},
mounted() {
// customer only
console.log('[page3 mounted]')
}
}
</script>
- 第 30 行:[fetch] 函数的行为与 [asyncData] 函数类似:
- 它在生命周期函数之前执行;
- 该函数中不识别 [this] 对象;
- 它以异步方式运行;
- 生命周期不会在异步函数返回结果之前开始;
- 结果通过 [Promise] 的 [then] 方法在此处返回(第 43 行);
- [fetch] 函数接收 [context] 参数。该参数表示当前的 [nuxt] 上下文;
- 第 30 行:在 [context] 对象的众多属性中,有一个 [store] 属性,它表示应用程序的 [Vuex] 存储;
- 第 41 行:我们人为地在 1 秒后触发 [Promise] 的成功信号(参见文档 | 通过示例了解 ECMAScript 6 |);
- 第 45 行:随后执行 [then] 方法。在此处,[store] 计数器被递增;
5.4.3. 执行
我们运行 [nuxt-02] 项目,并手动输入 [localhost:81/nuxt-02/page3] 来触发服务器。与启动时的 [index] 页面一样:
- 服务器执行 [page3.vue] 页面;
- 将生成的页面发送至浏览器。页面随即显示;
- 发送页面中嵌入的客户端脚本接管并再次执行 [page3.vue] 页面;
- 随后显示的页面被修改;
最终结果如下:

显示的值确实是服务器设定的,而且从视觉上看,页面并未因客户端对服务器显示值的更改而“跳转”。这次发生了什么?
服务器执行了以下 [page3] 页面:
<!-- page3 -->
<template>
<Layout :left="true" :right="true">
<!-- navigation -->
<Navigation slot="left" />
<!-- message -->
<b-alert slot="right" show variant="secondary"> Page 3 - value = {{ value }} </b-alert>
</Layout>
</template>
<script>
/* eslint-disable no-console */
/* eslint-disable nuxt/no-timing-in-fetch-data */
import Navigation from '@/components/navigation'
import Layout from '@/components/layout'
export default {
name: 'Page3',
// components used
components: {
Layout,
Navigation
},
data() {
return {
value: 0
}
},
fetch(context) {
// who executes this code?
console.log('fetch, client=', process.client, 'serveur=', process.server)
// only for the server
if (process.server) {
// we return a promise
return new Promise(function(resolve, reject) {
// this is normally an asynchronous function
// we simulate it with a one-second wait
setTimeout(() => {
// success
resolve()
}, 1000)
}).then(() => {
// modify the blind
context.store.commit('increment', 28)
// log
console.log('fetch commit terminé')
})
}
},
// life cycle
beforeCreate() {
// client and server
console.log('[page3 beforeCreate]')
},
created() {
// client and server
this.value = this.$store.state.counter
console.log('[page3 created], value=', this.value)
},
beforeMount() {
// customer only
console.log('[page3 beforeMount]')
},
mounted() {
// customer only
console.log('[page3 mounted]')
}
}
</script>
- 第 45 行:异步函数 [fetch] 是上述函数中第一个执行的。它接收一个名为 [context] 的对象作为参数,该对象是当前的 [Nuxt] 上下文。在这个对象的众多属性中,[context.store] 属性代表 [Vuex] 存储;
- 第 45 行:在异步函数 [fetch] 中,服务器将存储的计数器设置为 28;
- 第 56 行:当 [created] 函数运行时,[nuxt] 会确保异步 [fetch] 函数已完成其工作;
- 第 58 行:将存储计数器的值赋给第 27 行中的 [value] 属性;
- 第 7 行:显示 [value] 的值,即存储器的计数器;
客户端浏览器接收到的页面如下:
<!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-02/">
<link rel="preload" href="/nuxt-02/_nuxt/runtime.js" as="script">
<link rel="preload" href="/nuxt-02/_nuxt/commons.app.js" as="script">
<link rel="preload" href="/nuxt-02/_nuxt/vendors.app.js" as="script">
<link rel="preload" href="/nuxt-02/_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-02] : page serveur, page client</h4>
</div>
<div>
<div class="row">
<div class="col-2">
<ul class="nav flex-column">
<li class="nav-item">
<a href="/nuxt-02/" target="_self" class="nav-link">
Home
</a>
</li>
<li class="nav-item">
<a href="/nuxt-02/page1" target="_self" class="nav-link">
Page 1
</a>
</li>
<li class="nav-item">
<a href="/nuxt-02/page2" target="_self" class="nav-link">
Page 2
</a>
</li>
<li class="nav-item">
<a href="/nuxt-02/page3" target="_self" class="nav-link active nuxt-link-active">
Page 3
</a>
</li>
</ul>
</div> <div class="col-10">
<div role="alert" aria-live="polite" aria-atomic="true" class="alert alert-secondary">
Page 3 - value = 28
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
window.__NUXT__ = (function (a, b, c) {
return {
layout: "default", data: [{}], error: null, state: { counter: 28 }, serverRendered: true,
logs: [
{ date: new Date(1574169916025), args: ["fetch, client=", "false", "serveur=", "true"], type: a, level: b, tag: c },
{ date: new Date(1574169917038), args: ["fetch commit terminé"], type: a, level: b, tag: c },
{ date: new Date(1574169917137), args: ["[page3 beforeCreate]"], type: a, level: b, tag: c },
{ date: new Date(1574169917167), args: ["[page3 created], value=", "28"], type: a, level: b, tag: c }
]
}
}("log", 2, ""));</script>
<script src="/nuxt-02/_nuxt/runtime.js" defer></script>
<script src="/nuxt-02/_nuxt/commons.app.js" defer></script>
<script src="/nuxt-02/_nuxt/vendors.app.js" defer></script>
<script src="/nuxt-02/_nuxt/app.js" defer></script>
</body>
</html>
- 第 52 行:我们看到接收到的页面中的值为 28;
- 第 65 行:在服务器响应中,我们看到服务器将 [Vuex] 存储的状态发送给了客户端。利用这些信息,客户端脚本将能够重建一个 [Vuex] 存储;
客户端脚本随后将执行页面 [page3] 的代码:
<!-- page3 -->
<template>
<Layout :left="true" :right="true">
<!-- navigation -->
<Navigation slot="left" />
<!-- message -->
<b-alert slot="right" show variant="secondary"> Page 3 - value = {{ value }} </b-alert>
</Layout>
</template>
<script>
/* eslint-disable no-console */
/* eslint-disable nuxt/no-timing-in-fetch-data */
import Navigation from '@/components/navigation'
import Layout from '@/components/layout'
export default {
name: 'Page3',
// components used
components: {
Layout,
Navigation
},
data() {
return {
value: 0
}
},
fetch(context) {
// who executes this code?
console.log('fetch, client=', process.client, 'serveur=', process.server)
// only for the server
if (process.server) {
// we return a promise
return new Promise(function(resolve, reject) {
// this is normally an asynchronous function
// we simulate it with a one-second wait
setTimeout(() => {
// success
resolve()
}, 1000)
}).then(() => {
// modify the blind
context.store.commit('increment', 28)
// log
console.log('fetch commit terminé')
})
}
},
// life cycle
beforeCreate() {
// client and server
console.log('[page3 beforeCreate]')
},
created() {
// client and server
this.value = this.$store.state.counter
console.log('[page3 created], value=', this.value)
},
beforeMount() {
// customer only
console.log('[page3 beforeMount]')
},
mounted() {
// customer only
console.log('[page3 mounted]')
}
}
</script>
- 第 58 行:客户端执行的 [created] 函数将计数器值设置为第 27 行中的 [value] 属性;
- 第 7 行显示该值。由于该值与服务器发送的值相同,因此我们不会因值的变化而看到页面“跳转”;
日志还显示,客户端并未执行 [fetch] 函数:

[fetch] 函数由服务器 [1] 执行,而非客户端 [2]。此外,请注意,在 [fetch] 函数完成之前,服务器不会执行生命周期函数 [3]。我们可以通过增加 [fetch] 函数内的等待时间来验证这一点。
页面 [page1] 和 [page3] 演示了使用 [Vuex] 存储将信息从服务器传输到客户端的两种方法。有人可能会质疑它们是否等效。我们将构建一个页面 [page4] 来验证这一点。
5.5. [page4] 页面
我们将向应用程序中添加一个新页面 [page4]:

5.5.1. [navigation] 组件
已修改 [navigation] 组件,使其支持导航至新页面:
<template>
<!-- bootstrap menu with five options -->
<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-item to="/page3" exact exact-active-class="active">
Page 3
</b-nav-item>
<b-nav-item to="/page4" exact exact-active-class="active">
Page 4
</b-nav-item>
</b-nav>
</template>
5.5.2. [page4] 的代码
[page4] 页面的代码如下:
<!-- page4 -->
<template>
<Layout :left="true" :right="true">
<!-- navigation -->
<Navigation slot="left" />
<!-- message -->
<b-alert slot="right" show variant="secondary"> Page 4 - value = {{ value }} </b-alert>
</Layout>
</template>
<script>
/* eslint-disable no-console */
import Navigation from '@/components/navigation'
import Layout from '@/components/layout'
export default {
name: 'Page4',
// components used
components: {
Layout,
Navigation
},
data() {
return {
value: 0
}
},
// life cycle
async beforeCreate() {
// client and server
console.log('[page4 beforeCreate]')
// only for the server
if (process.server) {
// execute the asynchronous function
const valeur = await new Promise(function(resolve, reject) {
// this is normally an asynchronous function
// we simulate it with a 10-second wait
setTimeout(() => {
// success - the counter value is returned
resolve(52)
}, 10000)
})
// modify the blind
this.$store.commit('increment', valeur)
// log
console.log('[page4 beforeCreate], fonction asynchrone terminée, compteur=', this.$store.state.counter)
}
},
created() {
// client and server
this.value = this.$store.state.counter
console.log('[page4 created], value=', this.value)
},
beforeMount() {
// customer only
console.log('[page4 beforeMount]')
},
mounted() {
// customer only
console.log('[page4 mounted]')
}
}
</script>
- 第 30 行:之前在 [fetch] 函数中执行的操作,现在移到了 [beforeCreate] 方法中。我们使用 async(第 30 行)/ await(第 36 行)这对语法来等待异步函数完成;
- 第 36 行:我们在 10 秒后(第 42 行)获取第 41 行返回的异步函数的结果;
- 第 50–54 行:在 [created] 方法中(该方法在服务器和客户端均会执行),将计数器赋值给页面的 [value] 属性;
5.5.3. 执行
运行 [nuxt-02] 项目,并手动输入 [localhost:81/nuxt-02/page4] 来触发服务器。与 [index] 页面的初始加载类似:
- 服务器执行 [page4.vue] 页面;
- 将生成的页面发送至浏览器。页面随即显示;
- 嵌入在发送页面中的客户端脚本接管并再次执行 [page4.vue] 页面;
- 随后显示的页面被修改;
最终结果如下:

与预期相反,[2]中显示的值并非52。这是怎么回事?
日志如下:

我们可以看到,在 [1] 中,表示异步操作结束的日志并未显示。显示计数器值的 [created] 函数显示为 0。这一切都表明 [nuxt] 并未等待异步操作完成。
如果我们回到用于启动应用程序的 VSCode 终端,会发现日志 [3-4]。我们可以看到,该异步函数确实已在服务器端执行。
最终,[beforeCreate] 函数确实完全在服务器端执行,但 [Nuxt] 在将页面发送至客户端浏览器前并未等待其执行完成,尽管它确实会等待 [fetch] 函数完成。因此,若希望由服务器初始化 [Vuex] 存储,应采用此方法。
5.6. [Vue] 应用中的导航
我们已经展示了当服务器初次加载 [index, page1, page2, page3, page4] 各页面时的情况。实际上,情况并非如此:在正常运行时,只有 [index] 页面是从服务器获取的。让我们来看一下这种情况下的三个页面:
[index] 页面

我们在“链接”一节中已经解释过这一结果。
现在,让我们点击 [第 1 页] 链接:

显示的值为 0。而当通过手动输入 URL 首次向服务器请求该页面时,该值为 25。原因很简单。执行的代码如下:
created() {
// client and server
console.log('[page1 created]')
// server only
if (process.server) {
this.$store.commit('increment', 25)
}
// client and server
this.value = this.$store.state.counter
console.log('value=', this.value)
},
第 6 行是将计数器设置为 25 的那行。由于页面并未从服务器请求,因此第 5 至 7 行未被执行,[Vuex] 存储中的计数器仍保持为 0。
现在,让我们点击 [Page 2] 链接:

这次,页面上没有显示任何值,而且我们在控制台日志中也看到了一条警告:

- 在 [1] 中,我们可以看到 [asyncData] 函数是由客户端执行的。情况总是如此:
- 如果页面是从服务器请求的,则由服务器执行。在这种情况下,它不是由客户端执行的;
- 当页面是客户端当前路由的目标时,它由客户端执行;
- 在 [2] 中:[nuxt] 发出警告,因为页面模板中包含响应式表达式 {{ value }},尽管该页面没有 [value] 属性;
让我们回顾一下由客户端执行的代码:
asyncData() {
// who executes this code?
console.log('asyncData, client=', process.client, 'serveur=', process.server)
// only for the server
if (process.server) {
// we return a promise
return new Promise(function(resolve, reject) {
// this is normally an asynchronous function - so not here
// this result will be included in the properties of [data]
resolve({ value: 87 })
})
}
},
- 第 3 行表明 [asyncData] 函数甚至在生命周期函数之前就被客户端执行了;
- 第 5 行阻止了创建 [value] 属性的其余代码执行,因为该代码由客户端执行;
现在让我们转到页面 [page3]:

- 在 [2] 中,当页面由服务器提供时,数值为 28。但此处情况并非如此;
- 在[4]中,我们可以看到[fetch]函数是在客户端执行的;
让我们来分析客户端执行的代码:
...
fetch(context) {
// who executes this code?
console.log('fetch, client=', process.client, 'serveur=', process.server)
// only for the server
if (process.server) {
// we return a promise
return new Promise(function(resolve, reject) {
// this is normally an asynchronous function
// we simulate it with a one-second wait
setTimeout(() => {
// success
resolve()
}, 1000)
}).then(() => {
// modify the blind
context.store.commit('increment', 28)
// log
console.log('fetch commit terminé')
})
}
},
...
- 客户端执行 [fetch] 方法,第 2 行;
- 第 6–21 行未被执行,因为 [process.server] 的条件为 false。因此,第 17 行(将计数器设置为 28)未被执行。计数器保持为零。这就是为什么客户端显示 0 而不是 28 的原因;
现在让我们转到页面 [page4]。我们得到以下结果:

- 在 [2] 中,计数器的值;
- 在 [3] 中,客户端日志;
客户端执行的代码如下:
...
data() {
return {
value: 0
}
},
// life cycle
async beforeCreate() {
// client and server
console.log('[page4 beforeCreate]')
// only for the server
if (process.server) {
// execute the asynchronous function
const valeur = await new Promise(function(resolve, reject) {
// this is normally an asynchronous function
// we simulate it with a 10-second wait
setTimeout(() => {
// success - the counter value is returned
resolve(52)
}, 10000)
})
// modify the blind
this.$store.commit('increment', valeur)
// log
console.log('[page4 beforeCreate], fonction asynchrone terminée, compteur=', this.$store.state.counter)
}
},
created() {
// client and server
this.value = this.$store.state.counter
console.log('[page4 created], value=', this.value)
},
...
- 第 12–26 行不会被客户端执行,因为第 12 行中的 [process.server] 条件为 false。因此,[Vuex] 存储中的计数器为 0(这是存储中的初始值),这也是页面上显示的值;
你可能会好奇,如果将第 12 行和第 26 行的 [if] 语句注释掉会发生什么。答案如下:
- 这次,客户端执行了第 14–25 行,但 [nuxt] 不会像在服务器端那样等待异步函数完成,因此计数器保持为 0;
- 10 秒后,异步函数完成,第 23 行将计数器设置为 52;
- 当导航返回 [page4] 页面时,便会显示数值 52;
5.7. 总结
通过各项测试,我们可以得出以下结论:
- 如果 [index] 页面必须包含外部数据,服务器可以使用 [asyncData] 函数获取该数据;
- 如果服务器在加载 [index] 页面时需要使用外部数据初始化 [Vuex] 存储,则会在 [fetch] 函数中进行;
- 服务器生成的页面与客户端生成的页面必须完全一致,以避免因客户端页面在视觉上替换服务器发送并初始显示的页面而导致的“闪烁”现象;
我们将通过一个新示例进一步探索 [Nuxt] 的其他方面。