Skip to content

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] 项目创建的:

Image

  • 在 [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] 的名称在此处也是任意的;

Image

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,如果存在,请将其删除,以便服务器创建一个新的会话:

Image

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

Image

随后浏览器的日志如下:

Image

  • 在 [2] 中,服务器在服务器的 [session] 插件中启动了一个新会话;
  • 在 [3] 中,该新会话在 [nuxtServerInit] 中被初始化;
  • 在 [4] 中,服务器端已识别该新会话;
  • 在 [5] 中,客户端已成功获取该会话;

现在让我们将计数器递增三次:

Image

  • 在 [3] 中,计数器已被递增,但 [2] 中的会话并未更新。虽然 [3] 显示的是响应式的存储,但 [2] 显示的是非响应式的会话:

现在让我们刷新页面(F5)。刷新后日志如下:

Image

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

随后服务器发送的页面如下:

Image

从该页面得出的结论是:会话除了存储之外还可以携带其他元素,但这些元素不具备响应式特性。

现在,让我们点击 [第 1 页] [第 4 页] 链接。显示的新页面如下:

Image

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

Image

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

Image

  • 在 [1-3] 中,是会话的值;
  • 在 [4-6] 中,会话元素的响应式 getter 和 setter;

现在,让我们点击 [Home] 链接 [4]。我们将看到以下页面:

Image

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

Image

我们可以看到,这里会话也已变为响应式 [2]。

让我们获取 [this.$session()] 函数返回的值:

Image

  • 在 [View] 选项卡中,选择当前页面 [Home] 以获取其引用 [$vm0] [3];

然后,在 [Console] 选项卡 [4] 中,让我们获取函数 [$vm0.$session()] 的值:

Image

  • 在 [5] 中,我们可以看到会话已变为响应式,而最初它并非如此;
  • 在 [6] 中,我们请求会话的值;
  • 在 [7-8] 中,我们发现该值也已变为响应式;

因此,这里出现了一个意想不到的结果:如果某个元素因被放入页面的属性中而在该页面上变得响应式,那么即使在它不属于该页面属性的其他页面上,它也会变得响应式。

9.9. 结论

示例 [nuxt-05] 展示了我们可以在向服务器发起的请求之间持久化存储。示例 [nuxt-06] 通过一个我们类比于 Web 会话而命名为 [session] 的对象实现了相同的功能。我们看到,尽管该会话本身并非原生响应式,但它可以拥有与 [Vuex] 存储相同的属性,并同样变得响应式。

那么 [Vuex] 存储的意义何在?我必须承认,目前我尚未完全弄清楚。很可能是我遗漏了某些关键点。因此,在存疑的情况下,我建议使用:

  • 使用 [Vuex] 存储来存放所有需要在客户端页面之间共享的内容,以及可能需要在客户端和服务器之间共享的内容;
  • 如果需要在客户端到服务器的请求过程中保持存储器的持久性,则使用会话 Cookie,且该会话仅包含存储器;

示例 [nuxt-05] 和 [nuxt-06] 的目的是展示当用户通过手动输入 URL 强制调用服务器时,如何确保应用程序的连续性。请注意,在此情况下默认行为是重启应用程序,这会导致其当前状态丢失。