将 Koa 与 Nuxt 集成

KoaNuxt 的集成可采用两种方式:单端口部署(适用于单域名应用)多端口部署(适用于跨域应用)。本章将重点介绍单域名集成方案,跨域集成方案将在第 12 章《实现用户登录与 API 认证》中详细讲解。我们将基于前文开发的 Koa 骨架工程实现这两种集成方式。单域名集成需要完成以下配置步骤,现在开始操作:

  1. Nuxt 项目根目录创建 /server/ 服务端目录,使用 create-nuxt-app 脚手架工具生成项目后,按以下结构组织目录:

    ├── package.json
    ├── nuxt.config.js
    ├── server
    │   ├── config       # 配置文件目录
    │   │   └── ...      # 配置文件
    │   ├── public       # 公共资源目录
    │   │   └── ...      # 公共资源文件
    │   ├── static       # 静态文件目录
    │   │   └── ...      # 静态资源
    │   └── index.js     # 服务入口文件
    └── pages            # Nuxt页面目录
        └── ...          # 页面组件
  2. 修改脚手架工具生成的默认 package.json 文件中的脚本配置,集成 Backpack 构建工具:

    package.json
    "scripts": {
      "dev": "backpack",  // 开发模式启动
      "build": "nuxt build && backpack build",  // 构建命令
      "start": "cross-env NODE_ENV=production node build/main.js",  // 生产启动
      "generate": "nuxt generate"  // 静态生成
    }
  3. 在项目根目录(与 nuxt.config.js 同级)创建 Backpack 配置文件,将默认入口目录修改为刚刚创建的 /server/ 目录:

    // backpack.config.js
    module.exports = {
      webpack: (config, options, webpack) => {
        config.entry.main = './server/index.js'  // 重定向入口路径
        return config
      }
    }
  4. /server/ 目录下创建 index.js 入口文件(确保已安装 Koa),将 Koa 作为主应用、Nuxt 作为中间件集成:

    // server/index.js
    import Koa from 'koa'
    import consola from 'consola'  // 控制台日志工具
    import { Nuxt, Builder } from 'nuxt'
    
    const app = new Koa()
    const nuxt = new Nuxt(config)
    
    async function start() {
      // 将Nuxt挂载为Koa中间件
      app.use((ctx) => {
        ctx.status = 200          // 设置HTTP状态码
        ctx.respond = false       // 禁用Koa默认响应
        ctx.req.ctx = ctx         // 注入上下文对象
        nuxt.render(ctx.req, ctx.res)  // Nuxt渲染管线
      })
    }
    start()

    请注意,我们创建了一个 async 函数来将 Nuxt 用作中间件,以便我们可以在下一步中使用 await 语句来运行 Nuxt 构建过程。

    要注意,Consola 是一个控制台日志记录器,你必须在使用它之前通过 npm 安装它。有关此软件包的更多信息,请访问 https://github.com/nuxt-contrib/consola

  5. 加载 Nuxt 配置并处理构建流程 在注册 Nuxt 中间件前,导入 Nuxt 配置文件并根据环境处理构建逻辑:

    // server/index.js
    let config = require('../nuxt.config.js')
    config.dev = !(app.env === 'production')  // 根据环境设置开发模式
    
    if (config.dev) {
      const builder = new Builder(nuxt)
      await builder.build()  // 开发模式执行构建
    } else {
        // TODO 到底是 nuxt.render() 还是 nuxt.ready()
      await nuxt.ready()     // 生产模式直接启动
    }
  6. 启动服务并监听端口 通过 Consola 输出服务状态信息:

    app.listen(port, host)  // 监听指定端口和主机
    consola.ready({
      message: `服务已启动: http://${host}:${port}`,  // 带格式化的日志输出
      badge: true  // 启用徽标样式
    })
  7. 开发模式启动命令

    $ npm run dev # 启动开发服务器

至此,NuxtKoa 已成功整合为单一应用。您应当注意到,Nuxt 现在作为 Koa 的中间件运行。所有 Nuxt 页面仍可通过 localhost:3000 正常访问(保持原有功能不变),我们将在下一节配置 localhost:3000/api 作为 API 主端点。

添加路由和其他必要的中间件

在前一节完成基础集成和目录结构搭建后,我们现在通过以下步骤完善 API 路由和其他中间件配置:

  1. 安装 Koa 路由和静态资源中间件

    $ npm i koa-router koa-static
  2. 创建服务端配置文件

    // server/config/index.js
    export default {
      static_dir: {
        root: '../static'  // 静态资源目录配置
      }
    }
  3. /server/ 目录下创建 routes.js 文件,用于定义对外开放的路由并配置模拟用户数据:

    // server/routes.js
    import Router from 'koa-router'
    const router = new Router({ prefix: '/api' })  // 统一添加/api前缀
    
    const users = [
      { id: 1, name: 'Alexandre' },
      { id: 2, name: 'Pooya' },
      { id: 3, name: 'Sébastien' }
    ]
    
    // 基础测试接口
    router.get('/', async (ctx) => {
      ctx.type = 'json'
      ctx.body = { message: 'Hello World!' }
    })
    
    // 用户列表接口
    router.get('/users', async (ctx) => {
      ctx.type = 'json'
      ctx.body = users
    })
    
    // 用户详情接口
    router.get('/users/:id', async (ctx) => {
      const id = parseInt(ctx.params.id)
      const found = users.find(user => user.id == id)
      found ? ctx.body = found : ctx.throw(404, 'user not found')
    })
  4. 在独立的 middlewares.js 文件中导入其他中间件,并引入第 1、2 步创建的路由和配置文件:

    // server/middlewares.js
    import serve from 'koa-static'
    import bodyParser from 'koa-bodyparser'
    import config from './config'
    import routes from './routes'
    
    export default (app) => {
    
      // 基础中间件注册
      app.use(serve(config.static_dir.root))
      app.use(bodyParser())
      app.use(routes.routes(), routes.allowedMethods())
    }

    我们将不在 API 中使用 koa-favicon 中间件,原因如下:

    • 当前 API 仅输出 JSON 格式数据,浏览器标签页不会显示 favicon.ico 图标

    • Nuxt 已在 nuxt.config.js 配置文件中内置了 favicon 处理功能

    • 因此可以从骨架工程中移除 koa-favicon 中间件,转而创建标准化 JSON 响应中间件,其输出格式规范如下:

      • 成功响应(状态码 200):

        {
          "status": <status code>,
          "data": <data>
        }
      • 错误响应(如 400/500 等):

        {
          "status": <status code>,
          "message": <error message>
        }
  5. app.use(serve(config.static_dir.root)) 代码行之前添加以下代码,以实现上述标准化 JSON 响应格式:

    // 标准化JSON响应格式
    app.use(async (ctx, next) => {
      try {
        await next();
        if (ctx.status === 404) {
          ctx.throw(404);
        }
        if (ctx.status === 200) {
          ctx.body = { status: 200, data: ctx.body };
        }
      } catch (err) {
        ctx.status = err.status || 500;
        ctx.body = {
          status: ctx.status,
          message: err.message,
        };
        ctx.app.emit('error', err, ctx);
      }
    });

    现在,通过该中间件处理后,原本的输出格式 {"message":"Hello World!"} 将被标准化为:

    {
      "status": 200,
      "data": {
        "message": "Hello World!"
      }
    }
  6. 在注册 Nuxt 中间件之前,于主 index.js 文件中导入 middlewares.js 模块:

    // server/index.js
    import middlewares from './middlewares'
    
    middlewares(app)  // 加载所有中间件
    app.use(ctx => {
        ...
        nuxt.render(ctx.req, ctx.res)
    })
  7. 开发模式运行验证

    $ npm run dev # 启动开发服务器
  8. 好的,如果你在浏览器中访问 localhost:3000/api,你将会在屏幕上看到以下输出:

    {"status":200,"data":{"message":"Hello World!"}}

    如果你访问用户索引页面 localhost:3000/api/users,你将会在屏幕上看到以下输出:

    {"status":200,"data":[{"id":1,"name":"Alexandre"},{"id":2,"name":"Pooya"},{"id":3,"name":"Sébastien"}]}

    你也可以使用 localhost:3000/api/users/<id> 来获取特定的用户。例如,如果你使用 /api/users/1,你将会在屏幕上看到以下输出:

    {"status":200,"data":{"id":1,"name":"Alexandre"}}

你可以在我们的 GitHub 仓库中的 /chapter-8/nuxt-universal/skeletons/koa/ 找到这个集成的示例应用。

好的,在接下来的部分,我们将探讨如何在客户端通过 Nuxt 页面的 asyncData 方法来请求前面提到的 API 数据。