路由与中间件: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 才是请求体。
中间件执行流程:洋葱模型
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() 本身也是一个中间件。所以如果你把路由中间件放在其他中间件之前或之后,效果是不一样的。
| 注册顺序 | 执行顺序 |
|---|---|
| 日志中间件 → 路由中间件 | 先记录日志,再匹配路由 |
| 路由中间件 → 错误处理中间件 | 路由先执行,错误处理兜底 |
| 多个路由中间件 | 按注册顺序依次匹配 |
说白了,中间件的顺序决定了请求的处理流程。我一般会这样安排:
- 错误处理中间件(最外层)
- 日志记录中间件
- bodyParser 中间件
- 路由中间件
- 静态文件中间件(如果有)
这个顺序不是绝对的,但大部分项目都适用。
避坑指南
- 忘记在中间件里调用
next(),导致请求卡住 - 在
next()之后修改了ctx.body,结果被后面的中间件覆盖了 - 路由中间件注册在 bodyParser 之前,导致 POST 参数拿不到
- 多个路由匹配了同一个路径,只有第一个生效
这些坑其实都不难避免。你只要记住一点:中间件的执行顺序就是代码的书写顺序。想清楚请求进来之后要经过哪些步骤,再动手写代码,基本不会出问题。
好了,这一章的内容就到这里。路由配置、参数获取、中间件流程,这三个东西搞明白了,Koa2 的基础就算打牢了。下一章我们聊聊更进阶的内容。