在 Nuxt 中编写路由中间件

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

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

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

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

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

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

编写全局中间件

添加全局中间件非常简单;你只需要在配置文件的 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)
    }
  2. router 选项的 middleware 键中声明上述中间件,如下所示:

    // nuxt.config.js
    module.exports = {
      router: {
        middleware: ['visits', 'user-agent']
      }
    }

    请注意,在 Nuxt 中,我们不需要像在 Vue 应用中那样使用第三方包来调用多个守卫。

  3. 创建 storestatemutations 来存储访问过的路由:

    // store/state.js
    export default () => ({
      visits: []
    })
    
    // store/mutations.js
    export default {
      addVisit (state, path) {
        state.visits.push({
          path,
          date: new Date().toJSON()
        })
      }
    }
  4. about 页面中使用 user-agent 中间件:

    // pages/about.vue
    <template>
      <p>{{ userAgent }}</p>
    </template>
    
    <script>
    export default {
      asyncData ({ userAgent }) {
        return {
          userAgent
        }
      }
    }
    </script>
  5. 至于 visits 中间件,我们想在一个组件中使用它,然后将这个组件注入到我们的布局中,即 default.vue 布局。首先,在 /components/ 目录中创建 visits 组件:

    // components/visits.vue
    <template>
      <ul>
        <li v-for="(visit, index) in visits" :key="index">
          <i>{{ visit.date | dates }} | {{ visit.date | times }}</i> - {{ visit.path }}
        </li>
      </ul>
    </template>
    
    <script>
    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()
        }
      }
    }
    </script>

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

  6. visits 组件导入到我们的布局中:

    // layouts/default.vue
    <template>
      <div>
        <nuxt />
        <Visits />
      </div>
    </template>
    
    <script>
    import Visits from '~/components/visits.vue'
    export default {
      components: {
        Visits
      }
    }
    </script>

    当我们在不同的路由之间导航时,应该在浏览器中看到以下结果:

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

    此外,当你在 about 页面时,你应该从请求头中获取到用户代理的信息:

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

    你可以在我们的 GitHub 仓库的 /chapter-11/nuxt-universal/route-middleware/global/ 中找到上述源代码。

全局中间件就介绍到这里。现在,让我们在下一节继续学习单个路由中间件。

编写路由前置的中间件

添加单个路由中间件也非常简单;你只需要在特定布局或页面的 middleware 键中声明它们。例如,请看以下代码:

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

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

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

  1. 创建一个 auth 中间件来检查我们 store 中的 state 是否有任何数据。如果没有经过身份验证的数据,那么我们使用 Nuxt 上下文中的 error 函数将错误发送到前端:

    // middleware/auth.js
    export default function ({ store, error }) {
      if (!store.state.auth) {
        error({
          message: '你尚未连接',
          statusCode: 403
        })
      }
    }
  2. 创建一个 token 中间件以确保令牌在 store 中;否则,它会将错误发送到前端。如果令牌存在于 store 中,我们将带有令牌的 Authorization 设置为默认的 axios header

    // middleware/token.js
    import axios from 'axios'
    
    export default async ({ store, error }) => {
      if (!store.state.auth.token) {
        error({
          message: '没有令牌',
          statusCode: 403
        })
      }
      axios.defaults.headers.common['Authorization'] = `Bearer: ${store.state.auth.token}`
    }
  3. 将这两个前面的中间件添加到受保护页面的 middleware 键中:

    // pages/secured.vue
    <template>
      <p>{{ greeting }}</p>
    </template>
    
    <script>
    import axios from 'axios'
    
    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']
    }
    </script>

    headers 中使用 JWT 设置 Authorization header 后,我们可以访问受保护的 API 路由,这些路由受服务器端中间件保护(我们将在第十二章 “创建用户登录和 API 身份验证” 中了解更多信息)。我们将从我们要访问的受保护的 API 路由获取数据,如果令牌不正确或已过期,将收到错误消息提示。

  4. /store/ 目录中创建 storestatemutationsactions 以存储身份验证数据:

    // store/state.js
    export default () => ({
      auth: null
    })
    
    // store/mutations.js
    export default {
      setAuth (state, data) {
        state.auth = data
      }
    }
    
    // store/actions.js
    import axios from 'axios'
    
    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) {
          // 处理错误
        }
      },
      async logout({ commit }) {
        await axios.post('/api/public/users/logout')
        commit('setAuth', null)
      }
    }

一个已知且预期的行为是,当页面刷新时,storestate 将重置为默认值。如果我们想持久化 state,可以使用以下几种解决方案:

  1. localStorage

  2. sessionStorage

  3. vuex-persistedstate (一个 Vuex 插件)

然而,在我们的例子中,由于我们使用 session 来存储身份验证信息,我们实际上可以通过以下方式从 session 中重新获取我们的数据:

  1. req.ctx.session (Koa) 或 req.session (Express)

  2. req.headers.cookie

一旦我们决定了我们想要使用的解决方案或选项(假设是 req.headers.cookie),那么我们可以按如下方式重新填充 state

// 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').toString('utf8'))
      commit('setAuth', auth)
    }
  }
}

你可以在我们的 GitHub 仓库的 /chapter-11/nuxt-universal/route-middleware/per-route/ 中找到上述源代码。

当遵循所有上述步骤并创建中间件后,我们可以使用 npm run dev 运行这个简单的身份验证应用,看看它是如何工作的。我们将在下一章学习服务器端身份验证。现在,我们只需要关注中间件并理解它的工作原理,这将有助于我们学习下一章。现在,让我们继续本章的最后一部分——服务器中间件。