十一、编写路由中间件和服务器中间件

还记得在使用膝关节炎的方法创建服务器上的中间件时,在第 8 章 AUTT1 中,ORT T2。中间件在您的膝关节炎应用中都是有用的和强大的,您可以在这里预测和控制整个应用的顺序。那 Nuxt 呢?嗯,我们应该在 Nuxt 中探索两种类型的中间件:路由中间件和服务器中间件。在本章中,您将学习如何区分它们,并创建一些基本的中间件,然后再继续下一章中关于身份验证的内容,其中非常需要中间件。我们还将在下一章之后的章节中使用中间件。因此,在本章中,与前面的许多章节一样,您将在 Vue 应用中创建一些带有导航卫士的中间件,以便在 Nuxt 应用中创建路由中间件和服务器中间件之前掌握 Vue/Nuxt 系统中的中间件机制。

在本章中,我们将介绍以下主题:

  • 用 Vue 路由编写中间件
  • 介绍 Vue-CLI
  • 在 Nuxt 中编写路由中间件
  • 写入 Nuxt 服务器中间件

用 Vue 路由编写中间件

在学习中间件在 Nuxt 应用中的工作原理之前,我们应该了解它在标准 Vue 应用中的工作原理。此外,在 Vue 应用中创建中间件之前,让我们先了解它们是什么。

什么是中间件?

简单地说,中间件是位于两个或多个软件之间的软件层。这是软件开发中的一个老概念。中间件是一个从 1968 年开始使用的术语。20 世纪 80 年代,它作为解决如何将较新的应用链接到较旧的遗留系统这一问题的解决方案而广受欢迎。对于它有很多定义,例如(从谷歌字典)“【中间件是一种】软件,它充当操作系统或数据库与应用之间的桥梁,尤其是在网络上。”

在 Web 开发领域,服务器端软件或应用,如膝关节炎和 Express,会接受请求并输出响应。中间件是在传入请求之后在中间执行的程序或函数,它们产生的输出可以是最终的输出,也可以是下一个中间件使用的输出,直到周期完成。这也意味着我们可以有多个中间件,它们将按照声明的顺序执行:

此外,中间件不仅限于服务器端技术。当你的应用中有路由时,这在客户端也是很常见的。Vue.js 提供的 Vue 路由就是使用这种中间件概念的一个很好的例子。我们已经在第 4 章中研究并使用了 Vue 路由,添加了视图、路由和过渡,为我们的 Vue 应用创建了路由。现在,让我们更深入地了解 Vue 路由的高级用法-导航卫士。

安装 Vue 路由

如果您从一开始就遵循了本书的章节,您应该已经知道如何从第 4 章添加视图、路由和转换中安装 Vue 路由。然而,这里是一个快速回顾。

按照以下步骤直接下载 Vue Router:

  1. 单击以下链接并下载源代码:
https://unpkg.com/vue-router/dist/vue-router.js
  1. 在 Vue 之后包括路由,以便它可以自动安装:
<script src="/path/to/vue.js"></script>
<script src="/path/to/vue-router.js"></script>

或者,您可以通过 npm 安装 Vue 路由:

  1. 使用 npm 将路由安装到您的项目:
$ npm i vue-router
  1. 使用use方法显式注册路由:
import Vue from 'vue'
import VueRouter from 'vue-router'

Vue.use(VueRouter)
  1. 路由安装到位后,您可以使用 Vue 路由附带的导航保护开始创建中间件:
const router = new VueRouter({ ... })
router.beforeEach((to, from, next) => {
  // ...
})

上例中的beforeEach导航卫士是一个全局导航卫士,在导航到任何路线时调用。除了全球警卫,还有特定路线的导航警卫,这是我们将在下一节更详细地探讨的内容。那么,让我们开始吧!

If you want to find out more information about Vue Router, please visit https://router.vuejs.org/.

使用导航卫士

导航卫士用于保护应用中的导航。这些守卫允许我们在进入、更新和离开路由之前调用函数。当某些条件不满足时,它们可以重定向或取消路由。有几种方法可以连接到路线导航过程中:全局、每条路线或组件中。让我们在下一节中探索全球卫士。

Note that you can find all of the following examples in /chapter-11/vue/non-sfc/ from our GitHub repository.

创建全球卫士

Vue 路由提供两种全球防护——防护前的全球防护和防护后的全球防护。在将它们应用到我们的应用之前,让我们先了解如何使用它们:

  • 全局前防护:进入路由时调用全局前防护。它们是按特定顺序调用的,可以是异步的。导航始终处于等待状态,直到解决所有防护。我们可以使用 Vue 路由的beforeEach方法注册这些防护,如下所示:
const router = new VueRouter({ ... })
router.beforeEach((to, from, next) => { ... })
  • 全局后防:进入路由后调用全局后防。与前全局防护不同,后全局防护不具备next功能,因此不会影响导航。我们可以使用 Vue 路由的afterEach方法注册这些防护,如下所示:
const router = new VueRouter({ ... })
router.afterEach((to, from) => { ... })

让我们创建一个带有简单 HTML 页面的 Vue 应用,并在以下步骤中使用这些防护:

  1. 使用<router-link>元素创建两条路由,如下所示:
<div id="app">
  <p>
    <router-link to="/page1">Page 1</router-link>
    <router-link to="/page2">Page 2</router-link>
  </p>
  <router-view></router-view>
</div>
  1. 定义路由的组件(Page1Page2,并将它们传递给<script>块中的路由实例:
const Page1 = { template: '<div>Page 1</div>' }
const Page2 = { template: '<div>Page 2</div>' }

const routes = [
  { path: '/page1', component: Page1 },
  { path: '/page2', component: Page2 }
]

const router = new VueRouter({
  routes
})
  1. 在路由实例之后声明全局前防护和全局后防护,如下所示:
router.beforeEach((to, from, next) => {
  console.log('global before hook')
  next()
})

router.afterEach((to, from,) => {
  console.log('global after hook')
})
  1. 在防护装置之后装载根实例并运行我们的应用:
const app = new Vue({
  router
}).$mount('#app')
  1. 在浏览器中运行应用,当您在路由之间切换时,应在浏览器控制台中获取以下日志:
global before hook
global after hook

当您想要将通用的东西应用于所有路由时,全局防护非常有用。然而,有时,我们只需要为某些路线提供特定的服务。为此,应使用每条路线的防护装置。让我们在下一节中学习如何部署它们。

创建每条路线的防护装置

我们可以直接在路由的配置对象上使用beforeEnter作为方法或属性来创建每路由防护。例如,请看以下内容:

beforeEnter: (to, from, next) => { ... }
// or:
beforeEnter (to, from, next) { ... }

让我们复制以前的 Vue 应用,并更改路由配置,以使用这些每路由防护,如下所示:

const routes = [
  {
    path: '/page1',
    component: Page1,
    beforeEnter: (to, from, next) => {
      console.log('before entering page 1')
      next()
    }
  },
  {
    path: '/page2',
    component: Page2,
    beforeEnter (to, from, next) {
      console.log('before entering page 2')
      next()
    }
  }
]

当您导航到/page1时,应在浏览器控制台上获取before entering page 1日志,当您在/page2上时,应获取before entering page 2日志。那么,既然我们可以对页面的路由应用防护,那么对路由组件本身应用防护呢?答案是肯定的,我们可以。让我们继续下一节,学习如何在组件保护中使用来保护特定组件。

在组件保护中创建

我们可以在路由组件内单独或一起使用以下方法来创建特定组件的导航卫士。

路由护卫前的

就像在全局 before-guard 和每个 route-guard 中一样,beforeRouteEnterguard 在 route 呈现组件之前被调用,但它应用于组件本身。我们可以使用beforeRouteEnter方法注册此类防护装置,如下所示:

beforeRouteEnter (to, from, next) { ... }

因为它是在组件实例之前调用的,所以它不能通过this关键字访问 Vue 组件。但这可以通过将 Vue 组件的回调传递给next参数来解决:

beforeRouteEnter (to, from, next) {
  next(vueComponent => { ... })
}

赛前安全防护

相比之下,当路由呈现的组件即将导航离开它时,调用beforeRouteLeave防护。由于它在呈现 Vue 组件时被调用,因此它可以通过this关键字访问 Vue 组件。我们可以使用beforeRouteLeave方法注册此类防护装置,如下所示:

beforeRouteLeave (to, from, next) { ... }

通常,这种类型的防护装置最好用于防止用户意外离开路线。因此,可以通过调用next(false)取消导航:

beforeRouteLeave (to, from, next) {
  const confirmed = window.confirm('Are you sure you want to leave?')
  if (confirmed) {
    next()
  } else {
    next(false)
  }
}

路由前更新防护装置

当路由呈现的组件发生变化,但组件在新路由中被重用时,调用beforeRouteUpdate防护;例如,如果您有使用相同路由组件的子例程组件:/page1/foo/page1/bar。因此,从/page1/foo导航到/page1/bar将触发此方法。由于在渲染组件时会调用它,因此它可以通过this关键字访问 Vue 组件。我们可以使用beforeRouteUpdate方法注册此类防护装置:

beforeRouteUpdate (to, from, next) { ... }

注意,beforeRouteEnter方法是next方法中唯一支持回调的保护。在调用beforeRouteUpdatebeforeRouteLeave方法之前,Vue 组件已经可用。因此,不支持在next方法中使用回调,因为这是不必要的。因此,如果要访问 Vue 组件,只需使用this关键字:

beforeRouteUpdate (to, from, next) {
  this.name = to.params.name
  next()
}

现在,让我们使用以下方法创建一个带有简单 HTML 页面的 Vue 应用:

  1. 使用beforeRouteEnterbeforeRouteUpdatebeforeRouteLeave方法一起创建页面组件,如下所示:
const Page1 = {
  template: '<div>Page 1 {{ $route.params.slug }}</div>',
  beforeRouteEnter (to, from, next) {
    console.log('before entering page 1')
    next(vueComponent => {
      console.log('before entering page 1: ', 
       vueComponent.$route.path)
    })
  },
  beforeRouteUpdate (to, from, next) {
    console.log('before updating page 1: ', this.$route.path)
    next()
  },
  beforeRouteLeave (to, from, next) {
    console.log('before leaving page 1: ', this.$route.path)
    next()
  }
}
  1. 仅使用beforeRouteEnterbeforeRouteLeave方法创建另一个页面组件,如下所示:
const Page2 = {
  template: '<div>Page 2</div>',
  beforeRouteEnter (to, from, next) {
    console.log('before entering page 2')
    next(vueComponent => {
      console.log('before entering page 2: ', 
       vueComponent.$route.path)
    })
  },
  beforeRouteLeave (to, from, next) {
    console.log('before leaving page 2: ', this.$route.path)
    next()
  }
}
  1. 在启动路由实例之前,定义主路由和子例程,如下所示:
const routes = [
  {
    path: '/page1',
    component: Page1,
    children: [
      {
        path: ':slug'
      }
    ]
  },
  {
    path: '/page2',
    component: Page2
  }
]
  1. 使用<router-link>Vue 组件创建导航链接,如下所示:
<div id="app">
  <ul>
    <li><router-link to="/">Home</router-link></li>
    <li><router-link to="/page1">Page 1</router-link></li>
    <li><router-link to="/page1/foo">Page 1: foo</router-link></li>
    <li><router-link to="/page1/bar">Page 1: bar</router-link></li>
    <li><router-link to="/page2">Page 2</router-link></li>
  </ul>
  <router-view></router-view>
</div>
  1. 在浏览器中运行应用,在路由之间切换时,应在浏览器控制台中获取以下日志:

  2. 当从/导航到/page1时,您应该看到以下内容:

before entering page 1
before entering page 1: /page1
  • 当从/page1导航到/page2时,您应该看到以下内容:
before leaving page 1: /page1
before entering page 2
before entering page 2: /page2
  • 当从/page2导航到/page1/foo时,您应该看到以下内容:
before leaving page 2: /page2
before entering page 1
before entering page 1: /page1/foo
  • 当从/page1/foo导航到/page1/bar时,您应该看到以下内容:
before updating page 1: /page1/foo
  • 当从/page1/bar导航到/时,您应该看到以下内容:
before leaving page 1: /page1/bar

如您所见,Vue 中的导航卫士只是 JavaScript 函数,允许我们使用一些默认参数创建中间件。现在,让我们仔细看看每个保护方法进入下一节的参数(AutoT0T,AuthT1,和 TyT2)。

了解导航卫士参数:to、from 和 next

您已经在前面部分中使用的导航卫士中看到了这些参数,但我们还没有向您介绍它们。除afterEach全局保护外,所有保护都使用以下三个参数:tofromnext

to参数

此参数是您导航到的 route 对象(因此,它被称为参数)。此对象保存 URL 和路由的解析信息:

| 名称元路径搞砸 | 查询 params 全程匹配 |

如果您想了解更多关于这些对象属性的信息,请访问https://router.vuejs.org/api/the-route-object

from参数

此参数是从中导航的当前路由对象。同样,此对象保存 URL 和路由的解析信息:

| 名称元路径搞砸 | 查询 params 全程匹配 |

next参数

此参数是您必须调用的函数,以便转到队列中的下一个守卫(中间件)。如果要中止当前导航,可以向此函数传递一个false布尔值:

next(false)

如果要重定向到其他位置,可以使用以下行:

next('/')
// or
next({ path: '/' })

如果您想使用Error实例中止导航,可以使用以下行:

const error = new Error('An error occurred!')
next(error)

然后,您可以从根目录捕获错误:

router.onError(err
 => { ... })

现在,让我们用一个简单的 HTML 页面创建一个 Vue 应用,并在以下步骤中尝试下一个函数:

  1. 使用beforeRouteEnter方法创建以下页面组件,如下所示:
const Page1 = {
  template: '<div>Page 1</div>',
  beforeRouteEnter (to, from, next) {
    const error = new Error('An error occurred!')
    error.statusCode = 500
    console.log('before entering page 1')
    next(error)
  }
}

 const Page2 = {
  template: '<div>Page 2</div>',
  beforeRouteEnter (to, from, next) {
    console.log('before entering page 2')
    next({ path: '/' })
  }
}

在前面的代码中,我们将Error实例传递给Page1的下一个函数,同时将路由重定向到Page2的主页。

  1. 在启动路由实例之前定义路由,如下所示:
const routes = [
  {
    path: '/page1',
    component: Page1
  },
  {
    path: '/page2',
    component: Page2
  }
]
  1. 创建路由实例,使用onError方法监听错误:
const router = new VueRouter({
  routes
})

router.onError(err => {
  console.error('Handling this error: ', err.message)
  console.log(err.statusCode)
})
  1. 使用<router-link>Vue 组件创建以下导航链接:
<div id="app">
  <ul>
    <li><router-link to="/">Home</router-link></li>
    <li><router-link to="/page1">Page 1</router-link></li>
    <li><router-link to="/page2">Page 2</router-link></li>
  </ul>
  <router-view></router-view>
</div>
  1. 在浏览器中运行应用,在路由之间切换时,应在浏览器控制台中获取以下日志:

  2. 当从/导航到/page1时,您应该看到以下内容:

before entering page 1
Handling this error: An error occurred!
500
  • 当从/page1导航到/page2时,您应该看到以下内容:
before entering page 2

您还将注意到,由于这一行代码:next({ path: '/' }),当您从/page1导航到/page2时,您被定向到/

到目前为止,我们已经在一个 HTML 页面中创建了中间件。但是,在实际项目中,我们应该尝试使用您在前几章中了解的 Vue 单文件组件(SFC)创建它们。因此,在下一节中,您将学习使用 Vue CLI 在 Vue SFC 中创建中间件,而不是迄今为止学习的自定义网页包构建过程。那么,让我们开始吧。

介绍 Vue-CLI

我们已经在第 5 章中使用 webpack 创建了我们的自定义 Vue SFC 应用,添加了 Vue 组件。作为一名开发人员,了解如何窥探复杂事物的机制非常有用,我们还必须了解如何使用通用和标准模式与其他人协作。因此,现在我们倾向于使用框架。Vue CLI 是 Vue 应用开发的标准工具。它与我们的 webpack 定制工具的功能相同,甚至更多。如果您不想创建自己的 Vue SFC 开发工具,Vue CLI 是一个不错的选择。它支持 Babel、ESLint、TypeScript、postss、PWA、单元测试和开箱即用的端到端测试。要了解有关 Vue CLI 的更多信息,请访问https://cli.vuejs.org/

安装 Vue CLI

开始使用 Vue CLI 非常容易。执行以下步骤:

  1. 使用 npm 进行全局安装:
$ npm i -g @vue/cli
  1. 如果要执行以下操作,请创建项目:
$ vue create my-project
  1. 系统将提示您选择一个预设值–defaultmanually select features,如下所示:
Vue CLI v4.4.6
? Please pick a preset: (Use arrow keys)
> default (babel, eslint) 
  Manually select features 
  1. 选择default预设,因为我们可以在以后手动安装所需的内容。安装完成后,您应该在终端中看到与以下输出的最后一部分类似的内容:
Successfully created project my-project. 
Get started with the following commands: 

 $ cd my-project
 $ npm run serve
  1. 将您的目录更改为my-project并开始开发过程:
$ npm run serve

你应该得到类似的东西:

 DONE Compiled successfully in 3469ms

  App running at:
  - Local: http://localhost:8080/
  - Network: http://199.188.0.44:8080/

  Note that the development build is not optimized.
  To create a production build, run npm run build.

在以下各节中,我们将使用 Vue CLI 将您在前几节中了解的导航保护转换为适当的中间件。这意味着我们将把所有的钩子和防护装置分离成单独的.js文件,并将它们保存在一个名为middlewares的公共文件夹中。但是,在此之前,我们应该首先了解 Vue CLI 为我们生成的项目目录结构,然后添加我们自己所需的目录。让我们开始吧。

了解 Vue CLI 的项目结构

使用 Vue CLI 创建项目后,如果查看项目目录,您会发现它为我们提供了一个基本结构,如下所示:

├── package.json
├── babel.config.js
├── README.md
├── public
│ ├── index.html
│ └── favicon.ico
└── src
    ├── App.vue
    ├── main.js
    ├── router.js
    ├── components
    │ └── HelloWorld.vue
    └── assets
        └── logo.png

根据这一基本结构,我们可以构建和发展我们的应用。因此,让我们在/src/目录中开发我们的应用,并使用路由文件将以下目录添加到其中:

└── src
    ├── middlewares/
    ├── store/
    ├── routes/
    └── router.js

我们将创建两个路由组件,login 和 secured,作为 SFC 页面,并将 secured 页面设置为 403 受保护页面,这将要求用户登录以提供访问页面的姓名和年龄。下面是这个简单的 Vue 应用需要的/src/目录中的文件和结构:

└── src
    ├── App.vue
    ├── main.js
    ├── router.js
    ├── components
    │ ├── secured.vue
    │ └── login.vue
    ├── assets
    │ └── ...
    ├── middlewares
    │ ├── isLoggedIn.js
    │ └── isAdult.js
    ├── store
    │ ├── index.js
    │ ├── mutations.js
    │ └── actions.js
    └── routes
        ├── index.js
        ├── secured.js
        └── login.js

我们现在知道了我们的应用需要哪些目录和文件。接下来,我们将继续编写这些文件的代码。

使用 Vue CLI 编写中间件和 Vuex 存储

如果您查看package.json,您将看到 Vue CLI 附带的默认依赖项非常基本且最小:

// package.json
"dependencies": {
  "core-js": "^2.6.5",
  "vue": "^2.6.10"
}

因此,我们将在以下步骤中安装项目依赖项并编写所需的代码:

  1. 通过 npm 安装以下软件包:
$ npm i vuex
$ npm i vue-router
$ npm i vue-router-multiguard

Note that Vue does not support multiple guards per route. So, if you want to create more than one guard for a route, Vue Router Multiguard allows you to do this. For more information about this package, please visit https://github.com/atanas-dev/vue-router-multiguard.

  1. 创建状态、操作和变体,以将经过身份验证的用户详细信息存储在 Vuex 存储中,以便任何组件都可以访问这些详细信息:
// src/store/index.js
import Vue from 'vue'
import Vuex from 'vuex'

import actions from './actions'
import mutations from './mutations'

Vue.use(Vuex)

export default new Vuex.Store({
  state: { user: null },
  actions,
  mutations
})

为了可读性和简单性,我们将存储的操作分离到一个单独的文件中,如下所示:

// src/store/actions.js
const actions = {
  async login({ commit }, { name, age }) {
    if (!name || !age) {
      throw new Error('Bad credentials')
    }
    const data = {
      name: name,
      age: age
    }
    commit('setUser', data)
  },

  async logout({ commit }) {
    commit('setUser', null)
  }
}
export default actions

我们还将把存储区的突变分离到一个单独的文件中,如下所示:

// src/store/mutations.js
const mutations = {
  setUser (state, user) {
    state.user = user
  }
}
export default mutations
  1. 创建中间件以确保用户已登录:
// src/middlewares/isLoggedIn.js
import store from '../store'

export default (to, from, next) => {
  if (!store.state.user) {
    const err = new Error('You are not connected')
    err.statusCode = 403
    next(err)
  } else {
    next()
  }
}
  1. 创建另一个中间件以确保用户年龄超过 18 岁:
// src/middlewares/isAdult.js
import store from '../store'

export default (to, from, next) => {
  if (store.state.user.age < 18) {
    const err = new Error('You must be over 18')
    err.statusCode = 403
    next(err)
  } else {
    next()
  }
}
  1. 通过使用vue-router-multiguardbeforeEnter中插入多个中间件,将这两个中间件导入到安全路径中:
// src/routes/secured.js
import multiguard from 'vue-router-multiguard'
import secured from '../components/secured.vue'
import isLoggedIn from '../middlewares/isLoggedIn'
import isAdult from '../middlewares/isAdult'

export default {
  name: 'secured',
  path: '/secured',
  component: secured,
  beforeEnter: multiguard([isLoggedIn, isAdult])
}
  1. 使用简单的登录页面创建客户端身份验证。以下是loginlogout方法所需的基本输入字段:
// src/components/login.vue
<form @submit.prevent="login">
  <p>Name: <input v-model="name" type="text" name="name"></p>
  <p>Age: <input v-model="age" type="number" name="age"></p>
  <button type="submit">Submit</button>
</form>

export default {
  data() {
    return {
      error: null,
      name: '',
      age: ''
    }
  },
  methods: {
    async login() { ... },
    async logout() { ... }
  }
}
  1. 通过调度trycatch块中的loginlogout动作方法,完成前面的loginlogout方法,如下所示:
async login() {
  try {
    await this.$store.dispatch('login', {
      name: this.name,
      age: this.age
    })
    this.name = ''
    this.age = ''
    this.error = null
  } catch (e) {
    this.error = e.message
  }
},
async logout() {
  try {
    await this.$store.dispatch('logout')
  } catch (e) {
    this.error = e.message
  }
}
  1. 将完成的登录组件导入登录路由,如下所示:
// src/routes/login.js
import Login from '../components/login.vue'

export default {
  name: 'login',
  path: '/',
  component: Login
}

请注意,我们将此路由命名为login,因为当我们从前面的中间件获得身份验证错误时,我们将需要此名称来重定向导航路由。

  1. loginsecured路由导入索引路由,如下所示:
// src/routes/index.js
import login from './login'
import secured from './secured'

const routes = [
  login,
  secured
]

export default routes
  1. 将前面的索引路由导入 Vue 路由实例,使用router.onError捕捉路由错误,如下所示:
// src/router.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import Routes from './routes'

Vue.use(VueRouter)

const router = new VueRouter({
  routes: Routes
})

router.onError(err => {
  alert(err.message)
  router.push({ name: 'login' })
})

export default router

在这一步中,我们使用router.onError处理从中间件传递过来的Error对象,当不满足认证条件时,使用router.push将导航路径重定向到登录页面。对象名称必须与步骤 7中的登录路径相同,即登录

  1. 导入路由并将其存储在main文件中:
// src/main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'

new Vue({
  router,
  store,
  render: h => h(App),
}).$mount('#app')
  1. 使用npm run serve运行项目,您应该会看到应用在localhost:8080加载。如果您在主页的输入字段中键入一个名称和一个小于 18 的数字,然后单击登录按钮,则在尝试访问受保护的页面时,您应该会收到一条警告,提示“您必须超过 18 岁”。另一方面,如果键入的数字大于 18,则应在安全页面上看到名称和数字:
Name: John
Age: 20

You can find the entire code for this app in /chapter-11/vue/vue-cli/basic/ from our GitHub repository. You can also find the app with the custom webpack in /chapter-11/vue/webpack/.

做得好!您已经完成了有关 Vue 项目中间件的所有章节。现在,让我们在下一节中应用您刚刚学到的有关 Nuxt 项目的知识。

在 Nuxt 中编写路由中间件

和往常一样,一旦我们了解了中间件在 Vue 中的工作原理,那么在 Nuxt 中使用它就更容易了,因为 Nuxt 已经为我们处理了 Vue 路由。在接下来的部分中,我们将学习如何为 Nuxt 应用使用全局和每路由中间件。

在 Nuxt 中,所有中间件都应该保存在/middleware/目录中,中间件文件名将是中间件的名称。例如,/middleware/user.js是用户中间件。中间件获取 Nuxt 上下文作为其第一个参数:

export default (context) => { ... }

此外,中间件可以是异步的:

export default async (context) => {
   const { data } = await axios.get('/api/path')
}

在通用模式下,中间件在服务器端调用一次(例如,当第一次请求 Nuxt 应用或刷新页面时),然后在导航到其他路由时在客户端调用。另一方面,无论您是第一次请求应用,还是在第一次请求后导航到其他路线,客户端始终会调用中间件。中间件首先在 Nuxt 配置文件中执行,然后在布局中执行,最后在页面中执行。在下一节中,我们将开始编写一些全球中间件。

编写全球中间件

添加全球中间件非常简单;您只需在config文件的router选项中的middleware键中声明它们。例如,请看以下内容:

// nuxt.config.js
export default {
  router: {
    middleware: 'auth'
  }
}

现在,让我们在以下步骤中创建一些全局中间件。在本练习中,我们希望从 HTTP 请求头获取用户代理的信息,并跟踪用户导航到的路由:

  1. /middleware/目录中创建两个中间件,一个用于获取用户代理信息,另一个用于获取用户导航到的路由路径信息:
// middleware/user-agent.js
export default (context) => {
  context.userAgent = process.server ? context.req.headers[
    'user-agent'] : navigator.userAgent
}

// middleware/visits.js
export default ({ store, route, redirect }) => {
  store.commit('addVisit', route.path)
}
  1. router选项的middleware键中声明前面的中间件,如下所示:
// nuxt.config.js
module.exports = {
  router: {
    middleware: ['visits', 'user-agent']
  }
}

Note that, in Nuxt, we do not need a third-party package like we do in the Vue app to call multiple guards.

  1. 创建存储访问路线的存储状态和状态:
// store/state.js
export default () => ({
  visits: []
})

// store/mutations.js
export default {
  addVisit (state, path) {
    state.visits.push({
      path,
      date: new Date().toJSON()
    })
  }
}
  1. 使用about页面中的user-agent中间件:
// pages/about.vue
<p>{{ userAgent }}</p>

export default {
  asyncData ({ userAgent }) {
    return {
      userAgent
    }
  }
}
  1. 至于visits中间件,我们想在一个组件上使用它,然后将这个组件注入我们的布局,即default.vue布局。首先,在/components/目录中创建visits组件:
// components/visits.vue
<li v-for="(visit, index) in visits" :key="index">
  <i>{{ visit.date | dates }} | {{ visit.date | times }}</i> - {{ 
    visit.path }}
</li>

export default {
  filters: {
    dates(date) {
      return date.split('T')[0]
    },
    times(date) {
      return date.split('T')[1].split('.')[0]
    }
  },
  computed: {
    visits() {
      return this.$store.state.visits.slice().reverse()
    }
  }
}

因此,我们在这个组件中创建了两个过滤器。date过滤器用于从字符串中获取日期。例如,我们将从2019-05-24T21:55:44.673Z中获取2019-05-24。相比之下,time过滤器用于从字符串获取时间。例如,我们将从2019-05-24T21:55:44.673Z中获取21:55:44

  1. visits组件导入我们的布局:
// layouts/default.vue
<template>
  <Visits />
</template>

import Visits from '~/components/visits.vue'
export default {
  components: {
    Visits
  }
}

当我们在路线周围导航时,应在浏览器中获得以下结果:

2019-06-06 | 01:55:44 - /contact
2019-06-06 | 01:55:37 - /about
2019-06-06 | 01:55:30 - /

此外,当您在“关于”页面上时,我们应该从请求头获取用户代理的信息:

Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.75 Safari/537.36

You can find the preceding source code in /chapter-11/nuxt-universal/route-middleware/global/ in our GitHub repository.

这就是全局中间件的全部内容。现在,让我们在下一节中继续讨论每路中间件。

按路径写入中间件

添加每条路线的中间件也非常简单;您只需在特定布局或页面的middleware键中声明它们。例如,请看以下内容:

// pages/index.vue or layouts/default.vue
export default {
  middleware: 'auth'
}

因此,让我们在下面的步骤中创建一些每路由中间件。在本练习中,我们将使用会话和 JSON Web 令牌(JWT)访问受限页面或安全 API。虽然在现实生活中,我们可以只使用会话或令牌作为身份验证系统,但我们将在练习中使用这两种方法,以便我们知道如何将它们一起用于可能更复杂的生产系统。在我们的练习中,我们希望用户登录并从服务器获取令牌。当令牌过期或无效时,用户将无法访问安全路由。

此外,当会话时间结束时,用户将注销:

  1. 创建一个auth中间件来检查我们商店中的状态是否有任何数据。如果没有经过验证的数据,那么我们使用 Nuxt 上下文中的error函数将错误发送到前端:
// middleware/auth.js
export default function ({ store, error }) {
  if (!store.state.auth) {
    error({
      message: 'You are not connected',
      statusCode: 403
    })
  }
}
  1. 创建token中间件,确保令牌在店内;否则,它会将错误发送到前端。如果存储中存在令牌,我们将令牌的Authorization设置为默认的axios头:
// middleware/token.js
export default async ({ store, error }) => {
  if (!store.state.auth.token) {
    error({
      message: 'No token',
      statusCode: 403
    })
  }
  axios.defaults.headers.common['Authorization'] = `Bearer: ${store.state.auth.token}`
}
  1. 将上述两个中间件添加到安全页面上的middleware键:
// pages/secured.vue
<p>{{ greeting }}</p>

export default {
  async asyncData ({ redirect }) {
    try {
      const { data } = await axios.get('/api/private')
      return {
        greeting: data.data.message
      }
    } catch (error) {
      if(process.browser){
        alert(error.response.data.message)
      }
      return redirect('/login')
    }
  },
  middleware: ['auth', 'token']
}

通过在头中设置 JWT 的Authorization头,我们可以访问由服务器端中间件保护的安全 API 路由(我们将在第 12 章创建用户登录和 API 认证中了解更多信息)。我们将从想要访问的安全 API 路由中获取数据,如果令牌不正确或已过期,我们将收到错误消息提示。

  1. /store/目录中创建存储的状态、变化和操作,以存储经过身份验证的数据:
// store/state.js
export default () => ({
  auth: null
})

// store/mutations.js
export default {
  setAuth (state, data) {
    state.auth = data
  }
}

// store/actions.js
export default {
  async login({ commit }, { username, password }) {
    try {
      const { data } = await axios.post('/api/public/users/login', 
      { username, password })
      commit('setAuth', data.data)
    } catch (error) {
      // handle error
    }
  },

  async logout({ commit }) {
    await axios.post('/api/public/users/logout')
    commit('setAuth', null)
  }
}

已知和预期的行为是,当​ 页面刷新后,存储的状态将重置为默认状态。如果我们想保持这种状态,可以使用以下几种解决方案:

  1. 本地存储
  2. 会话存储
  3. vuex persistedstate(vuex 插件)

然而,在我们的例子中,由于我们使用会话来存储经过身份验证的信息,我们实际上可以通过以下方式从会话中回溯数据:

  1. req.ctx.session(Koa)或 req.session(Express)
  2. req.headers.cookie

一旦我们决定了我们想要的解决方案或选项(比如req.headers.cookie,那么我们可以按如下方式重新填充状态:

// store/index.js
const cookie = process.server ? require('cookie') : undefined

export const actions = {
  nuxtServerInit({ commit }, { req }) {
    var session = null
    var auth = null
    if (req.headers.cookie && req.headers.cookie.indexOf('koa:sess') > -1) {
      session = cookie.parse(req.headers.cookie)['koa:sess']
    }
    if (session) {
      auth = JSON.parse(Buffer.from(session, 'base64'))
      commit('setAuth', auth)
    }
  }
}

You can find the preceding source code in /chapter-11/nuxt-universal/route-middleware/per-route/ in our GitHub repository.

在完成上述所有步骤并创建中间件后,我们可以使用npm run dev运行这个简单的身份验证应用,看看它是如何工作的。我们将在下一章中讨论服务器端身份验证。现在,我们只需要关注中间件并了解它是如何工作的,这将在下一章帮助我们。现在,让我们进入本章的最后一部分-服务器中间件。

写入 Nuxt 服务器中间件

简单地说,服务器中间件是在 Nuxt 中用作中间件的服务器端应用。我们一直在服务器端框架下运行我们的 NUXT 应用,比如膝关节炎,因为 Oracle T2A.第 8 章 AUTT3T,ORT T4。如果您使用的是 Express,这是您的package.json文件中的scripts对象:

// package.json
"scripts": {
  "dev": "cross-env NODE_ENV=development nodemon server/index.js --watch 
   server",
  "build": "nuxt build",
  "start": "cross-env NODE_ENV=production node server/index.js",
  "generate": "nuxt generate"
}

在此 npm 脚本中,devstart脚本指示服务器从/server/index.js运行您的应用。这可能并不理想,因为我们已经将 Nuxt 和服务器端框架紧密地耦合在一起,这会导致配置中的额外工作。但是,我们可以告诉 Nuxt 不要附加到/server/index.js中的服务器端框架配置,并保留我们原来的 Nuxt 运行脚本,如下所示:

// package.json
"scripts": {
  "dev": "nuxt",
  "build": "nuxt build",
  "start": "nuxt start",
  "generate": "nuxt generate"
}

相反,我们可以使用 Nuxt 配置文件中的serverMiddleware属性,让服务器端框架在 Nuxt 下运行。例如,请看以下内容:

// nuxt.config.js
export default {
  serverMiddleware: [
    '~/api'
  ]
}

与路由中间件不同,路由中间件在客户端的每个路由之前被调用,服务器中间件总是在服务器端的vue-server-renderer之前被调用。因此,服务器中间件可以用于服务器特定的任务,就像我们以前的章节中使用膝关节炎或快件一样。所以,让我们来探讨如何在下一节中使用 Express 和膝关节炎作为我们的服务器中间件。

使用 Express 作为 Nuxt 的服务器中间件

让我们使用 Express 作为 Nuxt 的服务器中间件创建一个简单的身份验证应用。我们仍将使用身份验证练习中的客户端代码以及您在上一节中了解的每路由中间件,其中要求用户提供用户名和密码以访问安全页面。此外,我们将像以前一样使用 Vuex 存储来集中经过身份验证的用户数据。本练习的主要区别在于,我们的 Nuxt 应用将作为中间件移出服务器端应用,而服务器端应用将作为中间件移入Nuxt 应用。那么,让我们从以下步骤开始:

  1. 安装cookie-sessionbody-parser作为服务器中间件,并在 Nuxtconfig文件中在它们之后添加我们 API 的路径,如下所示:
// nuxt.config.js
import bodyParser from 'body-parser'
import cookieSession from 'cookie-session'

export default {
  serverMiddleware: [
    bodyParser.json(),
    cookieSession({
      name: 'express:sess',
      secret: 'super-secret-key',
      maxAge: 60000
    }),
    '~/api'
  ]
}

请注意,cookie 会话是一种基于 cookie 的 Express 会话中间件,它将会话存储在客户端的 cookie 中。相比之下,体分析器是一个用于解析的身体解析中间件,就像 KoA 中的 Ont0,你在《膝关节炎》T1 章第 8 章中了解到了 To2 T2,AUTT3。

For more information about cookie-session and body-parser for Express, please visit https://github.com/expressjs/cookie-session and https://github.com/expressjs/body-parser.

  1. 使用index.js文件创建/api/目录,其中 Express 作为另一个服务器中间件导入导出:
// api/index.js
import express from 'express'
const app = express()

app.get('/', (req, res) => res.send('Hello World!'))

// Export the server middleware
export default {
  path: '/api',
  handler: app
}
  1. 使用npm run dev运行应用,您应该会在localhost:3000/api中看到“Hello World!”消息。
  2. /api/index.js中增加loginlogoutpost 方式,如下:
// api/index.js
app.post('/login', (req, res) => {
  if (req.body.username === 'demo' && req.body.password === 'demo') {
    req.session.auth = { username: 'demo' }
    return res.json({ username: 'demo' })
  }
  res.status(401).json({ message: 'Bad credentials' })
})

app.post('/logout', (req, res) => {
  delete req.session.auth
  res.json({ ok: true })
})

在前面的代码中,当用户成功登录时,我们将经过身份验证的有效负载作为auth存储在 HTTP 请求对象中。然后,我们将在用户注销后删除auth会话,以清除该会话。

  1. 使用state.jsmutations.js创建一个存储,就像编写每路由中间件一样,如下所示:
// store/state.js
export default () => ({
  auth: null,
})

// store/mutations.js
export default {
  setAuth (state, data) {
    state.auth = data
  }
}
  1. 与编写每路由中间件一样,在商店的actions.js文件中创建loginlogout动作方法,如下所示:
// store/actions.js
import axios from 'axios'

export default {
  async login({ commit }, { username, password }) {
    try {
      const { data } = await axios.post('/api/login', { username,
        password })
      commit('setAuth', data)
    } catch (error) {
      // handle error...
    }
  },

  async logout({ commit }) {
    await axios.post('/api/logout')
    commit('setAuth', null)
  }
}
  1. 在存储区的index.js中添加一个nuxtServerInit操作,以在刷新页面时从 HTTP 请求对象中的 Express 会话重新填充状态:
// store/index.js
export const actions = {
  nuxtServerInit({ commit }, { req }) {
    if (req.session && req.session.auth) {
      commit('setAuth', req.session.auth)
    }
  }
}
  1. 最后,就像在每路由中间件身份验证中一样,使用表单在/pages/目录中创建一个登录页面。使用与之前相同的loginlogout方法在商店中调度loginlogout动作方法:
// pages/index.vue
<form v-if="!$store.state.auth" @submit.prevent="login">
  <p v-if="error" class="error">{{ error }}</p>
  <p>Username: <input v-model="username" type="text"
     name="username"></p>
  <p>Password: <input v-model="password" type="password" 
     name="password"></p>
  <button type="submit">Login</button>
</form>

export default {
  data () {
    return {
      error: null,
      username: '',
      password: ''
    }
  },
  methods: {
    async login () { ... },
    async logout () { ... }
  }
}
  1. 使用npm run dev运行应用。您应该有一个与以前一样工作的身份验证应用,但它不再从/server/index.js运行。

You can find the preceding source code in /chapter-11/nuxt-universal/server-middleware/express/ in our GitHub repository.

使用serverMiddleware属性可以让我们的 Nuxt 应用从服务器端应用中释放出来,让它看起来整洁,感觉轻松,你不这么认为吗?通过这种方法,我们也可以使其更加灵活,因为我们可以使用任何服务器端框架或应用。例如,我们不用 KEXE,可以使用膝关节炎,我们将在下一节中研究。

使用膝关节炎作为 NoXT 的服务器中间件

就像膝关节炎和 Express 一样,Connect 是一个简单的框架,用于将各种中间件连接在一起,用于处理 HTTP 请求。Nuxt 在内部使用 Connect 作为服务器,因此大多数 Express 中间件都使用 Nuxt 的服务器中间件。相比之下,膝关节炎中间件作为 NUXT 服务器中间件工作有点困难,因为 OLE T0 和 Ty1 T1 对象是 ToT T3。我们可以用一个简单的“Hello World”消息来比较这三个框架,如下所示:

// Connect
const connect = require('connect')
const app = connect()
app.use((req, res, next) => res.end('Hello World'))

// Express
const express = require('express')
const app = express()
app.get('/', (req, res, next) => res.send('Hello World'))

// Koa
const Koa = require('koa')
const app = new Koa()
app.use(async (ctx, next) => ctx.body = 'Hello World')

请注意,req是 Node.js HTTP 请求对象,res是 Node.js HTTP 响应对象。它们可以被命名为您喜欢的任何名称,例如,请求而不是请求响应而不是回复。从前面的比较中,您可以看到膝关节炎如何处理这两个对象不同于其他框架。因此,膝关节炎膝关节炎的中间件不能像 KEXT 那样使用 KOA,我们不能在 OLE T2AY 属性中定义任何膝关节炎的中间件,而是添加 KOA API 保持的目录路径。请放心,让它们在我们的 Nuxt 应用中作为中间件工作并不困难。让我们继续执行以下步骤:

  1. 添加要用 Koa 创建 API 的路径,如下所示:
// nuxt.config.js
export default {
  serverMiddleware: [
    '~/api'
  ]
}
  1. 导入koakoa-router,与路由创建Hello World!消息,然后导出到/api/目录下的index.js文件:
// api/index.js
import Koa from 'koa'
import Router from 'koa-router'

router.get('/', async (ctx, next) => {
  ctx.type = 'json'
  ctx.body = {
    message: 'Hello World!'
  }
})

app.use(router.routes())
app.use(router.allowedMethods())

// Export the server middleware
export default {
  path: '/api',
  handler: app.listen()
}
  1. 导入 OT0 和 T1,并将其登记为膝关节炎病例中的 MooLeWes,在 ORT T2AL 文件中,如下:
// api/index.js
import bodyParser from 'koa-bodyparser'
import session from 'koa-session'

const CONFIG = {
  key: 'koa:sess',
  maxAge: 60000,
}

app.use(session(CONFIG, app))
app.use(bodyParser())
  1. 使用膝关节炎路由创建 Apple T0 和 Ty1 T1 路由,如下:
// api/index.js
router.post('/login', async (ctx, next) => {
  let request = ctx.request.body || {}
  if (request.username === 'demo' && request.password === 'demo') {
    ctx.session.auth = { username: 'demo' }
    ctx.body = {
      username: 'demo'
    }
  } else {
    ctx.throw(401, 'Bad credentials')
  }
})

router.post('/logout', async (ctx, next) => {
  ctx.session = null
  ctx.body = { ok: true }
})

膝关节炎膝关节炎的对象,在前面的代码中,就像在前面一节中的 Express 示例中一样,当用户登录成功时,我们将认证有效载荷存储在 KOA 上下文对象中的 OKT0。然后,当用户注销时,我们将通过将会话设置为null来清除auth会话。

  1. 创建一个包含状态、突变和操作的存储,就像在 Express 示例中所做的那样。此外,在商店的index.js文件中创建nuxtServerInit,就像您编写每路中间件时所做的那样:
// store/index.js
export const actions = {
  nuxtServerInit({ commit }, { req }) {
    // ...
  }
}
  1. 与前面一样,在/pages/目录中创建表单loginlogout方法,从存储中分派动作方法:
// pages/index.vue
<form v-if="!$store.state.auth" @submit.prevent="login">
  //...
</form>

export default {
  methods: {
    async login () { ... },
    async logout () { ... }
  }
}
  1. 使用npm run dev运行应用。您应该有一个身份验证应用,其工作原理与上一节中 Express 中的相同,但它不再从/server/index.js运行。

You can find the entire source code for this example in /chapter-11/nuxt-universal/server-middleware/koa/ in our GitHub repository.

根据您的偏好,您可以在下一个项目中使用 Express 或膝关节炎作为 NUXT 的服务器中间件。在这本书中,我们主要使用膝关节炎的简单性。您甚至可以创建自定义服务器中间件,而不需要它们中的任何一个。让我们来看看如何在下一节中创建自定义服务器中间件。

创建自定义服务器中间件

由于 Nuxt 内部使用 Connect 作为服务器,所以我们可以添加自定义中间件,而不需要外部服务器,如膝关节炎或 Express。您可以开发一个复杂的 NUXT 服务器中间件,就像我们在膝关节炎和上一节中所表达的一样。然而,让我们不要无休止地重复我们已经做过的事情。让我们创建一个非常基本的自定义中间件,它打印一条“Hello World”消息,以确认通过以下步骤从基本中间件构建复杂中间件的可行性:

  1. 添加要创建自定义中间件的路径:
// nuxt.config.js
serverMiddleware: [
  { path: '/api', handler: '~/api/index.js' }
]
  1. 将 API 路由添加到/api/目录下的index.js文件中:
// api/index.js
export default function (req, res, next) {
  res.end('Hello world!')
}
  1. 使用npm run dev运行应用并导航到localhost:3000/api。您应该会看到屏幕上打印的“Hello World!”消息。

You can refer to the Connect documentation at https://github.com/senchalabs/connect for more information. Additionally, you can find the source code for this example in /chapter-11/nuxt-universal/server-middleware/custom/ in our GitHub repository.

做得好!你已经设法完成了 Nuxt 的另一个重要章节。在继续下一章之前,让我们总结一下您到目前为止学到的知识。

总结

在本章中,您学习了路由中间件和服务器中间件之间的区别。您使用 Vue 路由的导航卫士为 Vue 应用创建中间件。您还使用 Vue CLI 开发了一个简单的 Vue 身份验证应用。根据您对 Vue 应用的了解,您在 Nuxt 应用中使用全局和每路由中间件实现了相同的概念(路由中间件)。之后,您成功地了解了 Nuxt 的服务器中间件以及如何使用 Express 和膝关节炎作为服务器中间件。中间件非常重要和有用,特别是在身份验证和安全方面。我们已经制作了一些身份验证应用,我们将在下一章更详细地研究和理解它们。

在下一章中,您将详细了解如何开发用户登录和身份验证 API,以改进您在本章中创建的身份验证应用。我们将引导您完成基于会话的身份验证和基于令牌的身份验证。虽然您已经使用这两种技术创建了一个身份验证应用,但我们尚未解释它们是什么。但请放心,在下一章中您将更好地理解它们。除此之外,您还将学习如何为您的 Nuxt 应用创建后端和前端身份验证以及使用 Google OAuth 登录。所以,请继续关注!