Node.js Koa 框架完全指南

小飞兽 Node.js 387 次阅读 2026-05-26

Koa 框架简介

Koa 是 Express 原班人马打造的下一代 Web 框架,核心思想是"洋葱模型"中间件。与 Express 不同,Koa 不绑定任何中间件,由开发者自由组合。Koa 2.x 是目前稳定版本,全面使用 async/await 语法。

洋葱模型原理

洋葱模型是 Koa 最核心的概念:中间件像洋葱一样层层包裹,请求从外层进入,穿过每一层中间件到达核心业务,然后原路返回,每层中间件可以在"外层"和"内层"分别做处理。

请求 → middleware1 (外层) → middleware2 (外层) → 核心逻辑 → middleware2 (内层) → middleware1 (内层) → 响应

基础项目结构

npm init -y
npm install koa koa-router koa-bodyparser koa-static koa-json
npm install --save-dev nodemon
// app.js
const Koa = require('koa');
const Router = require('@koa/router');
const bodyParser = require('koa-bodyparser');
const static = require('koa-static');
const json = require('koa-json');

const app = new Koa();
const router = new Router({ prefix: '/api/v1' });

// ===== 中间件 =====

// 记录请求耗时
app.use(async (ctx, next) => {
  const start = Date.now();
  await next();  // 等待下游中间件执行
  const ms = Date.now() - start;
  console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
});

// JSON 美化输出(仅开发环境)
if (process.env.NODE_ENV !== 'production') {
  app.use(json());
}

// 解析请求体
app.use(bodyParser({
  enableTypes: ['json', 'form'],
  jsonLimit: '1mb'
}));

// 静态文件服务
app.use(static('public'));

// 错误处理
app.use(async (ctx, next) => {
  try {
    await next();
  } catch (err) {
    console.error('Koa 错误:', err.message);
    ctx.status = err.status || 500;
    ctx.body = {
      error: process.env.NODE_ENV === 'production'
        ? 'Internal Server Error'
        : err.message
    };
  }
});

// ===== 路由 =====

router.get('/health', (ctx) => {
  ctx.body = { status: 'ok', timestamp: new Date().toISOString() };
});

router.get('/posts', async (ctx) => {
  const { page = 1, limit = 10 } = ctx.query;
  ctx.body = {
    data: [],
    meta: { page: Number(page), limit: Number(limit) }
  };
});

router.post('/posts', async (ctx) => {
  const { title, content } = ctx.request.body;
  if (!title) {
    ctx.throw(400, '标题不能为空');
  }
  ctx.status = 201;
  ctx.body = { id: Date.now(), title, content };
});

// 注册路由
app.use(router.routes());
app.use(router.allowedMethods());  // 自动处理 OPTIONS 请求,返回 Allowed Methods

// 启动
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Koa 服务运行在 http://localhost:${PORT}`);
});

洋葱模型详解

// onion-model.js - 演示洋葱模型
const Koa = require('koa');
const app = new Koa();

// 模拟认证中间件
const auth = async (ctx, next) => {
  console.log('[1] 认证 - 外层 before next');
  await next();
  console.log('[1] 认证 - 内层 after next(响应前最后处理)');
};

// 模拟日志中间件
const logger = async (ctx, next) => {
  console.log('[2] 日志 - 外层 before next');
  await next();
  console.log('[2] 日志 - 内层 after next');
};

// 模拟数据处理
const processData = async (ctx, next) => {
  console.log('[3] 业务处理 - 外层 before next');
  ctx.state.data = { message: 'Hello Koa!' };
  await next();
  console.log('[3] 业务处理 - 内层(永远不会执行,因为 next 后没有代码)');
};

// 实际路由
async function handleRequest(ctx, next) {
  console.log('[4] 路由处理器 - 外层 before next');
  ctx.body = ctx.state.data;
  await next();
  console.log('[4] 路由处理器 - 内层(永远不会执行)');
}

// 注册顺序影响执行顺序(先注册先执行)
app.use(auth);
app.use(logger);
app.use(processData);
app.use(handleRequest);

// 输出顺序:
// [1] 认证 - 外层 before next
// [2] 日志 - 外层 before next
// [3] 业务处理 - 外层 before next
// [4] 路由处理器 - 外层 before next
// [3] 业务处理 - 内层(不会再执行)
// [2] 日志 - 内层 after next
// [1] 认证 - 内层 after next(响应前最后处理)

Koa Context 对象

// ctx-object.js
// Koa 的 Context(ctx)封装了 Node 的 req/res,并提供了很多便捷属性和方法

router.get('/ctx-demo', (ctx) => {
  // 请求信息
  const { method, url, path, query, querystring } = ctx.request;
  const headers = ctx.request.headers;
  const ip = ctx.ip;
  const fresh = ctx.fresh;        // 缓存新鲜度(If-None-Match/If-Modified-Since)
  const accepts = ctx.accepts();  // 内容协商(Accept)

  // 请求体(需配合 bodyParser)
  const body = ctx.request.body;

  // 动态设置(通过 ctx.state 在中间件间传递数据)
  ctx.state.user = { id: 1, name: 'Alice' };

  // 响应快捷方法
  ctx.body = { message: 'ok' };           // 设置响应体
  ctx.status = 201;                         // 设置状态码
  ctx.set('X-Request-Id', '12345');        // 设置响应头
  ctx.redirect('/login');                   // 重定向
  ctx.throw(403, 'Forbidden');             // 抛出错误(被错误处理中间件捕获)

  // JSON 响应(自动设置 Content-Type: application/json)
  ctx.json({ data: [] });

  // 响应 HTML
  ctx.html('<h1>Hello</h1>');

  // 文件下载(需要 koa-send)
  // await ctx.sendfile('./files/report.pdf');
});

常见问题

Q1: Koa 的 <code>ctx.throw()</code> 和 <code>ctx.assert()</code> 有什么区别?

ctx.throw(status, message) 抛出 HTTP 错误,会被错误处理中间件捕获,返回对应的错误响应。ctx.assert(value, status, message) 是断言方法,当 value 为 falsy 时抛出错误,常用于参数验证(如 ctx.assert(user, 401, 'Unauthorized'))。

Q2: Koa 没有内置路由,如何选择路由中间件?

Koa 官方推荐的路由方案是 @koa/router(原 koa-router),它同时支持 routes()allowedMethods()allowedMethods() 会自动对没有对应路由方法但有匹配路径的 OPTIONS 请求返回 200,并填充 Allow 头;也会对未实现的请求方法返回 405。

Q3: 什么时候应该用 <code>ctx.state</code> 而不是 <code>ctx.body</code>?

ctx.state 用于在中间件链中传递数据(如认证后的用户信息),它是请求级别的共享对象,不会直接作为响应体。ctx.body 是响应体数据,设置后会作为 HTTP 响应返回给客户端。两者作用不同,不能互换。

延伸阅读

  • <a href="https://koa.bootcss.com/">Koa 官方文档</a>
  • <a href="https://github.com/koajs/koa">Koa 进阶:从中间件到框架</a>
  • <a href="https://github.com/ZijianHe/koa-router">@koa/router 官方文档</a>