路由与中间件:Koa-router 路由配置、GET/POST 参数获取、中间件执行流程与 next() 机制

这一章我们聊聊 Koa2 里最核心的两个概念——路由和中间件。说实话,我刚接触 Koa 的时候,觉得它比 Express 清爽太多了。但清爽归清爽,该有的东西一个都不能少。路由怎么配?参数怎么拿?中间件到底怎么跑?这些搞不清楚,后面写接口就是一团乱麻。

Koa-router:让路由变得简单

Koa 本身是没有路由功能的。它只提供了一个非常轻量的内核。所以我们需要借助 koa-router 这个中间件来搞定路由。我个人习惯用 @koa/router,这是官方维护的版本,更稳定一些。

先装包:

npm install @koa/router

然后看一个最基础的路由配置:

const Koa = require('koa');
const Router = require('@koa/router');

const app = new Koa();
const router = new Router();

router.get('/', (ctx) => {
  ctx.body = '首页';
});

router.get('/user', (ctx) => {
  ctx.body = '用户页面';
});

app.use(router.routes());
app.use(router.allowedMethods());

app.listen(3000);

这里有个细节——router.allowedMethods()。它的作用是自动处理不支持的请求方法。比如你只配了 GET,别人发了个 POST,它会返回 405。我在项目中遇到过忘记加这行,结果前端报错半天查不出原因。嗯,后来就长记性了。

GET 参数获取:query 和 params

GET 请求的参数通常有两种来源:URL 查询字符串和路径参数。

查询字符串

// 请求:/search?keyword=koa&page=1
router.get('/search', (ctx) => {
  const { keyword, page } = ctx.query;
  ctx.body = `搜索:${keyword},第${page}页`;
});

ctx.query 返回的是一个对象,Koa 已经帮你解析好了。你想想看,这比 Node 原生去解析 querystring 方便了多少?

路径参数

// 请求:/user/123
router.get('/user/:id', (ctx) => {
  const { id } = ctx.params;
  ctx.body = `用户ID:${id}`;
});

ctx.params 就是路由中冒号匹配到的部分。这里要注意,路径参数的名字要和路由定义保持一致。我曾经见过有人写 :userId 却用 ctx.params.id 去拿,结果拿了个 undefined 回来。

POST 参数获取:body 解析

POST 请求的参数不会自动解析。Koa 不像 Express 那样内置了 body-parser。你需要自己加一个中间件来处理。

我推荐用 koa-bodyparser

npm install koa-bodyparser

使用方式:

const bodyParser = require('koa-bodyparser');
app.use(bodyParser());

router.post('/login', (ctx) => {
  const { username, password } = ctx.request.body;
  ctx.body = `登录:${username}`;
});

注意这里用的是 ctx.request.body,不是 ctx.body。很多人一开始会搞混。说白了,ctx.body 是响应体,ctx.request.body 才是请求体。

注意:bodyParser 必须在路由之前注册。否则请求体还没解析,路由里就拿不到数据。

中间件执行流程:洋葱模型

Koa 的中间件执行机制非常有意思。它像洋葱一样,一层一层往里进,再一层一层往外出。这就是著名的「洋葱模型」。

看个例子你就明白了:

app.use(async (ctx, next) => {
  console.log('中间件 1 开始');
  await next();
  console.log('中间件 1 结束');
});

app.use(async (ctx, next) => {
  console.log('中间件 2 开始');
  await next();
  console.log('中间件 2 结束');
});

app.use(async (ctx) => {
  console.log('中间件 3 执行');
  ctx.body = 'Hello Koa';
});

执行结果:

中间件 1 开始
中间件 2 开始
中间件 3 执行
中间件 2 结束
中间件 1 结束

为什么会这样?因为 await next() 会暂停当前中间件,把控制权交给下一个中间件。等后面的中间件全部执行完,再回来继续执行当前中间件剩下的代码。

我在项目中用这个特性做过很多事情。比如在最外层加一个计时中间件:

app.use(async (ctx, next) => {
  const start = Date.now();
  await next();
  const ms = Date.now() - start;
  console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
});

这样每个请求的耗时就一目了然了。

next() 机制:控制权传递

next() 是中间件里最重要的函数。它负责把控制权交给下一个中间件。如果你不调用 next(),后面的中间件就不会执行。

举个例子:

app.use(async (ctx, next) => {
  console.log('验证中间件');
  // 这里没有调用 next()
  ctx.body = '未通过验证';
});

app.use(async (ctx) => {
  console.log('这个永远不会执行');
  ctx.body = 'Hello';
});

第二个中间件永远不会被执行。这在做权限验证时非常有用。如果验证不通过,直接返回错误,不再往下走。

小技巧:如果你在中间件里调用了 next(),但后面还有代码,记得用 await next()。否则后面的代码会在 next 返回之前就执行了,顺序会乱。

路由级别的中间件

除了全局中间件,你还可以给某个路由单独加中间件。这在做接口权限控制时特别实用。

const authMiddleware = async (ctx, next) => {
  const token = ctx.headers['authorization'];
  if (!token) {
    ctx.status = 401;
    ctx.body = '未授权';
    return;
  }
  await next();
};

router.get('/admin', authMiddleware, (ctx) => {
  ctx.body = '管理员页面';
});

这里 authMiddleware 只对 /admin 这个路由生效。其他路由不受影响。我曾经在一个项目中,把所有的权限校验都写成了路由中间件,代码清晰了很多。

中间件的执行顺序

中间件的执行顺序取决于注册顺序。先注册的先执行。但要注意,router.routes() 本身也是一个中间件。所以如果你把路由中间件放在其他中间件之前或之后,效果是不一样的。

注册顺序 执行顺序
日志中间件 → 路由中间件 先记录日志,再匹配路由
路由中间件 → 错误处理中间件 路由先执行,错误处理兜底
多个路由中间件 按注册顺序依次匹配

说白了,中间件的顺序决定了请求的处理流程。我一般会这样安排:

  1. 错误处理中间件(最外层)
  2. 日志记录中间件
  3. bodyParser 中间件
  4. 路由中间件
  5. 静态文件中间件(如果有)

这个顺序不是绝对的,但大部分项目都适用。

避坑指南

我曾经踩过的坑:
  • 忘记在中间件里调用 next(),导致请求卡住
  • next() 之后修改了 ctx.body,结果被后面的中间件覆盖了
  • 路由中间件注册在 bodyParser 之前,导致 POST 参数拿不到
  • 多个路由匹配了同一个路径,只有第一个生效

这些坑其实都不难避免。你只要记住一点:中间件的执行顺序就是代码的书写顺序。想清楚请求进来之后要经过哪些步骤,再动手写代码,基本不会出问题。

好了,这一章的内容就到这里。路由配置、参数获取、中间件流程,这三个东西搞明白了,Koa2 的基础就算打牢了。下一章我们聊聊更进阶的内容。