9. 示例 [nuxt-06]:在会话管理器上下文中的注入
9.1. 概述
示例 [nuxt-05] 展示了即使用户强制发起服务器请求,数据存储(store)仍可保持持久化。由于数据存储中的元素具有响应式特性,因此若将其集成到视图中,这些视图将对数据存储中的变化做出响应。此外,您可能希望在客户端与服务器交互过程中保持某些元素的持久化,但又不希望它们具有响应式特性——仅仅是因为这些元素并未在视图中显示。此时,您可以将这些元素存储在会话中,而无需将其放入数据存储中。
通过 [context.app.$store](视图外部)或 [this.$store](视图内部)等属性,可以轻松访问存储。 我们希望对会话也能实现类似的操作,例如 [context.app.$session] 或 [this.$session]。我们将看到,借助注入的概念,这是可行的。不过,我们无法将会话对象注入到上下文中,只能注入函数。随后,该函数可通过表达式 [context.app.$session()] 或 [this.$session()] 调用。
最后,我们将介绍 [nuxt] 中的 [plugin] 概念。
[nuxt-06] 示例最初是通过克隆 [nuxt-05] 项目创建的:

- 在 [1] 中,我们将添加一个 [plugins] 文件夹;
9.2. [nuxt] 插件的概念
[nuxt] 将 [plugin] 定义为应用程序启动时执行的任何代码,甚至在服务器执行 [nuxtServerInit] 函数之前(此前该函数是用户代码中最早被执行的函数)。应用程序的插件必须在 [nuxt.config.js] 配置文件的 [plugins] 键下进行声明:
/*
** Plugins to load before mounting the App
*/
plugins: [
{ src: '~/plugins/client/session', mode: 'client' },
{ src: '~/plugins/server/session', mode: 'server' }
],
- 第 5-6 行:插件通过其路径 [src] 和执行模式 [mode] 进行标识。[mode] 可以有三个取值:
- [client]:插件必须仅在客户端执行;
- [server]:插件必须仅在服务器端执行;
- 缺少 [mode] 键:在此情况下,插件必须在客户端和服务器端同时执行;
- 第 5-6 行:我们将两个插件放置在 [plugins] 文件夹中。这并非强制要求。插件可以放置在项目目录结构的任意位置。同样,子文件夹 [client, server] 的名称在此处也是任意的;

9.3. 服务器端 [session] 插件
[server / session.js] 插件如下:
/* eslint-disable no-console */
export default (context, inject) => {
// server session management
// is there an existing session?
let value = context.app.$cookies.get('session')
if (!value) {
// new session
console.log("[plugin session server], démarrage d'une nouvelle session")
value = initValue
} else {
// existing session
console.log("[plugin session server], reprise d'une session existante")
}
// session definition
const session = {
// session content
value,
// save the session in a cookie
save(context) {
context.app.$cookies.set('session', this.value, { path: context.base, maxAge: context.env.maxAge })
}
}
// we inject a function into [context, Vue] that will render the current session
inject('session', () => session)
}
// initial session value
const initValue = {
initSessionDone: false
}
- 第 2 行:每次有服务器请求时都会执行插件:包括启动时,以及用户通过手动输入 URL 强制发起服务器请求时:
- 首先,执行服务器端插件;
- 一旦客户端浏览器收到服务器的响应,就轮到客户端插件运行;
- 第 2 行:每个插件(无论是客户端还是服务器端)都会接收两个参数:
- [context]:服务器或客户端上下文,取决于执行插件的一方;
- [inject]:一个允许将函数注入到服务器或客户端上下文中的函数;
- [server / session] 插件有两个目的:
- 定义会话(第 16–23 行);
- 在上下文中定义一个名为 [$session] 的函数,该函数返回第 16 行定义的会话。第 25 行实现了这一功能;
- 第 16–23 行:会话将把其数据封装在第 18 行的 [value] 对象中;
- 第 20–22 行:它有一个 [save] 函数,该函数将 [context] 对象作为参数。调用代码提供此上下文。借助该上下文,[save] 函数将会话值(即 [value] 对象)保存到会话 Cookie 中;
- 第 6 行:当 [server / session] 插件运行时,它首先检查服务器是否已收到会话 Cookie;
- 如果已收到,则第 6 行中的 [value] 对象即代表会话值,即其内部封装的数据集;
- 若未收到,则在第 7–11 行设置会话的初始值。这将对应第 29–31 行中的 [initValue] 对象。会话元素将在 [nuxtServerInit] 函数中定义,该函数在服务器插件执行后运行;
- 第 18 行:[value] 语法是 [value:value] 语法的简写形式。左侧的 [value] 是对象键的名称;右侧的 [value] 是第 6 行声明的 [value] 对象;
- 第 25 行:当执行到这一行时,会话要么因不存在而被创建,要么从客户端浏览器的 HTTP 请求中获取;
- 第 25 行:我们将一个新函数注入到服务器上下文中:
- [inject] 的第一个参数是正在创建的函数名称,此处为 ‘session’。[nuxt] 实际上会将其命名为 ‘$session’;
- 第二个参数是函数定义。在此,[$session] 函数
- 不接受任何参数;
- 返回第 16 行中的 [session] 对象;
- 插件执行后:
- 在 [context] 对象可用的任何位置,[$session] 函数均可通过 [context.app.$session] 访问;而在视图或 [vuex] 存储中,则可通过 [this.$session] 访问;
- [$session] 函数返回一个包含单个 [value] 键的 [session] 对象;
- 在初始会话创建时,[value] 对象仅包含一个键 [initStoreDone](第 29–31 行)。键 [initStoreDone:false] 表示存储尚未被添加到会话中。此操作将由 [nuxtServerInit] 函数完成;
9.4. 会话初始化
一旦服务器执行了 [session / server] 插件,服务器将执行以下 [store / index.js] 脚本:
/* eslint-disable no-console */
export const state = () => ({
// meter
counter: 0
})
export const mutations = {
// increment counter by one [inc] value
increment(state, inc) {
state.counter += inc
},
// state replacement
replace(state, newState) {
for (const attr in newState) {
state[attr] = newState[attr]
}
}
}
export const actions = {
async nuxtServerInit(store, context) {
// who executes this code?
console.log('nuxtServerInit, client=', process.client, 'serveur=', process.server, 'env=', context.env)
// waiting for a promise to be fulfilled
await new Promise(function(resolve, reject) {
// this is normally an asynchronous function
// we simulate it with a one-second wait
setTimeout(() => {
// init session
initSession(store, context)
// success
resolve()
}, 1000)
})
}
}
function initSession(store, context) {
// store is the blind to be initialized
// retrieve the session
const session = context.app.$session()
// has the session already been initiated?
if (!session.value.initSessionDone) {
// start a new blind
console.log("nuxtServerInit, initialisation d'une nouvelle session")
// initialize the blind
store.commit('increment', 77)
// put the blind in the session
session.value.store = store.state
// initialize a new session
session.value.somethingImportant = { x: 2, y: 4 }
// the session is now initialized
session.value.initSessionDone = true
} else {
console.log("nuxtServerInit, reprise d'un store existant")
// update the store with the session store
store.commit('replace', session.value.store)
}
// save the session
session.save(context)
// log
console.log('initSession terminé, store=', store.state, 'session=', session.value)
}
与 [nuxt-05] 项目中的 store 相比,仅第 38–60 行中的 [initSession] 函数(原名为 initStore)发生了变化:
- 第 42 行:我们使用 [$session] 函数获取会话,该函数已被注入到服务器上下文中;
- 第 44 行:检查会话是否已初始化;
- 第 45–54 行:若未初始化:
- 第 48 行:初始化存储;
- 第 50 行:将存储器的状态存入会话;
- 第 52 行:我们将另一个对象 [somethingImportant] 添加到会话中。该对象不会成为存储器的一部分;
- 第 54 行:我们记录会话现已初始化;
- 第 55–59 行:如果会话已初始化:
- 第 58 行:使用会话的内容初始化新的存储器;
- 第 61 行:会话被保存在会话 Cookie 中。请注意,这涉及将 Cookie 放入服务器将发送给客户端浏览器的 HTTP 响应中;
9.5. 客户端的 [client / session] 插件
服务器执行完 [plugins / server / session] 和 [store / index] 脚本后,会将 [index, page1] 中的某一个页面发送至客户端浏览器。服务器的 HTTP 响应中将包含会话 Cookie。当客户端浏览器接收到页面后,页面中嵌入的客户端脚本将开始执行。随后 [client / session] 插件将执行:
/* eslint-disable no-console */
export default (context, inject) => {
// customer session management
// the session necessarily exists, initialized by the server
console.log('[plugin session client], reprise de la session du serveur')
// session definition
const session = {
// session content
value: context.app.$cookies.get('session'),
// save the session in a cookie
save(context) {
context.app.$cookies.set('session', this.value, { path: context.base, maxAge: context.env.maxAge })
}
}
// we inject a function into [context, Vue] that will render the current session
inject('session', () => session)
}
- 当客户端插件运行时,客户端浏览器已经接收到了会话 Cookie;
- [client] 插件的目标是将一个名为 [$session] 的函数注入到客户端上下文中。该函数将返回服务器发送的会话;
- 第 19 行:注入的 [$session] 函数将返回第 9–16 行中的会话;
- 第 9–16 行:由客户端管理的 [session] 对象。这将是服务器发送的会话的副本;
- 第 11 行:从 [nuxt] 服务器发送的会话 Cookie 中获取客户端会话的值;
- 第 13–15 行:与服务器会话类似,客户端会话也具有 [save] 函数,该函数允许将会话值(第 14 行中的 [this.value])保存到浏览器中存储的会话 Cookie 中;
9.6. [index] 页面
[index] 页面的演变过程如下:
<!-- page [index] -->
<template>
<Layout :left="true" :right="true">
<!-- navigation -->
<Navigation slot="left" />
<!-- message-->
<template slot="right">
<b-alert show variant="warning"> Home - session= {{ jsonSession }}, counter= {{ $store.state.counter }} </b-alert>
<!-- bouton -->
<b-button @click="incrementCounter" class="ml-3" variant="primary">Incrémenter</b-button>
</template>
</Layout>
</template>
<script>
/* eslint-disable no-undef */
/* eslint-disable no-console */
/* eslint-disable nuxt/no-env-in-hooks */
import Layout from '@/components/layout'
import Navigation from '@/components/navigation'
export default {
name: 'Home',
// components used
components: {
Layout,
Navigation
},
computed: {
jsonSession() {
return JSON.stringify(this.$session().value)
}
},
// life cycle
beforeCreate() {
// client and server
console.log('[home beforeCreate]')
},
created() {
// client and server
console.log('[home created], session=', this.$session().value)
},
beforeMount() {
// customer only
console.log('[home beforeMount]')
},
mounted() {
// customer only
console.log('[home mounted]')
},
// event management
methods: {
incrementCounter() {
console.log('incrementCounter')
// counter increment of 1
this.$store.commit('increment', 1)
// session modification
const session = this.$session()
session.value.store = this.$store.state
session.value.somethingImportant.x++
session.value.somethingImportant.y++
// save session in session cookie
session.save(this.$nuxt.context)
}
}
}
</script>
请注意,此页面在服务器和客户端两端均会运行。
- 第 8 行:现在我们同时显示会话和存储;
- 第 30 行:[jsonSession] 是一个计算属性,返回会话值的 JSON 字符串;
- 第 41 行:我们使用注入的函数 [this.$session] 显示会话值。该函数在服务器和客户端上下文中均存在;
- 第 53 行:[incrementCounter] 方法仅在客户端执行;
- 第 56 行:盲计数器被递增并如前所示显示;
- 第 58 行:使用注入函数 [this.$session] 获取会话;
- 第 59 行:更新会话存储;
- 第 60–61 行:我们递增会话属性 [somethingImportant.x, somethingImportant.y]。这仅是为了展示会话除了存储数据外,还可以用于传递其他数据;
- 第 63 行:将会话保存到浏览器中的会话 Cookie 中。在客户端视图中,可通过 [this.$nuxt.context] 访问会话上下文;
[index] 页面的目的是演示会话本身并非响应式的,而存储则是响应式的。当我们增加会话元素的值时,会发现视图并未更新。[page1] 视图提供了解决此问题的方案。
9.7. [page1] 页面
[page1] 页面是通过复制 [index] 页面并稍作修改创建的:
<!-- page [index] -->
<template>
<Layout :left="true" :right="true">
<!-- navigation -->
<Navigation slot="left" />
<!-- message-->
<template slot="right">
<b-alert show variant="warning"> Page1 - session= {{ jsonSession }}, counter= {{ $store.state.counter }} </b-alert>
<!-- bouton -->
<b-button @click="incrementCounter" class="ml-3" variant="primary">Incrémenter</b-button>
</template>
</Layout>
</template>
<script>
/* eslint-disable no-undef */
/* eslint-disable no-console */
/* eslint-disable nuxt/no-env-in-hooks */
import Layout from '@/components/layout'
import Navigation from '@/components/navigation'
export default {
name: 'Page1',
// components used
components: {
Layout,
Navigation
},
data() {
return {
session: {}
}
},
computed: {
jsonSession() {
return JSON.stringify(this.session.value)
}
},
// life cycle
beforeCreate() {
// client and server
console.log('[page1 beforeCreate]')
},
created() {
// client and server
// set the session in the page's reactive properties
this.session = this.$session()
// log
console.log('[page1 created], session=', this.session.value)
},
beforeMount() {
// customer only
console.log('[page1 beforeMount]')
},
mounted() {
// customer only
console.log('[page1 mounted]')
},
// event management
methods: {
incrementCounter() {
console.log('incrementCounter')
// counter increment of 1
this.$store.commit('increment', 1)
// session modification
this.session.value.store = this.$store.state
this.session.value.somethingImportant.x++
this.session.value.somethingImportant.y++
// save session in session cookie
this.session.save(this.$nuxt.context)
}
}
}
</script>
- 第 47 行:主要区别在于我们将当前会话添加到了页面的属性中(第 29–33 行)。这将使会话具有响应性。当 [incrementCounter] 函数递增会话中的元素时,[page1] 视图将会更新;
9.8. 运行项目
在运行项目之前,请检查浏览器的会话 Cookie,如果存在,请将其删除,以便服务器创建一个新的会话:

现在让我们请求 URL [http://localhost:81/nuxt-06/]:

随后浏览器的日志如下:

- 在 [2] 中,服务器在服务器的 [session] 插件中启动了一个新会话;
- 在 [3] 中,该新会话在 [nuxtServerInit] 中被初始化;
- 在 [4] 中,服务器端已识别该新会话;
- 在 [5] 中,客户端已成功获取该会话;
现在让我们将计数器递增三次:

- 在 [3] 中,计数器已被递增,但 [2] 中的会话并未更新。虽然 [3] 显示的是响应式的存储,但 [2] 显示的是非响应式的会话:
现在让我们刷新页面(F5)。刷新后日志如下:

- 在 [2] 中,我们可以看到服务器接收到了客户端浏览器发送的会话 Cookie;
- 在 [4] 中,我们可以看到存储值并未重置,而是从接收的会话中继承而来;
- 在 [4-5] 中:我们可以看到会话属性确实都已递增了三次;
随后服务器发送的页面如下:

从该页面得出的结论是:会话除了存储之外还可以携带其他元素,但这些元素不具备响应式特性。
现在,让我们点击 [第 1 页] [第 4 页] 链接。显示的新页面如下:

接着,让我们点击三次 [Increment] 按钮。页面将变为如下所示:

这次,会话在 [2] 中显示正确了。这里是响应式的。这可以在日志中看到:

- 在 [1-3] 中,是会话的值;
- 在 [4-6] 中,会话元素的响应式 getter 和 setter;
现在,让我们点击 [Home] 链接 [4]。我们将看到以下页面:

然后点击 [Increment] 按钮 [4] 两次。页面将变为如下所示:

我们可以看到,这里会话也已变为响应式 [2]。
让我们获取 [this.$session()] 函数返回的值:

- 在 [View] 选项卡中,选择当前页面 [Home] 以获取其引用 [$vm0] [3];
然后,在 [Console] 选项卡 [4] 中,让我们获取函数 [$vm0.$session()] 的值:

- 在 [5] 中,我们可以看到会话已变为响应式,而最初它并非如此;
- 在 [6] 中,我们请求会话的值;
- 在 [7-8] 中,我们发现该值也已变为响应式;
因此,这里出现了一个意想不到的结果:如果某个元素因被放入页面的属性中而在该页面上变得响应式,那么即使在它不属于该页面属性的其他页面上,它也会变得响应式。
9.9. 结论
示例 [nuxt-05] 展示了我们可以在向服务器发起的请求之间持久化存储。示例 [nuxt-06] 通过一个我们类比于 Web 会话而命名为 [session] 的对象实现了相同的功能。我们看到,尽管该会话本身并非原生响应式,但它可以拥有与 [Vuex] 存储相同的属性,并同样变得响应式。
那么 [Vuex] 存储的意义何在?我必须承认,目前我尚未完全弄清楚。很可能是我遗漏了某些关键点。因此,在存疑的情况下,我建议使用:
- 使用 [Vuex] 存储来存放所有需要在客户端页面之间共享的内容,以及可能需要在客户端和服务器之间共享的内容;
- 如果需要在客户端到服务器的请求过程中保持存储器的持久性,则使用会话 Cookie,且该会话仅包含存储器;
示例 [nuxt-05] 和 [nuxt-06] 的目的是展示当用户通过手动输入 URL 强制调用服务器时,如何确保应用程序的连续性。请注意,在此情况下默认行为是重启应用程序,这会导致其当前状态丢失。