# koa2简析结构
30 秒速记
Application:app.use把中间件推进数组,app.listen实际是http.createServer(app.callback())- 每个请求进来,
createContext新建一个ctx,挂上request/response和原生req/res koa-compose把中间件数组串成一个函数,dispatch(i)调第i个,next就是dispatch(i + 1)- 整条链跑完后
respond(ctx)根据ctx.body的类型统一写响应;出错走ctx.onerror - 同一个中间件调两次
next()会被拦下,报next() called multiple times
Koa 的源码结构可以浓缩成一句话:app 维护一个中间件数组,每个请求新建一个 ctx,用 compose 把中间件串成洋葱,跑完再统一写响应。 app.listen 本质就是 http.createServer(app.callback()).listen(),callback 里先 compose 好中间件,每来一个请求就 createContext 造一个 ctx,然后执行这条链。中间件里只是给 ctx.body 赋值,真正 res.end 是链条结束后 respond 函数干的,所以外层中间件在 await next() 之后还能改响应内容。抛出的错误沿 Promise 链冒到最外面,没人接就交给 ctx.onerror 返回 500。
理解「koa2简析结构」时,要先明确输入、运行阶段和可观察输出。这里的交互实验会分别执行正常路径与失败路径,并把 Node.js 的关键状态变化和同屏代码对应起来;不能只记命令名称。
工程中还要验证退出码、资源清理和异常输入。属于 middleware 场景的配置应保持最小可复现,并在开发环境与 CI 中使用相同命令复跑,避免把平台差异或隐式默认值带到线上。
Koa 的核心源码只有几百行,抓住三个函数就能看懂整个流程。
第一个是 use,它就是往数组里推一个函数:
use(fn) {
this.middleware.push(fn)
return this
}
第二个是 listen 和 callback。listen 只是 http.createServer(this.callback()).listen(...args) 的简写;callback 先用 koa-compose 把中间件数组合成一个函数,再返回 Node 需要的 (req, res) 处理器:
callback() {
const fn = compose(this.middleware)
return (req, res) => {
const ctx = this.createContext(req, res) // 每个请求一个新 ctx
return this.handleRequest(ctx, fn)
}
}
第三个是 handleRequest,执行中间件链,成功了统一响应,失败了统一处理错误:
handleRequest(ctx, fnMiddleware) {
ctx.res.statusCode = 404 // 默认 404,有人设了 body 才会改
const onerror = err => ctx.onerror(err)
return fnMiddleware(ctx).then(() => respond(ctx)).catch(onerror)
}
respond 根据 ctx.body 的类型决定怎么写:字符串和 Buffer 直接 res.end,流就 pipe 过去,普通对象 JSON.stringify 后发送。这也是为什么在中间件里只需要给 ctx.body 赋值,不用自己 res.end。
compose 是洋葱模型的来源:dispatch(i) 执行第 i 个中间件,并把 () => dispatch(i + 1) 作为 next 传进去,所以 await next() 就是「等里面所有中间件跑完」,返回后再执行自己剩下的代码。
面试官追问
追问 1为什么 Koa 里外层中间件在 await next() 之后改 ctx.body 还能生效?
因为中间件只是改 ctx 上的值,并没有真正发响应。整条链的 Promise 完成之后 respond(ctx) 才读 ctx.body 写到 res,所以最后一次赋值算数。
追问 2ctx.request 和 ctx.req 有什么区别?
ctx.req 是 Node 原生的 IncomingMessage,ctx.request 是 Koa 包的一层,提供 query、path、accepts() 这些便捷属性和方法。日常用 ctx.request,直接读原生流(比如自己解析上传)才碰 ctx.req。
追问 3中间件里在成功分支和 finally 里各调了一次 next(),会怎样?
第二次调用 compose 会直接 reject,报 next() called multiple times。它靠记录上一次执行到的下标来判断,同一个位置再调就拦下。
追问 4测试时怎么不起端口就测 Koa 应用?
app.callback() 返回的就是 (req, res) => {} 处理函数,supertest(app.callback()) 直接发请求,不占端口。
追问 5为什么每个请求都要新建一个 ctx,不能复用吗?
ctx 上挂着这次请求的 body、state、status,并发请求共用一个就会互相串数据。每次 Object.create 一个新对象,原型指向 app.context,所以 app.context 上加的属性每个 ctx 都能读到。
# koa2简析结构
# 源码文件
30 秒速记
- 核心就
lib/下四个文件:application.js/context.js/request.js/response.js application.js= 入口:use收中间件、listen起服务、callback组装compose并创建ctxcontext.js=ctx的原型:onerror、throw,以及用delegates把request/response的属性代理过来request.js/response.js= 对原生req/res的封装,ctx.query、ctx.body的真正实现在这- 读源码路线:从
application.js的listen→callback→handleRequest顺着往下看
Koa2 的源码核心只有 lib 目录下四个文件,application.js 是入口,另外三个分别管上下文、请求和响应。 application.js 负责收集中间件、创建 ctx、执行中间件链再统一响应;request.js 和 response.js 包了原生 req / res,比如 query 怎么解析、body 赋值后 Content-Type 怎么推断都在这里。context.js 是 ctx 的原型,它本身没多少逻辑,主要是把 request 和 response 上的常用属性代理出来,所以你能直接写 ctx.path、ctx.body。看源码我会从 application.js 的 listen 开始顺着调用往下读,比四个文件挨个看好懂得多。
├── lib
│ ├── application.js
│ ├── context.js
│ ├── request.js
│ └── response.js
└── package.json
这个就是 GitHub https://github.com/koajs/koa上开源的koa2源码的源文件结构,核心代码就是lib目录下的四个文件
- application.js 是整个koa2 的入口文件,封装了context,request,response,以及最核心的中间件处理流程。
- context.js 处理应用上下文,里面直接封装部分request.js和response.js的方法
- request.js 处理http请求
- response.js 处理http响应
面试官追问
追问 1ctx.body = 'hi' 这行赋值,真正处理逻辑在哪个文件?
context.js 只是把 body 代理到 response,真正的 set body() 在 response.js 里,它会顺手把状态码设成 200、根据内容推断 Content-Type。
追问 2ctx.path 为什么能直接用,ctx 上明明没定义这个属性?
context.js 用 delegates 库把 request.path 代理到了 ctx 上,读 ctx.path 实际读的是 ctx.request.path。哪些属性被代理了,看 context.js 底部那一串 .access() / .getter() 就清楚。
追问 3想给所有 ctx 加一个 ctx.success(data) 方法,改哪?
不用改源码,挂到 app.context 上就行:
