17. 示例 [nuxt-20]:基于示例 [vuejs-22] 的移植
17.1. Présentation
本文旨在将示例 [vuejs-22](该示例是类型为 SPA 的应用程序 [vue.js])移植到 [nuxt] SSR 环境中。 [vuejs-22]是税务计算服务器的客户端应用程序,包含以下视图:
第一个视图是身份验证视图:

第二个视图是税款计算视图:

第三个视图是显示用户所做模拟列表的视图:

上图显示可以删除第1个模拟。随后将显示以下界面:

如果现在删除最后一个模拟,将显示如下新视图:

我们将逐步将应用程序 [vuejs-22] 迁移至应用程序 [nuxt-20]。此处不再赘述 [vuejs-22] 的代码。 建议读者重新阅读文档《通过示例了解 VUE.JS 框架》。各个步骤将展示 [vuejs] 应用程序与 [nuxt] 应用程序之间的差异。
17.2. 步骤 1
[nuxt-20] 项目最初是通过复制 [nuxt-12] 项目获得的。该项目确实是一个很好的起点:
- 它能够与税务计算服务器进行交互;
- 能正确处理服务器返回的错误;
- [nuxt]的客户端和服务器能够通过[nuxt]会话进行通信;
因此我们拥有良好的初始基础设施。我们的主要工作应是修改:
- 页面。我们将采用 [vuejs-22] 项目中的页面,并需将其适配至新环境;
- 商店管理功能。部分信息(如模拟列表)需新增,而其他信息可能不再需要;
- 客户端和服务器路由管理(项目[nuxt]);
因此,首先通过复制项目 [nuxt-12] 来创建项目 [nuxt-20]:

然后删除不再需要的页面和组件 [2]:
- 组件 [components/navigation] 消失;
- 布局 [layout/default] 消失;
- 页面 [index, authentification, get-admindata, fin-session] 消失;
然后将 [vuejs-22] 和 [3] 中的元素整合到 [nuxt-20] 中:
- 应用程序 [vuejs-22] 中的三个页面 [Authentification, CalculImpot, ListeSimulations] 移至文件夹 [pages];
- 应用程序 [vuejs-22] 中的组件 [FormCalculImpot, Menu, Layout] 移至文件夹 [components];
- [vuejs-22] 中的页面 [Main](此前作为 [layout] 服务于应用程序 [vuejs-22])将移至文件夹 [layouts];
将嵌入元素 [4] 重命名为:

- 在 [layouts] 中,[Main] 已更名为 [default],因为这是应用程序 [nuxt] 布局的默认名称;
- 在 [pages] 中,页面 [Authentification] 已变为 [index],因为在应用程序 [vuejs-22] 中,[Authentification] 承担了这一角色;
此时,我们可以编译该项目以查看最初的错误。修改示例 [nuxt-12] 中的文件 [nuxt.config],以便现在执行 [nuxt-20]:
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: false,
/*
** Global CSS
*/
css: [],
/*
** Plugins to load before mounting the App
*/
plugins: [
{ src: '@/plugins/client/plgSession', mode: 'client' },
{ src: '@/plugins/server/plgSession', mode: 'server' },
{ src: '@/plugins/client/plgDao', mode: 'client' },
{ src: '@/plugins/server/plgDao', mode: 'server' },
{ src: '@/plugins/client/plgEventBus', mode: 'client' }
],
/*
** 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',
// https://www.npmjs.com/package/cookie-universal-nuxt
'cookie-universal-nuxt'
],
/*
** Axios module configuration
** See https://axios.nuxtjs.org/options
*/
axios: {},
/*
** Build configuration
*/
build: {
/*
** You can extend webpack config here
*/
extend(config, ctx) {}
},
// 源代码目录
srcDir: 'nuxt-20',
// 路由器
router: {
// 应用程序的 URL 根目录
base: '/nuxt-20/',
// 路由中间件
middleware: ['routing']
},
// 服务器
server: {
// 服务端口,默认 3000
port: 81,
// 监听的网络地址,默认 localhost:127.0.0.1
// 0.0.0.0 = 该机器的所有网络地址
host: 'localhost'
},
// 环境
env: {
// axios 配置
timeout: 2000,
withCredentials: true,
baseURL: 'http://localhost/php7/scripts-web/impots/version-14',
// 会话cookie配置 [nuxt]
maxAge: 60 * 5
}
}
随后对该项目生成 [build]:

报告的错误如下:
- 第 1 行的错误表明引用了一张不存在的图片。我们将在 [vuejs-22] 中获取该图片;
- 第 2 行错误显示组件 [./FormCalculImpot] 不存在。实际上,该组件现已移至 [@/components/form-calcul-impot];
- [3-5] 行中的错误表明组件 [./Layout] 不存在。实际上,该组件现已移至 [@/components/layout] 中;
- [6-7] 行中的错误表明组件 [./Menu] 不存在。确实,它现在名为 [@/components/menu];
将图像 [assets/logo.jpg] 添加到项目 [nuxt-20] 中:

此外,我们将修正所有页面中组件的路径。以页面 [calcul-impot] 为例:
<!-- 视图定义 HTML -->
<template>
<div>
<Layout :left="true" :right="true">
<!-- 右侧税款计算表单 -->
<FormCalculImpot slot="right" @resultatObtenu="handleResultatObtenu" />
<!-- 左侧导航菜单 -->
<Menu slot="left" :options="options" />
</Layout>
<!-- 表单下方的税额计算结果显示区 -->
<b-row v-if="résultatObtenu" class="mt-3">
<!-- 三列空白区域 -->
<b-col sm="3" />
<!-- 九列区域 -->
<b-col sm="9">
<b-alert show variant="success">
<span v-html="résultat"></span>
</b-alert>
</b-col>
</b-row>
</div>
</template>
<script>
// 导入
import FormCalculImpot from './FormCalculImpot'
import Menu from './Menu'
import Layout from './Layout'
export default {
// 所用组件
components: {
Layout,
FormCalculImpot,
Menu
},
第26至28行中的三个[import]将修改为:
// 导入
import FormCalculImpot from '@/components/form-calcul-impot'
import Menu from '@/components/menu'
import Layout from '@/components/layout'
通过这种方式,检查并修正所有组件、布局和页面中的 [import]。完成这些修正后,可以尝试生成新的 [build]。通常此时不会再出现错误。
此时可以尝试执行:

此时会出现以下错误:
可见,[dev]命令与[eslint]模块结合使用时,在语法层面比[build]命令更为严格。 在此,系统要求将比较运算符 [!=] 改写为 [!==],后者是一个更严格的运算符(它还会检查操作数的类型)。这些错误出现在页面 [index.vue] 中。
修正上述错误后,重新运行项目。此时,[eslint] 模块发出警告:

通过[eslint]和[2]模块中的[Quick fix]修正此错误。
重新运行项目。此时不再出现编译错误。随后通过浏览器调用 URL 和 [http://localhost:81/nuxt-20/]。结果出现运行错误:

错误出现在 [index.vue] [2] 中。错误 [1] 的原因在于,在 [vuejs-22] 中, [dao] 层在 [this.$dao] 中可用,而在我们采用其架构的 [nuxt-12] 中,该层在 [this.$dao()] 函数中可用。
错误出现在页面生命周期 [index] 的函数 [created] 中:

目前,我们只需将 [created] 重命名为 [created2],以避免执行生命周期函数 [created](即 [3])。
保存修改后,在浏览器中重新加载页面 [index]。这次成功了:

17.3. 步骤 2
项目中的页面 [vuejs-22] 使用了以下注入元素:
- $dao:用于客户端 [vue.js] 的 [dao] 层;
- $session:用于存储在浏览器 [localStorage] 中的会话;
在我们复制的 [nuxt-12] 项目架构中,这些元素已不复存在:
- 现在有两个 [dao] 层,一个用于客户端 [nuxt],另一个用于服务器 [nuxt]。 这两个层均可通过名为 [$dao] 的注入函数访问。这意味着在应用程序的页面中,[this.$dao] 必须替换为 [this.$dao()];
- 由应用程序 [nuxt-20] 管理的会话 [nuxt] 已与应用程序 [$session] 中的,该应用程序中并不存在会话cookie的概念。尽管如此,它们具有相似的功能:在用户操作过程中存储持久性信息。会话 [nuxt] 将信息存储在存储器中,而非直接存储在会话中。 在应用程序的页面中,当需要将信息存储在会话中时,应将 [this.$session] 替换为 [this.$store];当需要操作会话本身时,则应替换为 [this.$session()];
- 若要查询存储器中属性 P 的状态,需使用 [this.$store.state.P];
- 若要修改存储器的属性 P,需编写 [this.$store.commit(‘replace’, {P:value}];
我们在页面 [index] 中进行这些修改:
<!-- 视图定义 HTML -->
<template>
<Layout :left="false" :right="true">
<template slot="right">
<!-- 表单 HTML - 使用操作 [authentifier-utilisateur] 提交其值 -->
<b-form @submit.prevent="login">
<!-- 标题 -->
<b-alert show variant="primary">
<h4>Bienvenue. Veuillez vous authentifier pour vous connecter</h4>
</b-alert>
<!-- 第一行 -->
<b-form-group label="Nom d'utilisateur" label-for="user" label-cols="3">
<!-- 用户输入区 -->
<b-col cols="6">
<b-form-input id="user" v-model="user" type="text" placeholder="Nom d'utilisateur" />
</b-col>
</b-form-group>
<!-- 第二行 -->
<b-form-group label="Mot de passe" label-for="password" label-cols="3">
<!-- 密码输入框 -->
<b-col cols="6">
<b-input id="password" v-model="password" type="password" placeholder="Mot de passe" />
</b-col>
</b-form-group>
<!-- 第三行 -->
<b-alert v-if="showError" show variant="danger" class="mt-3">L'erreur suivante s'est produite : {{ message }}</b-alert>
<!-- 第三行上的 [submit] 类型按钮 -->
<b-row>
<b-col cols="2">
<b-button :disabled="!valid" variant="primary" type="submit">Valider</b-button>
</b-col>
</b-row>
</b-form>
</template>
</Layout>
</template>
<!-- 视图动态 -->
<script>
/* eslint-disable no-console */
import Layout from '@/components/layout'
export default {
// 使用的组件
components: {
Layout
},
// 组件状态
data() {
return {
// 用户
user: '',
// 其密码
password: '',
// 控制错误消息的显示
showError: false,
// 错误消息
message: ''
}
},
// 计算属性
computed: {
// 有效输入
valid() {
return this.user && this.password && this.$store.state.started
}
},
// 生命周期:组件刚被创建
mounted() {
// eslint-disable-next-line
console.log("Authentification mounted");
// 用户能否进行模拟?
if (this.$store.state.started && this.$store.state.authenticated && this.$métier.taxAdminData) {
// 那么用户可以进行模拟
this.$router.push({ name: 'calculImpot' })
// 返回事件循环
return
}
// 如果会话 jSON 已启动,则不再重新启动
if (!this.$store.state.started) {
// 开始等待
this.$emit('loading', true)
// 与服务器初始化会话 - 异步请求
// 使用 [dao] 层方法返回的 Promise
this.$dao()
// 初始化会话 jSON
.initSession()
// 已获取响应
.then((response) => {
// 等待结束
this.$emit('loading', false)
// 分析响应
if (response.état !== 700) {
// 显示错误
this.message = response.réponse
this.showError = true
// 返回事件循环
return
}
// 会话已启动
this.$store.commit('replace', { started: true })
console.log('[authentification], session=', this.$session())
})
// 发生错误时
.catch((error) => {
// 将错误上报至视图 [Main]
this.$emit('error', error)
})
// 无论何种情况
.finally(() => {
// 保存会话
this.$session().save()
})
}
},
// 事件处理程序
methods: {
// ----------- 身份验证
async login() {
try {
// 开始等待
this.$emit('loading', true)
// 尚未通过身份验证
this.$store.commit('replace', { authenticated: false })
// 向服务器进行阻塞式身份验证
const response = await this.$dao().authentifierUtilisateur(this.user, this.password)
// 加载完成
this.$emit('loading', false)
// 分析服务器响应
if (response.état !== 200) {
// 显示错误
this.message = response.réponse
this.showError = true
// 返回事件循环
return
}
// 无错误
this.showError = false
// 身份验证通过
this.$store.commit('replace', { authenticated: true })
// --------- 现在请求税务管理数据
// 初始时无数据
this.$métier.setTaxAdminData(null)
// 开始等待
this.$emit('loading', true)
// 向服务器发送阻塞请求
const response2 = await this.$dao().getAdminData()
// 加载完成
this.$emit('loading', false)
// 响应分析
if (response2.état !== 1000) {
// 显示错误
this.message = response2.réponse
this.showError = true
// 返回事件循环
return
}
// 无错误
this.showError = false
// 将接收到的数据存储在 [métier] 层中
this.$métier.setTaxAdminData(response2.réponse)
// 可以进行税款计算
this.$router.push({ name: 'calculImpot' })
} catch (error) {
// 将错误上报至主组件
this.$emit('error', error)
} finally {
// 更新存储
this.$store.commit('replace', { métier: this.$métier })
// 保存会话
this.$session().save()
}
}
}
}
</script>
请注意以下几点:
- 第 69 行:函数 [created2] 已重命名为 [mounted], 此举是为了避免 [nuxt] 服务器执行该函数(该服务器既不执行 [beforeMount],也不执行 [mounted])。 只有客户端 [nuxt] 会执行它,这与示例 [vuejs-22] 的情况相同;
- 第 73 行:引用了 [this.$métier],但该客户端目前尚不存在;
- 第75行:我们在[nuxt]应用程序中从未使用过此方法。需要验证它在[nuxt]环境中是否有效;
- 第112行、第172行:在[vuejs-22]中,项目会话是按此方式保存的。对于[nuxt-20]项目,[save]方法必须接收当前上下文。 已知在页面 [nuxt] 中,对象 [context] 在 [this.$nuxt.context] 中可用;
因此,第 112 行和第 172 行重写如下:
this.$session().save(this.$nuxt.context)
需要注意的是,此代码尚未经过优化。与其多次调用函数 [this.$session()],不如改写为:
const session=this.$session()
随后使用变量 [session]。对于函数 [this.$dao()],也可采用同样的思路。
完成这些修正后,我们可以使用浏览器重新加载 URL 和 [http://localhost:81/nuxt-20/]。我们得到的页面与之前完全相同:

让我们查看浏览器的日志:

日志 [1] 是客户端生成的最后一条日志,其前一条为 [nuxt]。在 [2] 中, 可以看到属性 [started] 指向 [vrai],这意味着函数 [mounted] 已成功与税费计算服务器建立会话 jSON。 我们还看到该存储具有一些属性,这些属性需要被放弃或重命名。请注意,我们使用的是示例中的存储 [nuxt-12]。
现在,在税费计算服务器未启动的情况下,重新请求 URL 和 [http://localhost:81/nuxt-20/]。首先,请务必删除 [nuxt] 会话的 Cookie:

上图是 Chrome 的屏幕截图。完成上述操作后,URL [http://localhost:81/nuxt-20/] 显示的结果如下:

该错误在 [vuejs-22] 项目中已得到正确处理。在 [nuxt-20] 项目中,该错误仍得到正确处理。
17.4. 步骤 3
现在我们已经有了身份验证页面,接下来需要查看用户点击 [Valider] 按钮时执行的代码:
// 事件处理程序
methods: {
// ----------- 身份验证
async login() {
try {
// 开始等待
this.$emit('loading', true)
// 尚未通过身份验证
this.$store.commit('replace', { authenticated: false })
// 正在向服务器进行阻塞式身份验证
const response = await this.$dao().authentifierUtilisateur(this.user, this.password)
// 加载完成
this.$emit('loading', false)
// 分析服务器响应
if (response.état !== 200) {
// 显示错误
this.message = response.réponse
this.showError = true
// 返回事件循环
return
}
// 无错误
this.showError = false
// 身份验证通过
this.$store.commit('replace', { authenticated: true })
// --------- 现在请求税务管理数据
// 初始时无数据
this.$métier.setTaxAdminData(null)
// 开始等待
this.$emit('loading', true)
// 向服务器发送阻塞请求
const response2 = await this.$dao().getAdminData()
// 加载完成
this.$emit('loading', false)
// 响应分析
if (response2.état !== 1000) {
// 显示错误
this.message = response2.réponse
this.showError = true
// 返回事件循环
return
}
// 无错误
this.showError = false
// 将接收到的数据存储在 [métier] 层中
this.$métier.setTaxAdminData(response2.réponse)
// 可以进行税额计算
this.$router.push({ name: 'calculImpot' })
} catch (error) {
// 将错误上报至主组件
this.$emit('error', error)
} finally {
// 更新存储
this.$store.commit('replace', { métier: this.$métier })
// 保存会话
this.$session().save(this.$nuxt.context)
}
}
}
这里的主要问题似乎是缺少 [this.$métier] 数据。为了解决这个问题,我们将:
- 将示例 [vuejs-22] 中的类 [Métier] 引入;我们将该类放置在 [api] 文件夹中;
- 在客户端 [nuxt] 的上下文中注入函数 [$métier],以便访问该类;
首先将类 [Métier] 复制到文件夹 [api] 中:

当类 [Métier] 已存在于项目中后,为客户端 [nuxt] 创建一个新插件。 该插件名为 [pluginMétier],将注入一个名为 [$métier] 的函数,该函数将提供对类 [Métier] 的访问:
/* eslint-disable no-console */
// 创建通往 [métier] 层的入口
import Métier from '@/api/client/Métier'
export default (context, inject) => {
// 实例化 [métier] 层
const métier = new Métier()
// 将函数 [$métier] 注入上下文
inject('métier', () => métier)
// 日志
console.log('[fonction client $métier créée]')
}
完成上述操作后,我们可以修改页面 [index]:
// 生命周期:组件刚刚创建
mounted() {
// eslint-disable-next-line
console.log("Authentification mounted");
// 用户能否进行模拟?
if (this.$store.state.started && this.$store.state.authenticated && this.$métier().taxAdminData) {
// 那么用户可以进行模拟
this.$router.push({ name: 'calcul-impot' })
// 返回事件循环
return
}
// 如果会话 jSON 已启动,则不再重新启动
...
},
// 事件处理程序
methods: {
// ----------- 身份验证
async login() {
try {
// 开始等待
this.$emit('loading', true)
// 尚未通过身份验证
this.$store.commit('replace', { authenticated: false })
// 服务器端阻塞式身份验证
const response = await this.$dao().authentifierUtilisateur(this.user, this.password)
// 加载完成
this.$emit('loading', false)
// 分析服务器响应
if (response.état !== 200) {
// 显示错误
this.message = response.réponse
this.showError = true
// 返回事件循环
return
}
// 无错误
this.showError = false
// 身份验证通过
this.$store.commit('replace', { authenticated: true })
// --------- 现在请求税务管理数据
// 初始时无数据
this.$métier().setTaxAdminData(null)
// 开始等待
this.$emit('loading', true)
// 向服务器发送阻塞请求
const response2 = await this.$dao().getAdminData()
// 加载完成
this.$emit('loading', false)
// 响应分析
if (response2.état !== 1000) {
// 显示错误
this.message = response2.réponse
this.showError = true
// 返回事件循环
return
}
// 无错误
this.showError = false
// 将接收到的数据存储在 [métier] 层中
this.$métier().setTaxAdminData(response2.réponse)
// 可以进行税款计算
this.$router.push({ name: 'calcul-impot' })
} catch (error) {
// 将错误上报至主组件
this.$emit('error', error)
} finally {
// 更新存储
this.$store.commit('replace', { métier: this.$métier() })
// 保存会话
this.$session().save(this.$nuxt.context)
}
}
}
- 第 43、61、69 行,[this.$métier] 已被替换为 [this.$métier()];
- 第8、63行:项目[vuejs-22]中的页面[CalculImpot]已更名为项目[nuxt-20]中的页面[calcul-impot];
完成这些修正后,可以尝试验证身份验证页面:

生成的页面如下:

我们确实成功获取了税款计算页面。现在来看一下日志:

在 [2] 中,可以看到身份验证状态已被正确记录。 在 [3-4] 中,可以看到已获取 [taxAdminData] 数据,该数据通过类 [Métier] 实现了税款计算。
17.5. 步骤 4
让我们查看生成的页面 [calcul-impot]:
<!-- 视图定义 HTML -->
<template>
<div>
<Layout :left="true" :right="true">
<!-- 右侧税款计算表单 -->
<FormCalculImpot slot="right" @resultatObtenu="handleResultatObtenu" />
<!-- 左侧导航菜单 -->
<Menu slot="left" :options="options" />
</Layout>
<!-- 表单下方的税款计算结果显示区 -->
<b-row v-if="résultatObtenu" class="mt-3">
<!-- 三列空白区域 -->
<b-col sm="3" />
<!-- 九列区域 -->
<b-col sm="9">
<b-alert show variant="success">
<span v-html="résultat"></span>
</b-alert>
</b-col>
</b-row>
</div>
</template>
<script>
// 导入
import FormCalculImpot from '@/components/form-calcul-impot'
import Menu from '@/components/menu'
import Layout from '@/components/layout'
export default {
// 使用的组件
components: {
Layout,
FormCalculImpot,
Menu
},
// 内部报表
data() {
return {
// 菜单选项
options: [
{
text: 'Liste des simulations',
path: '/liste-des-simulations'
},
{
text: 'Fin de session',
path: '/fin-session'
}
],
// 税款计算结果
résultat: '',
résultatObtenu: false
}
},
// 生命周期
created() {
// eslint-disable-next-line
console.log("CalculImpot created");
},
// 事件管理方法
methods: {
// 税款计算结果
handleResultatObtenu(résultat) {
// 将结果构建为字符串HTML
const impôt = "Montant de l'impôt : " + résultat.impôt + ' euro(s)'
const décôte = 'Décôte : ' + résultat.décôte + ' euro(s)'
const réduction = 'Réduction : ' + résultat.réduction + ' euro(s)'
const surcôte = 'Surcôte : ' + résultat.surcôte + ' euro(s)'
const taux = "Taux d'imposition : " + résultat.taux
this.résultat = impôt + '<br/>' + décôte + '<br/>' + réduction + '<br/>' + surcôte + '<br/>' + taux
// 显示结果
this.résultatObtenu = true
// ---- 更新存储 [Vuex]
// 进行一次加法模拟
this.$store.commit('addSimulation', résultat)
// 保存会话
this.$session.save()
}
}
}
</script>
- 第44行和第48行:导航菜单的链接正确。页面[/fin-session]不存在。项目[vuejs-22]通过路由设置解决了此问题。我们将对项目[nuxt-20]采取同样的处理方式;
- 第76行:引用了目前尚不存在的变更[addSimulation]。我们将创建该变更;
- 第78行:与[index]页面一样,应写为[this.$session().save(this.$nuxt.context)];
修改存储 [store/index]。该存储继承自项目 [nuxt-12],目前内容如下:
/* eslint-disable no-console */
// 存储状态
export const state = () => ({
// jSON 会话已启动
jsonSessionStarted: false,
// 用户已认证
userAuthenticated: false,
// PHP 会话 Cookie
phpSessionCookie: '',
// adminData
adminData: ''
})
// 存储变更
export const mutations = {
// 状态更新
replace(state, newState) {
for (const attr in newState) {
state[attr] = newState[attr]
}
},
// 存储器重置
reset() {
this.commit('replace', { jsonSessionStarted: false, userAuthenticated: false, phpSessionCookie: '', adminData: '' })
}
}
// 存储器操作
export const actions = {
nuxtServerInit(store, context) {
// 谁在执行这段代码?
console.log('nuxtServerInit, client=', process.client, 'serveur=', process.server, 'env=', context.env)
// 初始化会话
initStore(store, context)
}
}
function initStore(store, context) {
// store 是待初始化的存储
// 获取会话
const session = context.app.$session()
// 会话是否已初始化?
if (!session.value.initStoreDone) {
// 启动一个新的存储器
console.log("nuxtServerInit, initialisation d'un nouveau store")
// 将存储器放入会话中
session.value.store = store.state
// 存储区现已初始化
session.value.initStoreDone = true
} else {
console.log("nuxtServerInit, reprise d'un store existant")
// 使用会话中的存储器更新当前存储器
store.commit('replace', session.value.store)
}
// 正在保存会话
session.save(context)
// 日志
console.log('initStore terminé, store=', store.state)
}
- 第3-27行:我们将沿用应用程序[vuejs-22]的状态及其变更(参见文档[3]):
// 卷帘状态
export const state = () => ({
// jSON 会话已启动
started: false,
// 用户已通过身份验证
authenticated: false,
// 会话cookie PHP
phpSessionCookie: '',
// 模拟列表
simulations: [],
// 最后一次模拟的编号
idSimulation: 0,
// 图层 [métier]
métier: null
})
// 百叶窗状态变化
export const mutations = {
// 状态替换
replace(state, newState) {
for (const attr in newState) {
state[attr] = newState[attr]
}
},
// 存储器重置
reset() {
this.commit('replace', { started: false, authenticated: false, phpSessionCookie: '', idSimulation: 0, simulations: [], métier: null })
},
// 删除索引号为 n 的行
deleteSimulation(state, index) {
// eslint-disable-next-line no-console
console.log('mutation deleteSimulation')
// 删除第 [index] 行
state.simulations.splice(index, 1)
console.log('store simulations', state.simulations)
},
// 添加模拟
addSimulation(state, simulation) {
// eslint-disable-next-line no-console
console.log('mutation addSimulation')
// 模拟编号
state.idSimulation++
simulation.id = state.idSimulation
// 将模拟添加到模拟表中
state.simulations.push(simulation)
}
}
- 第 4 行和第 6 行:引入已使用的属性;
- 第 8 行:我们保留会话 Cookie PHP。这对于确保客户端和服务器 [nuxt] 与税款计算服务器拥有相同的会话 PHP 至关重要;
- 第 10 行:用户已进行的模拟列表;
- 第12行:用户最近一次模拟的编号;
- 第14行:[métier]层;
- 第30-47行:项目[vuejs-22]存储中存在的变更,且被应用程序页面引用。项目[vuejs-22]曾有一个名为[clear]的变更,该变更会清空模拟列表。 我们不将其纳入,因为已存在的变体 [reset] 应该能满足需求;
- 第26-28行:修改了变体[reset],以适应状态的新内容;
页面 [calcul-impot] 使用以下组件 [form-calcul-impot]:
<!-- 视图定义 HTML -->
<template>
<!-- 表单 HTML -->
<b-form @submit.prevent="calculerImpot" class="mb-3">
<!-- 12列蓝色背景消息 -->
<b-row>
<b-col sm="12">
<b-alert show variant="primary">
<h4>Remplissez le formulaire ci-dessous puis validez-le</h4>
</b-alert>
</b-col>
</b-row>
<!-- 表单元素 -->
<!-- 第一行 -->
<b-form-group label="Etes-vous marié(e) ou pacsé(e) ?">
<!-- 5列单选按钮-->
<b-col sm="5">
<b-form-radio v-model="marié" value="oui">Oui</b-form-radio>
<b-form-radio v-model="marié" value="non">Non</b-form-radio>
</b-col>
</b-form-group>
<!-- 第二行 -->
<b-form-group label="Nombre d'enfants à charge" label-for="enfants">
<b-form-input id="enfants" v-model="enfants" :state="enfantsValide" type="text" placeholder="Indiquez votre nombre d'enfants"></b-form-input>
<!-- 可能出现的错误信息 -->
<b-form-invalid-feedback :state="enfantsValide">Vous devez saisir un nombre positif ou nul</b-form-invalid-feedback>
</b-form-group>
<!-- 第三行 -->
<b-form-group label="Salaire annuel net imposable" label-for="salaire" description="Arrondissez à l'euro inférieur">
<b-form-input id="salaire" v-model="salaire" :state="salaireValide" type="text" placeholder="Salaire annuel"></b-form-input>
<!-- 可能出现的错误信息 -->
<b-form-invalid-feedback :state="salaireValide">Vous devez saisir un nombre positif ou nul</b-form-invalid-feedback>
</b-form-group>
<!-- 第四行,按钮 [submit] -->
<b-col sm="3">
<b-button :disabled="formInvalide" type="submit" variant="primary">Valider</b-button>
</b-col>
</b-form>
</template>
<!-- 脚本 -->
<script>
export default {
// 内部状态
data() {
return {
// 已婚或未婚
marié: 'non',
// 子女数量
enfants: '',
// 年薪
salaire: ''
}
},
// 计算出的内部状态
computed: {
// 表单验证
formInvalide() {
return (
// 工资无效
!this.salaire.match(/^\s*\d+\s*$/) ||
// 或子女信息无效
!this.enfants.match(/^\s*\d+\s*$/) ||
// 或未获取税务数据
!this.$métier.taxAdminData
)
},
// 工资验证
salaireValide() {
// 必须为数字且大于等于0
return Boolean(this.salaire.match(/^\s*\d+\s*$/) || this.salaire.match(/^\s*$/))
},
// 子女信息验证
enfantsValide() {
// 必须为数字且大于等于0
return Boolean(this.enfants.match(/^\s*\d+\s*$/) || this.enfants.match(/^\s*$/))
}
},
// 生命周期
created() {
// 日志
// eslint-disable-next-line
console.log("FormCalculImpot created");
},
// 事件管理器
methods: {
calculerImpot() {
// 使用 [métier] 层计算税款
const résultat = this.$métier.calculerImpot(this.marié, Number(this.enfants), Number(this.salaire))
// eslint-disable-next-line
console.log("résultat=", résultat);
// 补充结果
résultat.marié = this.marié
résultat.enfants = this.enfants
résultat.salaire = this.salaire
// 触发事件 [resultatObtenu]
this.$emit('resultatObtenu', résultat)
}
}
}
</script>
- 第 65、89 行:[this.$métier] 的引用应更改为 [this.$métier()];
完成这些修正后,可以尝试进行模拟:

得到以下响应:

查看日志:

- 在 [9-10] 中,可以看到第一次模拟确实位于 [store] 中;
- 在[5]中,上次模拟的编号确实已递增;
17.6. 步骤 5
现在我们已经完成了一次模拟,点击链接 [Liste des simulations]。我们将看到以下页面:

客户端 [nuxt] 的路由已正确完成。让我们查看页面 [liste-des-simulations] 的代码:
<!-- 视图定义 HTML -->
<template>
<div>
<!-- 版面设计 -->
<Layout :left="true" :right="true">
<!-- 右侧栏中的模拟 -->
<template slot="right">
<template v-if="simulations.length == 0">
<!-- 无模拟 -->
<b-alert show variant="primary">
<h4>Votre liste de simulations est vide</h4>
</b-alert>
</template>
<template v-if="simulations.length != 0">
<!-- 有模拟 -->
<b-alert show variant="primary">
<h4>Liste de vos simulations</h4>
</b-alert>
<!-- 模拟图表 -->
<b-table :items="simulations" :fields="fields" striped hover responsive>
<template v-slot:cell(action)="data">
<b-button @click="supprimerSimulation(data.index)" variant="link">Supprimer</b-button>
</template>
</b-table>
</template>
</template>
<!-- 左侧栏导航菜单 -->
<Menu slot="left" :options="options" />
</Layout>
</div>
</template>
<script>
// 导入
import Layout from '@/components/layout'
import Menu from '@/components/menu'
export default {
// 组件
components: {
Layout,
Menu
},
// 内部状态
data() {
return {
// 导航菜单选项
options: [
{
text: "Calcul de l'impôt",
path: '/calcul-impot'
},
{
text: 'Fin de session',
path: '/fin-session'
}
],
// 表参数 HTML
fields: [
{ label: '#', key: 'id' },
{ label: 'Marié', key: 'marié' },
{ label: "Nombre d'enfants", key: 'enfants' },
{ label: 'Salaire', key: 'salaire' },
{ label: 'Impôt', key: 'impôt' },
{ label: 'Décôte', key: 'décôte' },
{ label: 'Réduction', key: 'réduction' },
{ label: 'Surcôte', key: 'surcôte' },
{ label: '', key: 'action' }
]
}
},
// 计算出的内部状态
computed: {
// 从 Vuex 存储中获取的模拟列表
simulations() {
return this.$store.state.simulations
}
},
// 生命周期
created() {
// eslint-disable-next-line
console.log("ListeSimulations created");
},
// 方法
methods: {
supprimerSimulation(index) {
// eslint-disable-next-line
console.log("supprimerSimulation", index);
// 删除模拟编号 [index]
this.$store.commit('deleteSimulation', index)
// 保存会话
this.$session.save()
}
}
}
</script>
- 第 47-56 行:导航菜单的目标链接正确;
- 第 75 行:卷帘窗的引用正确;
- 第 89 行:使用了我们在上一步骤中集成的 [deleteSimulation] 变体;
- 第 91 行:该行应重写为 [this.$session().save(this.$nuxt.context)];
我们进行必要的修改,然后尝试删除当前显示的模拟:

随后将显示以下页面:

查看日志:

- 在 [6] 中,可以看到模拟表格为空;
现在回到税款计算表单:

我们得到以下页面:

因此路由功能已正常运行。
17.7. 步骤 6
接下来我们需要处理导航菜单中的导航选项 [Fin de session]:
// 菜单选项
options: [
{
text: 'Liste des simulations',
path: '/liste-des-simulations'
},
{
text: 'Fin de session',
path: '/fin-session'
}
]
- 第 9 行,页面 [/fin-session] 不存在。项目 [vuejs-22] 通过 [router.js] 文件中的路由规则处理了这种情况:
// 导入
import Vue from 'vue'
import VueRouter from 'vue-router'
// 视图
import Authentification from './views/Authentification'
import CalculImpot from './views/CalculImpot'
import ListeSimulations from './views/ListeSimulations'
import NotFound from './views/NotFound'
// 会话
import session from './session'
// 路由插件
Vue.use(VueRouter)
// 应用程序路由
const routes = [
// 身份验证
{ path: '/', name: 'authentification', component: Authentification },
{ path: '/authentification', name: 'authentification2', component: Authentification },
// 税款计算
{
path: '/calcul-impot', name: 'calculImpot', component: CalculImpot,
meta: { authenticated: true }
},
// 模拟列表
{
path: '/liste-des-simulations', name: 'listeSimulations', component: ListeSimulations,
meta: { authenticated: true }
},
// 结束会话
{
path: '/fin-session', name: 'finSession'
},
// 未知页面
{
path: '*', name: 'notFound', component: NotFound,
},
]
// 路由器
const router = new VueRouter({
// 路由
routes,
// URL的显示模式
mode: 'history',
// 应用程序的基础URL
base: '/client-vuejs-impot/'
})
// 路由验证
router.beforeEach((to, from, next) => {
// eslint-disable-next-line no-console
console.log("router to=", to, "from=", from);
// 该路由仅限经过身份验证的用户访问?
if (to.meta.authenticated && !session.authenticated) {
next({
// 进入身份验证
name: 'authentification',
})
// 返回事件循环
return;
}
// 会话结束的特殊情况
if (to.name === "finSession") {
// 清理会话
session.clear();
// 跳转至视图 [authentification]
next({
name: 'authentification',
})
// 返回事件循环
return;
}
// 其他情况 - 路由的下一个常规视图
next();
})
// 路由器导出
export default router
- 第 64-76 行处理了路由到路径 [/fin-session] 的特殊情况;
- 第 66 行:清空当前会话;
- 第 68-70 行:显示视图 [authentification];
我们将尝试在客户端路由文件 [nuxt] 中实现类似的功能:

脚本 [client/routing.js] 变为如下:
/* eslint-disable no-console */
export default function(context) {
// 谁在执行这段代码?
console.log('[middleware client], process.server', process.server, ', process.client=', process.client)
// 浏览器中 PHP 会话 Cookie 的管理
// 浏览器中的会话 Cookie PHP 必须与 Nuxt 会话中找到的 Cookie 完全一致
// 操作 [fin-session] 接收一个新的 Cookie PHP(服务器作为 Nuxt 客户端)
// 如果是由服务器接收该cookie,客户端必须将其转发给浏览器
// 以便其自身与服务器 PHP 进行通信
// 此处属于客户端路由
// 获取会话cookie PHP
const phpSessionCookie = context.store.state.phpSessionCookie
if (phpSessionCookie) {
// 如果存在,则将会话cookie PHP 分配给浏览器
document.cookie = phpSessionCookie
}
// 接下来该去哪里?
const to = context.route.path
if (to === '/fin-session') {
// 清理会话
const session = context.app.$session()
session.reset(context)
// 重定向至首页
context.redirect({ name: 'index' })
}
}
- 我们在现有代码中添加了 [19-27] 相关行;
- 第 20 行:从当前路由的目标中获取 [path];
- 第 21 行:检查是否为 [/fin-session]。如果是:
- 第23-24行:重置会话;
- 第 26 行:将客户端 [nuxt] 重定向至首页;
该会话的 [session.reset(context)] 方法(第 24 行)如下:
// 重置会话
reset(context) {
console.log('nuxt-session reset')
// 重置存储
context.store.commit('reset')
// 将新存储区保存到会话中并保存会话
this.save(context)
}
[context.store.commit('reset')]方法(第5行)如下:
// 重置存储
reset() {
this.commit('replace', { started: false, authenticated: false, phpSessionCookie: '', idSimulation: 0, simulations: [], métier: null })
}
现在使用链接 [Fin de session] 时,将显示主页并生成以下日志:

- 在 [3] 中,可以看到已不再处于认证状态;
- 在 [4] 中,可见 jSON 会话已启动;
- 在 [6] 中,[métier] 层已不在存储中(但在包含 [this.$métier()] 的页面中仍存在);
- 在 [5, 7] 中,已无模拟;
必须清楚理解会话结束时发生的情况:
- [nuxt]会话被重置:存储库的[started]属性变为[false];
- 系统将重定向至页面 [index];
- 执行页面 [index] 上的方法 [mounted]。该方法会与税费计算服务器建立新的会话 jSON。 如果操作成功,存储的 [started] 属性将更改为 [true];
17.8. 步骤 7
至此,[nuxt-20] 应用程序已具备 [vuejs-22] 应用程序的所有功能。移植工作似乎已完成。
我们将以 [nuxt] 的思路进一步深入。[index] 页面中的 [mounted] 方法存在问题。它会启动一项异步操作,而搜索引擎不会等待该操作结束。 我们知道,在这种情况下,必须将异步操作封装在函数 [asyncData] 中,因为这样,执行该操作的服务器 [nuxt] 才会等待其完成,然后才将页面交付给搜索引擎。
这里我们借助在应用程序 [nuxt-12] 中为页面 [index] 编写的函数 [asyncData]:
export default {
name: 'InitSession',
// 使用的组件
components: {
Layout,
Navigation
},
// 异步数据
async asyncData(context) {
// 日志
console.log('[index asyncData started]')
// 如果页面已被请求过,则不再重复处理
if (process.server && context.store.state.jsonSessionStarted) {
console.log('[index asyncData canceled]')
return { result: '[succès]' }
}
try {
// 启动会话jSON
const dao = context.app.$dao()
const response = await dao.initSession()
// 日志
console.log('[index asyncData response=]', response)
// 获取会话cookie PHP 以供后续请求使用
const phpSessionCookie = dao.getPhpSessionCookie()
// 将会话cookie PHP 存储在会话 [nuxt] 中
context.store.commit('replace', { phpSessionCookie })
// 是否发生错误?
if (response.état !== 700) {
// 错误位于 response.réponse
throw new Error(response.réponse)
}
// 请注意会话 jSON 已启动
context.store.commit('replace', { jsonSessionStarted: true })
// 输出结果
return { result: '[succès]' }
} catch (e) {
// 日志
console.log('[index asyncData error=]', e)
// 记录 jSON 会话未启动
context.store.commit('replace', { jsonSessionStarted: false })
// 报告错误
return { result: '[échec]', showErrorLoading: true, errorLoadingMessage: e.message }
} finally {
// 保存存储区
const session = context.app.$session()
session.save(context)
// 日志
console.log('[index asyncData finished]')
}
},
// 生命周期
beforeCreate() {
console.log('[index beforeCreate]')
},
created() {
console.log('[index created]')
},
beforeMount() {
console.log('[index beforeMount]')
},
mounted() {
console.log('[index mounted]')
// 仅限客户端
if (this.showErrorLoading) {
console.log('[index mounted, showErrorLoading=true]')
this.$eventBus().$emit('errorLoading', true, this.errorLoadingMessage)
}
}
- 第 13、33、40 行:需将属性 [jsonSessionStarted] 更改为 [started];
- 第 13 行:在应用程序 [nuxt-12] 中,只有服务器 [nuxt] 执行页面 [index] 及其函数 [asyncData]。 客户端 [nuxt] 仅在从服务器 [nuxt] 接收页面 [index] 后才执行该页面,因此未执行函数 [asyncData]。 而在 [nuxt-20] 中情况不同:链接 [Fin de session] 将在客户端 [nuxt] 的环境中显示页面 [index]。 此时将执行函数 [asyncData]。然而,当通过这种方式到达页面 [index] 时, [nuxt] 会话在此期间已被重置,且存储中的 [started] 属性值变为 [false],因此第 13 行的条件必然为假。 因此可以保留 [process.server],这样客户端 [nuxt] 就不会进行此检测;
- 第15、35、42行:将属性[result]放入页面[index]的属性[data]中。 在 [nuxt-20] 中,该属性将不会被使用,因此我们将从函数返回的结果中移除它;
- 第 61-67 行:必须保留此 [mounted] 方法,因为正是它使客户端 [nuxt] 能够显示错误消息。不过,错误处理方式将进行修改;
在当前的 [index] 页面中,我们将上述的 [asyncData] 函数替换掉旧的 [mounted] 函数,并新增一个 [mounted] 函数。 示例 [nuxt-20] 中页面 [index] 的代码将变为如下所示:
...
<!-- 视图动态 -->
<script>
/* eslint-disable no-console */
import Layout from '@/components/layout'
export default {
// 使用的组件
components: {
Layout
},
// 组件状态
data() {
return {
// 用户
user: '',
// 其密码
password: '',
// 错误显示
showError: false
}
},
// 计算属性
computed: {
// 有效输入
valid() {
return this.user && this.password && this.$store.state.started
}
},
// 异步数据
async asyncData(context) {
// 日志
console.log('[index asyncData started]')
// 如果页面已被请求,则不重复处理
if (process.server && context.store.state.started) {
console.log('[index asyncData canceled]')
return
}
try {
// 开始一个会话jSON
const dao = context.app.$dao()
const response = await dao.initSession()
// 日志
console.log('[index asyncData response=]', response)
// 获取会话cookie PHP 以供后续请求使用
const phpSessionCookie = dao.getPhpSessionCookie()
// 将会话cookie PHP 存储在会话 [nuxt] 中
context.store.commit('replace', { phpSessionCookie })
// 是否发生错误?
if (response.état !== 700) {
// 错误位于 response.réponse
throw new Error(response.réponse)
}
// 请注意,会话 jSON 已启动
context.store.commit('replace', { started: true })
// 无结果
return
} catch (e) {
// 日志
console.log('[index asyncData error=]', e.message)
// 记录:会话 jSON 未启动
context.store.commit('replace', { started: false })
// 报告错误
return { showErrorLoading: true, errorLoadingMessage: e.message }
} finally {
// 正在保存存储区
const session = context.app.$session()
session.save(context)
// 日志
console.log('[index asyncData finished]')
}
},
// 生命周期
beforeCreate() {
console.log('[index beforeCreate]')
},
created() {
console.log('[index created]')
},
beforeMount() {
// 仅限客户端
console.log('[index beforeMount]')
// 潜在错误处理
if (this.showErrorLoading) {
// 日志
console.log('[index beforeMount, showErrorLoading=true]')
// 将错误上报给主组件 [default]
this.$emit('error', new Error(this.errorLoadingMessage))
}
},
mounted() {
console.log('[index mounted]')
},
// 事件处理程序
methods: {
// ----------- 身份验证
async login() {
...
}
</script>
- 第 58、65 行:函数 [asyncData] 不再在此处使属性 [result] 闲置;
- 第81行:客户端[nuxt]的[beforeMount]方法。该方法被选中以替代[mounted]方法,用于处理[asyncData]可能出现的错误;
- 第85行:检测属性[errorLoading]是否已被设置。该属性只能由函数[asyncData]进行设置;
- 第 85-90 行:如果函数 [asyncData] 报告了错误,则通过事件 [error] 将其传递给页面 [default]。 这就是我们刚刚替换的旧函数 [created] 处理潜在错误的方式;
让我们进行一些测试。
首先,如果存在,我们将删除会话 Cookie [nuxt] 和 PHP。 随后,在税费计算服务器未启动的情况下请求页面 [http://localhost:81/nuxt-20/]。我们得到以下页面:

在启动税费计算服务器后,我们重新加载同一页面:

查看日志:

- 在 [2-3] 中,可以看到会话 jSON 已被启动;
- 在 [4] 中,可以看到会话 Cookie PHP,该 Cookie 是服务器 [nuxt] 在与税费计算服务器通信时获取的。 客户端 [nuxt] 接下来将使用该 Cookie;
现在我们进行身份验证:

我们得到以下页面:

在 [1] 中,我们收到了一条错误信息。这意味着浏览器未发送正确的会话 Cookie,该会话由服务器 [nuxt] 在上一步中启动,会话 ID 为 PHP。 在 [nuxt-12] 中, PHP会话cookie从服务器[nuxt]传递给客户端[nuxt]的过程,是在脚本[middleware/client/routing]的客户端[nuxt]路由中完成的 :
/* eslint-disable no-console */
export default function(context) { // 谁在执行这段代码?
console.log('[middleware client], process.server', process.server, ', process.client=', process.client)
// 浏览器中 PHP 会话 Cookie 的管理
// 浏览器中的会话 Cookie PHP 必须与 Nuxt 会话中找到的完全一致
// 操作 [fin-session] 接收一个新的 PHP Cookie(服务器作为 Nuxt 客户端)
// 如果是由服务器接收该cookie,客户端必须将其转发给浏览器
// 以便其自身与服务器 PHP 进行通信
// 此处属于客户端路由
// 获取会话cookie PHP
const phpSessionCookie = context.store.state.phpSessionCookie
if (phpSessionCookie) {
// 如果存在,则将会话cookie PHP 分配给浏览器
document.cookie = phpSessionCookie
}
// 接下来该去哪里?
const to = context.route.path
if (to === '/fin-session') {
// 清理会话
const session = context.app.$session()
session.reset(context)
// 重定向至首页
context.redirect({ name: 'index' })
}
}
正是第13至17行代码,使得客户端[nuxt]能够从服务器[nuxt]获取会话PHP的cookie。
这里的问题在于,当点击按钮 [Valider] 时,客户端 [nuxt] 没有进行路由。因此,其路由函数未被调用。 解决方法是将第 12-17 行复制到页面 [index] 的身份验证方法开头:
// 事件处理程序
methods: {
// ----------- 身份验证
async login() {
// 从存储中检索会话cookie PHP
const phpSessionCookie = this.$store.state.phpSessionCookie
if (phpSessionCookie) {
// 如果存在,则将会话cookie PHP 分配给浏览器
document.cookie = phpSessionCookie
}
try {
// 开始等待
this.$emit('loading', true)
// 尚未通过身份验证
第5-10行,从存储中获取由服务器[nuxt]发起的会话PHP的Cookie。完成此修改后,成功获取了税款计算页面,这表明身份验证已成功。
17.9. 步骤 8
我们现在拥有了一个功能完备且符合[nuxt]设计理念的应用程序。与处理[nuxt-13]应用程序时一样,我们将重点关注[nuxt]服务器的导航机制。 如前所述,用户无需手动输入应用程序的 URL 代码。 用户应使用系统提供的链接,这些链接由运行在SPA模式下的[nuxt]客户端执行。尽管如此,我们将确保[nuxt]服务器的导航始终能使应用程序保持在稳定状态。
根据针对 [nuxt-13] 进行的研究(参见链接段落),我们知道需要:
- 修改脚本 [midleware/routing];
- 添加脚本 [middleware/server/routing];

脚本 [middleware/routing] 修改如下:
/* eslint-disable no-console */
// 导入服务器和客户端的中间件
import serverRouting from './server/routing'
import clientRouting from './client/routing'
export default function(context) {
// 谁在执行这段代码?
console.log('[middleware], process.server', process.server, ', process.client=', process.client)
if (process.server) {
// 服务器路由
serverRouting(context)
} else {
// 客户端路由
clientRouting(context)
}
}
- 第 4 行:导入服务器 [nuxt] 的路由脚本;
- 第10-12行:如果由服务器[nuxt]执行代码,则使用其路由功能;
脚本 [middleware/server/routing] 如下:
/* eslint-disable no-console */
export default function(context) {
// 谁在执行这段代码?
console.log('[middleware server], process.server', process.server, ', process.client=', process.client)
// 我们从这里那里收集了一些信息
const store = context.store
// 我们来自哪里?
const from = store.state.from || 'nowhere'
// 我们要去哪里?
let to = context.route.name
// /fin-session 没有属性的特例 [name]
if (context.route.path === '/fin-session') {
to = 'fin-session'
}
// 可能的重定向
let redirection = ''
// 路由管理完成
let done = false
// 是否已处于服务器重定向中 [nuxt]?
if (store.state.serverRedirection) {
// 无需操作
done = true
}
// 是否为页面刷新?
if (!done && from === to) {
// 无需操作
done = true
}
// 服务器导航控制 [nuxt]
// 参照客户端的导航菜单
// 首先处理会话结束的情况
if (!done && store.state.started && store.state.authenticated && to === 'fin-session') {
// 清理会话
const session = context.app.$session()
session.reset(context)
// 重定向至首页
redirection = 'index'
// 任务完成
done = true
}
// PHP 会话未启动的情况
if (!done && !store.state.started && to !== 'index') {
// 重定向至 [index]
redirection = 'index'
// 任务已完成
done = true
}
// 用户未通过身份验证的情况
if (!done && store.state.started && !store.state.authenticated && to !== 'index') {
redirection = 'index'
// 任务已完成
done = true
}
// 未获取 [adminData] 的情况
if (!done && store.state.started && store.state.authenticated && !store.state.métier.taxAdminData && to !== 'index') {
// 重定向至 [index]
redirection = 'index'
// 任务已完成
done = true
}
// 已获取 [adminData] 的情况
if (
!done &&
store.state.started &&
store.state.authenticated &&
store.state.métier.taxAdminData &&
to !== 'calcul-impot' &&
to !== 'liste-des-simulations'
) {
// 保持在同一页面
redirection = from
// 工作完成
done = true
}
// 通常已完成所有检查 ---------------------
// 重定向?
if (redirection) {
// 在商店中记录重定向
store.commit('replace', { serverRedirection: true })
} else {
// 无重定向
store.commit('replace', { serverRedirection: false, from: to })
}
// 将存储数据保存至会话中[nuxt]
const session = context.app.$session()
session.value.store = store.state
session.save(context)
// 执行服务器的重定向(如有)[nuxt]
if (redirection) {
context.redirect({ name: redirection })
}
}
- 在此脚本中,我们沿用了应用程序 [nuxt-13] 中服务器 [nuxt] 路由中已开发并使用的思路;
- 我们在应用程序的存储中添加了两个属性:
- [from]:上次显示的页面名称。我们知道客户端 [nuxt] 拥有此信息,但服务器 [nuxt] 则没有。 我们将通过在每次 [nuxt] 服务器路由时,将即将显示的页面名称存储到存储中,来为其添加此信息。 在客户端 [nuxt] 每次路由时,也将采取同样的操作。因此,在服务器 [nuxt] 的下一次路由中,它将在存储中找到应用程序最后显示的页面名称;
- [serverRedirection]:当路由目标被服务器 [nuxt] 拒绝时,该服务器将执行重定向。随后,它会在存储中标记服务器 [nuxt] 的下一个目标为重定向页面。 此重定向将触发服务器 [nuxt] 路由器的重新执行。若该路由器发现当前目标源自重定向,则允许其继续;
- 第 6-11 行:获取路由所需的信息;
- 第 13-16 行:目标 [/fin-session] 未与名为 [fin-session] 的页面关联。因此它没有名称。为其分配一个名称;
- 第19行:潜在重定向的目标;
- 第21行:路由测试完成后,目标为[done=true];
- 第23-27行:如前所述,如果当前路由源于重定向,则无需采取任何操作。因为在前一次路由过程中,路由器已决定将客户端浏览器重定向。没有必要重新考虑这一决定;
- 第29-33行:如果是页面刷新,则放任其进行。这并非适用于所有[nuxt]应用程序的定理:必须针对每页单独评估刷新带来的影响。 在此情况下,[index, calcul-impot, liste-des-simulations]页面的刷新并未引发不良影响;
- 第35-85行:服务器[nuxt]的路由设置沿用了客户端[nuxt]的路由配置。 当处于某页面时,服务器 [nuxt] 的路由必须反映客户端 [nuxt] 在该页面上提供的导航菜单;
- 第 38-47 行:首先处理目标 [fin-session] 不对应现有页面的情况。如果满足条件(会话已启动、用户已认证),则清除会话并将用户重定向至页面 [index];
- 第49-55行:如果与税费计算服务器的会话jSON尚未启动,则唯一可能的跳转目标是页面[index];
- 第57-62行:如果jSON会话已启动,且用户未通过身份验证也未请求身份验证页面,则重定向至身份验证页面(即[index]);
- 第64-70行:如果用户已通过身份验证,但未获取数据[adminData],则重定向至身份验证页面。 身份验证包含两步:首先进行身份验证,若验证成功,则进一步要求提供数据 [adminData]。若未获取到该数据,则需重新进行身份验证;
- 第72-85行:若已获取[adminData]数据,则唯一可能的目标仅为[calcul-impot]和[liste-des-simulations]。否则,拒绝路由;
- 第88-95行:根据是否进行重定向来更新存储;
- 第 94 行:没有重定向。因此,当前的 [to] 将成为下一次路由的 [from];
- 第96-99行:将存储信息保存到会话cookie [nuxt] 中;
- 第100-103行:如果需要进行重定向,则执行重定向;
进行测试时,需确保从空白状态开始,通过税费计算服务器删除会话cookie [nuxt] 和 PHP:

为测试服务器 [nuxt] 的路由功能,请在每个页面上尝试所有可能的 URL 和 [/, /calcul-impot, /liste-des-simulations] 值。每次操作后,应用程序都应保持一致状态。
17.10. 步骤 9
步骤 9 是部署 [nuxt-20] 应用程序。这需要一个提供 [node.js] 环境的托管服务来运行 [nuxt] 服务器。 我没有这样的环境。读者可以按照“链接”段落中描述的步骤,在自己的开发机器上部署应用程序 [nuxt-20],并使用协议 HTTPS 对其进行安全加固。
17.11. Conclusion
[vuejs-22] 应用程序向 [nuxt-20] 应用程序的移植现已完成。让我们回顾一下此次移植的几个要点:
- [vuejs-22] 的页面已保留;
- [vuejs-22]页面中原有的异步操作已迁移至[asyncData]函数中;
- 在 [nuxt-20] 中需要管理两个实体:客户端 [nuxt] 和服务器 [nuxt]。后者在 [vuejs-22] 中并不存在。 为保持两个实体的一致性,我们需要一个 [nuxt] 会话;
- 我们还需管理服务器 [nuxt] 的路由;
实际上,直接基于 [nuxt] 架构开始开发,无疑比先构建 [vue.js] 架构,再将其移植到 [nuxt] 环境中更为可取。