使用 Vue Router 编写中间件

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

什么是中间件?

简单来说,中间件是位于两个或多个软件之间的软件层。这是软件开发中的一个古老概念。中间件一词自 1968 年开始使用,并在 20 世纪 80 年代作为将较新的应用程序连接到较旧的遗留系统问题的解决方案而流行起来。关于它的定义有很多,例如(来自 Google 词典)“[中间件是]充当操作系统或数据库与应用程序之间桥梁的软件,尤其是在网络上。”

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

image 2025 04 29 21 41 40 766

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

安装 Vue Router

如果你从一开始就学习本书的章节,你应该已经从第四章 “添加视图、路由和过渡” 中了解如何安装 Vue Router。然而,这里有一个快速回顾。

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

  1. 点击以下链接并下载源代码: https://unpkg.com/vue-router/dist/vue-router.js

  2. 在 Vue 之后包含 router,以便它可以自动安装:

    <script src="/path/to/vue.js"></script>
    <script src="/path/to/vue-router.js"></script>

或者,你可以通过 npm 安装 Vue Router:

  1. 使用 npm 将 router 安装到你的项目中:

    $ npm i vue-router
  2. 使用 use 方法显式注册 router:

    import Vue from 'vue'
    import VueRouter from 'vue-router'
    Vue.use(VueRouter)
  3. 一旦你安装了 router,你就可以开始使用 Vue Router 自带的导航守卫创建中间件:

    const router = new VueRouter({ ... })
    
    router.beforeEach((to, from, next) => {
      // ...
    })

    上面示例中的 beforeEach 导航守卫是一个全局导航守卫,它在导航到任何路由时被调用。除了全局守卫之外,还有特定路由的导航守卫,这正是我们将在下一节中更详细探讨的内容。所以,让我们开始吧!

如果你想了解更多关于 Vue Router 的信息,请访问 https://router.vuejs.org/zh/。

使用导航守卫

导航守卫用于保护你的应用中的导航。这些守卫允许我们在进入、更新和离开路由之前调用函数。当某些条件不满足时,它们可以重定向或取消该路由。有几种方式可以介入路由导航过程:全局的、单个路由的或组件内的。让我们在下一节中探讨全局守卫。

请注意,你可以在我们的 GitHub 仓库的 /chapter-11/vue/non-sfc/ 中找到以下所有示例。

创建全局守卫

Vue Router 提供了两种全局守卫——全局前置守卫和全局后置守卫。让我们在将它们应用到我们的应用之前,先学习如何使用它们:

全局前置守卫 (Global before guards): 全局前置守卫在每次路由进入之前被调用。它们按照特定的顺序被调用,并且可以是异步的。导航会一直等待,直到所有的守卫都被解析完成。我们可以使用 Vue Router 的 beforeEach 方法注册这些守卫,如下所示:

const router = new VueRouter({ ... })
router.beforeEach((to, from, next) => { ... })

全局后置守卫 (Global after guards): 全局后置守卫在每次路由进入之后被调用。与全局前置守卫不同,全局后置守卫没有 next 函数,因此它们不会影响导航。我们可以使用 Vue Router 的 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>
  2. 定义路由的组件(Page1 和 Page2),并在 <script> 块中将它们传递给 router 实例:

    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
    })
  3. 在路由实例之后声明全局前置守卫和全局后置守卫,如下所示:

    router.beforeEach((to, from, next) => {
      console.log('global before hook')
      next()
    })
    
    router.afterEach((to, from,) => {
      console.log('global after hook')
    })
  4. 在守卫之后挂载根实例并运行我们的应用:

    const app = new Vue({
      router
    }).$mount('#app')
  5. 在浏览器中运行该应用,当你在路由之间切换时,你应该在浏览器控制台中看到以下日志:

    global before hook
    global after hook

当你想要对所有路由应用一些通用的操作时,全局守卫非常有用。然而,有时我们只需要对特定的路由进行一些特定的操作。为此,你应该使用 per-route guards(单个路由守卫)。让我们在下一节学习如何部署它们。

创建路由前置的守卫

我们可以通过直接在路由配置对象的 beforeEnter 属性或方法中创建单个路由守卫。例如,请看以下代码:

beforeEnter: (to, from, next) => { ... }
// 或:
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 的日志。既然我们可以将守卫应用于页面的路由,那么将守卫应用于路由组件本身呢?答案是肯定的,我们可以。让我们继续下一节,学习如何使用组件内的守卫来保护特定的组件。

创建组件内的守卫

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

beforeRouteEnter 守卫:

与全局前置守卫和单个路由的 beforeEnter 守卫类似,beforeRouteEnter 守卫在路由渲染组件之前被调用,但它应用于组件本身。我们可以使用 beforeRouteEnter 方法注册这种类型的守卫,如下所示:

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

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

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

beforeRouteLeave 守卫:

相比之下,当路由渲染的组件即将导航离开时,会调用 beforeRouteLeave 守卫。由于它在 Vue 组件渲染之后被调用,因此可以通过 this 关键字访问 Vue 组件。我们可以使用 beforeRouteLeave 方法注册这种类型的守卫,如下所示:

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

通常,这种类型的守卫最适合用于防止用户意外离开路由。因此,可以通过调用 next(false) 来取消导航:

beforeRouteLeave (to, from, next) {
  const confirmed = window.confirm('你确定要离开吗?')
  if (confirmed) {
    next()
  } else {
    next(false)
  }
}

beforeRouteUpdate 守卫:

当路由渲染的组件已更改,但该组件在新路由中被复用时,会调用 beforeRouteUpdate 守卫;例如,如果你有使用相同路由组件的子路由组件:/page1/foo 和 /page1/bar。因此,从 /page1/foo 导航到 /page1/bar 将触发此方法。并且由于它在组件渲染之后被调用,因此可以通过 this 关键字访问 Vue 组件。我们可以使用 beforeRouteUpdate 方法注册这种类型的守卫:

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

请注意,beforeRouteEnter 方法是唯一在 next 方法中支持回调的守卫。在调用 beforeRouteUpdate 和 beforeRouteLeave 方法之前,Vue 组件已经可用。因此,在这两个方法中使用 next 方法中的回调是不支持的,因为它是没有必要的。因此,如果你想访问 Vue 组件,只需使用 this 关键字:

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

现在,让我们创建一个包含一个简单 HTML 页面的 Vue 应用,使用以下守卫:

  1. 创建一个页面组件,其中包含 beforeRouteEnter、beforeRouteUpdate 和 beforeRouteLeave 方法,如下所示:

    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()
      }
    }
  2. 创建另一个仅包含 beforeRouteEnter 和 beforeRouteLeave 方法的页面组件,如下所示:

    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()
      }
    }
  3. 在初始化 router 实例之前定义主路由和子路由,如下所示:

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

理解导航守卫的参数:to, from, 和 next

你在前面章节中使用的导航守卫中已经见过这些参数,但我们尚未详细介绍它们。除了 afterEach 全局守卫之外,所有守卫都使用这三个参数:to、from 和 next。

to 参数:

此参数是你导航到的路由对象(因此,它被称为 to 参数)。此对象包含 URL 和路由的解析信息:

name

meta

path

hash

query

params

fullPath

matched

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

from 参数:

此参数是你当前导航离开的路由对象。同样,此对象包含 URL 和路由的解析信息:

name

meta

path

hash

query

params

fullPath

matched

next 参数:

此参数是一个你必须调用的函数,用于移动到队列中的下一个守卫(中间件)。如果你想中止当前的导航,你可以将一个布尔值 false 传递给这个函数: next(false)

如果你想重定向到不同的位置,你可以使用以下代码行:

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

如果你想使用 Error 的实例中止导航,你可以使用以下代码行:

const error = new Error('发生了一个错误!')
next(error)

然后,你可以在根组件中捕获该错误:

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

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

  1. 使用 beforeRouteEnter 方法创建以下页面组件,如下所示:

    const Page1 = {
      template: '<div>Page 1</div>',
      beforeRouteEnter (to, from, next) {
        const error = new Error('发生了一个错误!')
        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: '/' })
      }
    }

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

  2. 在初始化 router 实例之前定义路由,如下所示:

    const routes = [
      {
        path: '/page1',
        component: Page1
      },
      {
        path: '/page2',
        component: Page2
      }
    ]
  3. 创建 router 的实例并使用 onError 方法监听错误:

    const router = new VueRouter({
      routes
    })
    router.onError(err => {
      console.error('正在处理此错误:', err.message)
      console.log(err.statusCode)
    })
  4. 使用 <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>
  5. 在浏览器中运行该应用,当你在路由之间切换时,你应该在浏览器控制台中看到以下日志:

    • 当从 / 导航到 /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 中创建中间件,而不是你到目前为止学习过的自定义 webpack 构建过程。所以,让我们开始吧。