9. 示例 [nuxt-06]:在会话管理器上下文中注入
9.1. Présentation
示例 [nuxt-05] 表明,即使用户强制调用服务器,存储区仍可保持持久化。 存储中的元素具有响应性,因此若将其集成到视图中,视图将对存储中的变化做出响应。我们也可能希望在客户端与服务器交互过程中持久化某些元素,但并不希望它们具有响应性,仅仅是因为它们未被视图显示。此时,我们可以将这些元素存储在会话中,而无需将其放入存储中。
可以通过诸如视图外的 [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) => {
// 服务器会话管理
// 是否存在会话?
let value = context.app.$cookies.get('session')
if (!value) {
// 新建会话
console.log("[plugin session server], démarrage d'une nouvelle session")
value = initValue
} else {
// 已有会话
console.log("[plugin session server], reprise d'une session existante")
}
// 会话定义
const session = {
// 会话内容
value,
// 将会话保存到 Cookie 中
save(context) {
context.app.$cookies.set('session', this.value, { path: context.base, maxAge: context.env.maxAge })
}
}
// 向 [context, Vue] 注入一个函数,使其成为当前会话
inject('session', () => session)
}
// 会话的初始值
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];
- 插件执行后:
- 函数 [$session] 可在 [context.app.$session] 中使用,该处可访问对象 [context], 或在视图中,或在 [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 = () => ({
// 计数器
counter: 0
})
export const mutations = {
// 计数器增加一个值[inc]
increment(state, inc) {
state.counter += inc
},
// 状态更新
replace(state, newState) {
for (const attr in newState) {
state[attr] = newState[attr]
}
}
}
export const actions = {
async nuxtServerInit(store, context) {
// 谁在执行这段代码?
console.log('nuxtServerInit, client=', process.client, 'serveur=', process.server, 'env=', context.env)
// 等待一个 Promise 完成
await new Promise(function(resolve, reject) {
// 这里通常有一个异步函数
// 通过等待一秒来模拟它
setTimeout(() => {
// 初始化会话
initSession(store, context)
// 成功
resolve()
}, 1000)
})
}
}
function initSession(store, context) {
// store 是待初始化的存储器
// 获取会话
const session = context.app.$session()
// 会话是否已初始化?
if (!session.value.initSessionDone) {
// 启动一个新的存储区
console.log("nuxtServerInit, initialisation d'une nouvelle session")
// 初始化存储区
store.commit('increment', 77)
// 将存储器放入会话
session.value.store = store.state
// 正在初始化一个新的会话
session.value.somethingImportant = { x: 2, y: 4 }
// 会话现已初始化
session.value.initSessionDone = true
} else {
console.log("nuxtServerInit, reprise d'un store existant")
// 使用会话存储更新存储
store.commit('replace', session.value.store)
}
// 保存会话
session.save(context)
// 日志
console.log('initSession terminé, store=', store.state, 'session=', session.value)
}
与项目[nuxt-05]的代码库相比,仅第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) => {
// 客户端会话管理
// 会话必然存在,由服务器初始化
console.log('[plugin session client], reprise de la session du serveur')
// 会话定义
const session = {
// 会话内容
value: context.app.$cookies.get('session'),
// 将会话保存到 Cookie 中
save(context) {
context.app.$cookies.set('session', this.value, { path: context.base, maxAge: context.env.maxAge })
}
}
// 向 [context, Vue] 注入一个函数,使其成为当前会话
inject('session', () => session)
}
- 当客户端插件执行时,客户端浏览器已接收该会话cookie;
- [client] 插件的目标同样是向客户端上下文中注入一个名为 [$session] 的函数。该函数将使服务器发送的会话失效;
- 第 19 行:注入的函数 [$session] 将返回第 9-16 行中的会话;
- 第 9-16 行:由客户端管理的 [session] 对象。这将是服务器发送的会话的副本;
- 第 11 行:客户端会话的值取自服务器发送的会话 Cookie [nuxt];
- 第13-15行:与服务器会话类似,客户端会话也包含一个名为[save]的功能,用于将会话值(第14行的[this.value])保存到浏览器中存储的会话cookie中;
9.6. 页面 [index]
页面 [index] 的演变如下:
<!-- 页面 [index] -->
<template>
<Layout :left="true" :right="true">
<!-- 导航 -->
<Navigation slot="left" />
<!-- 消息-->
<template slot="right">
<b-alert show variant="warning"> Home - session= {{ jsonSession }}, counter= {{ $store.state.counter }} </b-alert>
<!-- 按钮 -->
<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: {
Layout,
Navigation
},
computed: {
jsonSession() {
return JSON.stringify(this.$session().value)
}
},
// 生命周期
beforeCreate() {
// 客户端与服务器
console.log('[home beforeCreate]')
},
created() {
// 客户端与服务器
console.log('[home created], session=', this.$session().value)
},
beforeMount() {
// 仅客户端
console.log('[home beforeMount]')
},
mounted() {
// 仅客户端
console.log('[home mounted]')
},
// 事件管理
methods: {
incrementCounter() {
console.log('incrementCounter')
// 计数器递增 1
this.$store.commit('increment', 1)
// 修改会话
const session = this.$session()
session.value.store = this.$store.state
session.value.somethingImportant.x++
session.value.somethingImportant.y++
// 将会话保存到会话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] 并稍作修改而获得的:
<!-- 页面 [index] -->
<template>
<Layout :left="true" :right="true">
<!-- 导航 -->
<Navigation slot="left" />
<!-- 消息-->
<template slot="right">
<b-alert show variant="warning"> Page1 - session= {{ jsonSession }}, counter= {{ $store.state.counter }} </b-alert>
<!-- 按钮 -->
<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: {
Layout,
Navigation
},
data() {
return {
session: {}
}
},
computed: {
jsonSession() {
return JSON.stringify(this.session.value)
}
},
// 生命周期
beforeCreate() {
// 客户端与服务器
console.log('[page1 beforeCreate]')
},
created() {
// 客户端与服务器
// 将会话放入页面的响应式属性中
this.session = this.$session()
// 日志
console.log('[page1 created], session=', this.session.value)
},
beforeMount() {
// 仅客户端
console.log('[page1 beforeMount]')
},
mounted() {
// 仅客户端
console.log('[page1 mounted]')
},
// 事件管理
methods: {
incrementCounter() {
console.log('incrementCounter')
// 计数器递增 1
this.$store.commit('increment', 1)
// 修改会话
this.session.value.store = this.$store.state
this.session.value.somethingImportant.x++
this.session.value.somethingImportant.y++
// 将会话保存到会话 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] 中:可以看到会话的属性确实都已增加三次;
服务器发送的页面如下:

由此页面得出的结论是:会话除了存储外还可以携带其他元素,但这些元素不具备响应性。
现在点击链接 [Page 1] [4]。此时显示的新页面如下:

接着,点击 [Incrémenter] 按钮三次。页面变为如下所示:

这次会话在 [2] 中显示正确。此时会话是可交互的。这在日志中可见:

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

然后双击按钮 [Incrémenter] [4]。页面变为如下所示:

我们可以看到,此时会话也已变为活动状态:[2]。
查询函数 [this.$session()] 返回的值:

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

- 在 [5] 中,可以看到会话已变为活动状态,而最初并非如此;
- 在 [6] 中,查询会话的值;
- 在 [7-8] 中,发现该值也已变为响应式;
因此,这里出现了一个意外的结果:如果某个元素因被添加到页面属性中而在该页面上变得可响应,那么即使在它未被添加到页面属性的其他页面中,它也会变得可响应。
9.9. Conclusion
示例 [nuxt-05] 展示了如何在向服务器发送的请求过程中持久化数据存储。示例 [nuxt-06] 通过一个名为 [session] 的对象实现了相同功能,该名称是参照 Web 会话命名的。 我们看到,该会话可以拥有与存储 [Vuex] 相同的属性,并同样变得具有响应性,尽管其原生状态并非如此。
那么,存储 [Vuex] 的意义何在?我必须承认,目前我尚未发现其用途。很可能是我遗漏了某些细节。因此,在存疑的情况下,我建议使用:
- 一个 [Vuex] 存储区,用于存放所有需要在客户端页面之间共享的数据,以及可能需要在客户端与服务器之间共享的数据;
- 如果存储在客户端向服务器发起请求时需要保持持久化,则使用会话cookie,此时会话中仅包含该存储;
示例 [nuxt-05] 和 [nuxt-06] 的目的是展示当用户通过手动输入 URL 强制调用服务器时,如何确保应用程序的连续性。 需要提醒的是,在此情况下,默认行为是应用程序重启,其当前状态将因此丢失。