别以为 Koa 只是"更精简的 Express"——这是选型会上最常见的误判,也是本节要拆掉的第一块砖。上一节的历史告诉我们两者同源,这一节把它们放上同一张评审桌:内核怎么不同、生态怎么分工、错误模型差在哪,最后给出一张能带进会议室的选型判断表。
Express 与 Koa 解决的是同一个问题——把 HTTP 请求组织成可维护的处理流程——但在"框架替你做多少"这个坐标上站在两端。Express 选择把常用能力内置:路由、静态文件、模板集成、错误中间件约定,装完就能开工。Koa 选择只内置控制流:内核只保留 Application、Context、中间件队列三样东西,路由、解析、静态服务统统外置成中间件,由你按需组装。
看同一个接口在两边长什么样:
// Express:内置路由,装完即用 const express = require('express'); const app = express(); app.get('/hello', (req, res) => { res.json({ msg: 'hi', name: req.query.name }); }); app.listen(3000);
// Koa:内核只给控制流,路由要装 koa-router const Koa = require('koa'); const Router = require('koa-router'); const app = new Koa(); const router = new Router(); router.get('/hello', (ctx) => { ctx.body = { msg: 'hi', name: ctx.query.name }; }); app.use(router.routes()).use(router.allowedMethods()); app.listen(3000);
Koa 版本多两行 require 与挂载,换来的是:路由器本身就是一个中间件层,你可以在这层之前塞鉴权、之后塞日志,层次完全由你排布。Express 不是做不到,但它的路由是"框架的一部分",插入点靠约定而非结构。
内核与请求模型。 Express 的处理函数拿 (req, res) 两个原生对象,直接读写;Koa 把原生对象包成 ctx,请求与响应统一从一个上下文进出,且 ctx 上的属性大量做了委托代理(细节在第 2.3 节展开)。后果是:Express 代码有时直接操作 res 的底层状态,Koa 代码则倾向于"给 ctx.body 赋值、让内核在出层时统一发送",响应时机更可预测。
错误模型。 Express 的错误中间件靠四参数签名识别,且只能捕获同步异常与手动 next(err);回调里的异常必须显式转交。Koa 的 async 中间件里 throw 的一切都会沿 Promise 链向上冒泡,最外层一个 try/catch 全接住。这不是风格差异,是"错误有没有机制性出口"的差异。
生态与团队惯性。 Express 生态更老、资料更多、招人更容易;Koa 生态质量高但需要自己选型组装。国内很多企业级框架(如 Egg)以 Koa 为内核,等于社区替你做了一轮中间件精选——如果你的团队用这类上层框架,选型讨论其实已经完成了一半。
| 维度 | Express | Koa |
|---|---|---|
| 内核定位 | 微内核加常用功能内置 | 极简内核,一切能力外置 |
| 中间件签名 | (req, res, next),同步为主 |
async (ctx, next),原生异步 |
| 控制流 | 单向流动,一去不回 | 洋葱模型,进层出层对称 |
| 错误出口 | 四参数错误中间件,同步异常为主 | Promise 冒泡,最外层统一捕获 |
| 响应时机 | 各层可直接写 res,时机分散 | 出层统一发送,可预测 |
| 路由 | 内置 | 外置 koa-router,本身是中间件层 |
| 学习资料与招人 | 存量大,上手快 | 相对少,但内核源码极短可通读 |
表格只能给方向,判断要落到具体场景。下面是我们在技术评审里实际使用的判断顺序:
我个人的倾向写在前面供参考:新起的 Node.js 后端服务,我更愿意从 Koa 起步。理由不是性能跑分,而是它逼着团队从第一天就想清楚"每一层做什么、顺序为什么这样排"——这个思考过程对任何框架都有益,只是 Koa 把它变成了必答题。
某内容平台把一个 Express 服务迁到 Koa,接口约莫两百个,两人全职做了三周。拆开账本看:真正的 API 改写只占三分之一时间;剩下的花在三类隐性成本上——中间件顺序重排引发的行为差异(bodyparser 的位置变了导致两处读取 body 为空)、错误处理从散落式改为统一捕获后监控告警规则的适配、以及团队从回调思维切换到洋葱思维的磨合。这份账的结论值得记住:迁移成本与代码量无关,与"隐式约定"的数量成正比。
常见坑:把"Express 中间件写法"直接搬进 Koa。Express 里
res.send()一调用响应就发出去了;Koa 里必须给ctx.body赋值、等出层统一发送。带着旧肌肉记忆写 Koa,最容易出现"设置了 body 却拿到 404"这类怪现象——因为函数没等发送时机就结束了。
下一节动手:从空目录搭出最小可跑的 Koa 应用,把本节纸面上的"洋葱"变成命令行里能看到的一次真实请求穿透。