编写 Nuxt 服务端中间件

简单来说,服务器中间件是在 Nuxt 中用作中间件的服务器端应用程序。自第八章 “添加服务器端框架” 以来,我们一直在 Koa 等服务器端框架下运行我们的 Nuxt 应用程序。如果你使用的是 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 之前调用。因此,服务器中间件可以用于服务器特定的任务,就像我们在之前的章节中使用 KoaExpress 所做的那样。因此,让我们在接下来的章节中探讨如何将 ExpressKoa 用作我们的服务器中间件。

将 Express 用作 Nuxt 的服务端中间件

让我们使用 Express 作为 Nuxt 的服务器中间件创建一个简单的身份验证应用。我们将继续使用来自身份验证练习的客户端代码以及你在上一节中学习的单个路由中间件,在该练习中,用户需要提供用户名和密码才能访问受保护的页面。此外,我们将像以前一样使用 Vuex store 来集中管理经过身份验证的用户数据。此练习的主要区别在于,我们的 Nuxt 应用将作为中间件从服务器端应用中移出,而服务器端应用将作为中间件移入 Nuxt 应用中。因此,让我们按照以下步骤开始:

  1. 安装 cookie-sessionbody-parser 作为服务器中间件,并在 Nuxt 配置文件中将我们的 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-sessionExpress 的基于 cookie 的会话中间件,它将会话存储在客户端的 cookie 中。相比之下,body-parserExpressbody 解析中间件,就像你在第八章 “添加服务器端框架” 中了解到的 Koakoa-bodyparser 一样。

    有关 Expresscookie-sessionbody-parser 的更多信息,请访问 https://github.com/expressjs/cookie-sessionhttps://github.com/expressjs/body-parser。

  2. 使用 index.js 文件创建一个 /api/ 目录,在该文件中导入并导出 Express 作为另一个服务器中间件:

    // api/index.js
    import express from 'express'
    const app = express()
    app.get('/', (req, res) => res.send('Hello World!'))
    // 导出服务器中间件
    export default {
      path: '/api',
      handler: app
    }
  3. 使用 npm run dev 运行应用,你应该在 localhost:3000/api 中看到 “Hello World!” 消息。

  4. /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 });
    });

    在上面的代码中,当用户成功登录后,我们将经过身份验证的 payload 作为 auth 存储到 HTTP 请求对象的 Express 会话中。然后,当用户注销时,我们将通过删除 auth 会话来清除它。

  5. 像编写单个路由中间件时一样,创建一个包含 state.jsmutations.jsstore,如下所示:

    // store/state.js
    export default () => ({
      auth: null,
    })
    
    // store/mutations.js
    export default {
      setAuth (state, data) {
        state.auth = data
      }
    }
  6. 就像编写单个路由中间件一样,在 storeactions.js 文件中创建 loginlogout action 方法,如下所示:

    // 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) {
          // 处理错误...
        }
      },
      async logout({ commit }) {
        await axios.post('/api/logout')
        commit('setAuth', null)
      }
    }
  7. nuxtServerInit action 添加到 storeindex.js 中,以便在刷新页面时从 HTTP 请求对象中的 Express 会话重新填充 state

    // store/index.js
    export const actions = {
      nuxtServerInit({ commit }, { req }) {
        if (req.session && req.session.auth) {
          commit('setAuth', req.session.auth)
        }
      }
    }
  8. 最后,就像在单个路由中间件身份验证中一样,在 /pages/ 目录中创建一个包含表单的登录页面。使用你之前使用的相同的 loginlogout 方法来 dispatch store 中的 loginlogout action 方法:

    // pages/index.vue
    <template>
      <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>
      <div v-else>
        <p>Logged in as {{ $store.state.auth.username }}</p>
        <button @click="logout">Logout</button>
      </div>
    </template>
    
    <script>
    import axios from 'axios'
    
    export default {
      data () {
        return {
          error: null,
          username: '',
          password: ''
        }
      },
      methods: {
        async login () {
          try {
            await this.$store.dispatch('login', { username: this.username, password: this.password })
            this.error = null
            this.$router.push('/secured')
          } catch (error) {
            this.error = error.response.data.message || '登录失败'
          }
        },
        async logout () {
          await this.$store.dispatch('logout')
          this.$router.push('/')
        }
      }
    }
    </script>
  9. 使用 npm run dev 运行应用。你应该拥有一个与之前工作方式相同的身份验证应用,但它不再从 /server/index.js 运行。

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

使用 serverMiddleware 属性可以使我们的 Nuxt 应用看起来更简洁、感觉更轻便,因为它摆脱了服务器端应用,你不觉得吗?通过这种方法,我们可以使其更灵活,因为我们可以使用任何服务器端框架或应用。例如,我们可以使用我们在下一节中看到的 Koa 来代替 Express

将 Koa 用作 Nuxt 的服务端中间件

就像 KoaExpress 一样,Connect 是一个简单的框架,用于将各种中间件粘合在一起以处理 HTTP 请求。Nuxt 内部使用 Connect 作为服务器,因此大多数 Express 中间件都适用于 Nuxt 的服务器中间件。相比之下,Koa 中间件作为 Nuxt 的服务器中间件工作起来有点困难,因为 reqres 对象被隐藏并保存在 Koactx 中。我们可以使用一个简单的 “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')

请注意,reqNode.js HTTP 请求对象,而 resNode.js HTTP 响应对象。你可以随意命名它们,例如,用 request 代替 req,用 response 代替 res。从上面的比较中,你可以看到 Koa 处理这两个对象的方式与其他框架不同。因此,我们不能像在 Express 中那样将 Koa 用作 Nuxt 的服务器中间件,也不能在 serverMiddleware 属性中定义任何 Koa 中间件,而只能添加保存 Koa API 的目录路径。请放心,让它们在我们的 Nuxt 应用中作为中间件工作并不困难。让我们继续执行以下步骤:

  1. 添加我们想要使用 Koa 创建 API 的路径,如下所示:

    // nuxt.config.js
    export default {
      serverMiddleware: [
        '~/api'
      ]
    }
  2. 导入 koakoa-router,使用 router 创建一个 Hello World! 消息,然后将它们导出到 /api/ 目录中的 index.js 文件:

    // api/index.js
    import Koa from 'koa'
    import Router from 'koa-router'
    
    const router = new Router()
    const app = new Koa()
    
    router.get('/', async (ctx, next) => {
      ctx.type = 'json'
      ctx.body = {
        message: 'Hello World!'
      }
    })
    
    app.use(router.routes())
    app.use(router.allowedMethods())
    
    // 导出服务器中间件
    export default {
      path: '/api',
      handler: app.callback() // 注意这里使用 app.callback()
    }
  3. 导入 koa-bodyparserkoa-session,并将它们注册为 /api/index.js 文件中 Koa 实例的中间件,如下所示:

    // api/index.js
    import Koa from 'koa'
    import Router from 'koa-router'
    import bodyParser from 'koa-bodyparser'
    import session from 'koa-session'
    
    const router = new Router()
    const app = new Koa()
    
    const CONFIG = {
      key: 'koa:sess',
      maxAge: 60000,
    }
    
    app.use(session(CONFIG, app))
    app.use(bodyParser())
    
    router.get('/', async (ctx, next) => {
      ctx.type = 'json'
      ctx.body = {
        message: 'Hello World!'
      }
    })
    
    app.use(router.routes())
    app.use(router.allowedMethods())
    
    // 导出服务器中间件
    export default {
      path: '/api',
      handler: app.callback()
    }
  4. 使用 Koa router 创建登录(login)和注销(logout)路由,如下所示:

    // api/index.js
    import Koa from 'koa'
    import Router from 'koa-router'
    import bodyParser from 'koa-bodyparser'
    import session from 'koa-session'
    
    const router = new Router()
    const app = new Koa()
    
    const CONFIG = {
      key: 'koa:sess',
      maxAge: 60000,
    }
    
    app.use(session(CONFIG, app))
    app.use(bodyParser())
    
    router.get('/', async (ctx, next) => {
      ctx.type = 'json'
      ctx.body = {
        message: 'Hello World!'
      }
    })
    
    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 }
    })
    
    app.use(router.routes())
    app.use(router.allowedMethods())
    
    // 导出服务器中间件
    export default {
      path: '/api',
      handler: app.callback()
    }

    在上面的代码中,就像前面 Express 示例中一样,当用户成功登录后,我们将经过身份验证的 payload 作为 auth 存储到 Koa 上下文对象中的 Koa 会话中。然后,当用户注销时,我们将通过将会话设置为 null 来清除 auth 会话。

  5. 创建一个包含 statemutationsactionsstore,就像你在 Express 示例中所做的那样。此外,在 storeindex.js 文件中创建 nuxtServerInit,就像你在编写单个路由中间件时所做的那样:

    // store/index.js
    export const actions = {
      nuxtServerInit({ commit }, { req }) {
        if (req.ctx.session && req.ctx.session.auth) {
          commit('setAuth', req.ctx.session.auth)
        }
      }
    }
  6. 和以前一样,在 /pages/ 目录中创建表单登录(login)和注销(logout)方法,以 dispatch store 中的 action 方法:

    // pages/index.vue
    <template>
      <form v-if="!$store.state.auth" @submit.prevent="login">
      //...
      </form>
      <div v-else>
        <p>Logged in as {{ $store.state.auth.username }}</p>
        <button @click="logout">Logout</button>
      </div>
    </template>
    
    <script>
    import axios from 'axios'
    
    export default {
      methods: {
        async login () {
          try {
            const { data } = await axios.post('/api/login', { username: this.username, password: this.password })
            this.error = null
            this.$router.push('/secured')
          } catch (error) {
            this.error = error.response.data.message || '登录失败'
          }
        },
        async logout () {
          await this.$store.dispatch('logout')
          this.$router.push('/')
        }
      }
    }
    </script>
  7. 使用 npm run dev 运行应用。你应该拥有一个与前面 Express 示例中工作方式相同的身份验证应用,但它不再从 /server/index.js 运行。

    你可以在我们的 GitHub 仓库的 /chapter-11/nuxt-universal/server-middleware/koa/ 中找到此示例的完整源代码。

根据你的偏好,你可以在你的下一个项目中使用 ExpressKoa 作为 Nuxt 的服务器中间件。在本书中,我们主要使用 Koa,因为它更简洁。你甚至可以创建自定义服务器中间件,而无需使用它们中的任何一个。让我们在下一节看看如何创建自定义服务器中间件。

创建自定义服务端中间件

由于 Nuxt 内部使用 Connect 作为服务器,因此我们可以添加自定义中间件,而无需 KoaExpress 等外部服务器。你可以开发复杂的 Nuxt 服务器中间件,就像我们在前面几节中使用 KoaExpress 所做的那样。但是,让我们不要无休止地重复我们已经做过的事情。让我们创建一个非常基本的自定义中间件,它打印 “Hello World” 消息,以确认从基本中间件构建复杂中间件的可行性,请按照以下步骤操作:

  1. 添加我们想要创建自定义中间件的路径:

    // nuxt.config.js
    serverMiddleware: [{ path: '/api', handler: '~/api/index.js' }]
  2. API 路由添加到 /api/ 目录中的 index.js 文件:

    // api/index.js
    export default function (req, res, next) {
      res.end('Hello world!')
    }
  3. 使用 npm run dev 运行应用并导航到 localhost:3000/api。你应该在屏幕上看到 “Hello World!” 消息。

    你可以参考 Connect 的文档 https://github.com/senchalabs/connect 以获取更多信息。此外,你可以在我们的 GitHub 仓库的 /chapter-11/nuxt-universal/server-middleware/custom/ 中找到此示例的源代码。

做得好!你已经成功完成了另一个关于 Nuxt 的重要章节。在继续下一章之前,让我们总结一下你到目前为止学到的内容。