Koa 源码解析
一个简单的 koa 程序
const Koa = require('koa');
const app = new Koa();
// response
app.use(ctx => {
ctx.body = 'Hello Koa';
});
app.listen(3000);
源码解析- koa 源码目录
-
application.js
:koa
的导出文件夹,也就是定义 koa 的地方 -
context.js
: 上下文,也就是常见的ctx
。每一个app
都会有一份context
,通过继承这个context.js
中定义的context
。同时每一个请求中,都会有一份单独的ctx
, 通过继承app
中定义的 context。说白了就是复用。 -
request.js
: 同前面的context.js
说明。 -
response.js
: 同前面的context.js
说明。
说明:在 koa
中 nodejs
原生对应的是 req
和 res
源码解析- application.js
包的功能可以参考截图中给出的注释
Application
这个就是 koa
的定义的类
第一步 - new koa()
New 会执行构造函数。
所以在实例化的时候,可以传入这些 options
。
第二步 - app.use
在这里会检查 middleware
的类型,如果是老的 middleware
会转换一下,最后直接放到 middleware
这个数组中。数组中的中间件,会在每一个请求中去挨个执行一遍。
第三步 - app.listen
listen
的时候,才会去创建 server
。
对于每一个请求,都会走到 callback
中去,所以 callback
是用于处理实际请求的。一般不要去重写这个 callback
。
接下来去看看 callback
做了什么:
这里涉及到几个大的点:
-
createContext
都干了什么 -
Compose
是如何实现洋葱模型的。 -
this.handleRequest(ctx, fn)
干了什么
这几个点分成两个大块来讲,2、3 两点放到一起讲。
createContext 干了什么
这里做了三件重要的事情
- 每一个
app
都有其对应的context、request、response
实例,每一个请求,都会基于这些实例去创建自己的实例。在这里就是创建了context、request、response
- 将
node
原生的res、req
以及this
挂载到context、request、response
上面。还有一些其他为了方便访问做得一些挂载,不过前面三个的挂载是必须的。 - 将创建的
context
返回,传给所有中间件的第一个ctx
参数,作为这个请求的上下文
下面着重解释一下第二点中,为什么要把这些属性挂载上去。因为所有的访问都是代理,最终都是访问的 req、res
上面的东西,context
访问的是 request、response
上面的东西,但是他们上面的东西又是访问的是 req、res
上面的。
例如访问 ctx.method
,context
会去 request
上面早,然后 request
会返回 req.method
。后面分析其他文件时会讲到这种代理结构。
compose 如何实现的洋葱模型
在第三步中最后讲到的 callback
中,middleware
全部通过 koa-compose
这个包包装,返回了一个可执行的方法,在请求阶段会去执行这个方法,从而执行每一个中间件。先自己来手撸一个 compose
的🌰
function compose(middleware) {
return function (ctx, next) {
function dispatch(i) {
if (i >= middleware.length) {
return Promise.resolve()
}
let curMiddleware = middleware[i]
return curMiddleware(ctx, dispatch.bind(null, i + 1))
}
return dispatch(0)
}
}
function mid1(ctx, next) {
console.log('mid1 before')
next()
console.log('mid1 after')
}
function mid2(ctx, next) {
console.log('mid2 before')
next()
console.log('mid2 after')
}
function mid3(ctx, next) {
console.log('mid3 before')
next()
console.log('mid3 after')
}
const fn = compose([mid1, mid2, mid3])
fn({})
--------------------------------------------------------------------打印结果
mid1 before
mid2 before
mid3 before
mid3 after
mid2 after
mid1 after
在 compose
中会根据 i
去挨个执行中间件,并且有一个回溯的过程。官方代码如下。
function compose (middleware) {
if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
for (const fn of middleware) {
if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
}
return function (context, next) {
// last called middleware #
let index = -1
return dispatch(0)
function dispatch (i) {
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
let fn = middleware[i]
if (i === middleware.length) fn = next
if (!fn) return Promise.resolve()
try {
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err)
}
}
}
}
所以总结一下,洋葱模型本质是通过递归去实现的。
讲了 compose
的原理之后,回到第三步中最后的 this.handleRequest(ctx, fn);
fn
就是compose
返回的包装过 middleware
的函数。下面进入 handleRequest
可以看到当一个请求来到的时候,最后会去执行包装过的中间件函数,也就是这里的最后一行,并在中间件执行完毕之后,到 handleResponse
中去处理响应。在 handleResponse
中最终执行的是 respond
respond
function respond(ctx) {
// allow bypassing koa
if (false === ctx.respond) return;
if (!ctx.writable) return;
const res = ctx.res;
let body = ctx.body;
const code = ctx.status;
// ignore body
if (statuses.empty[code]) {
// strip headers
ctx.body = null;
return res.end();
}
if ('HEAD' === ctx.method) {
if (!res.headersSent && !ctx.response.has('Content-Length')) {
const { length } = ctx.response;
if (Number.isInteger(length)) ctx.length = length;
}
return res.end();
}
// status body
if (null == body) {
if (ctx.req.httpVersionMajor >= 2) {
body = String(code);
} else {
body = ctx.message || String(code);
}
if (!res.headersSent) {
ctx.type = 'text';
ctx.length = Buffer.byteLength(body);
}
return res.end(body);
}
// responses
if (Buffer.isBuffer(body)) return res.end(body);
if ('string' == typeof body) return res.end(body);
if (body instanceof Stream) return body.pipe(res);
// body: json
body = JSON.stringify(body);
if (!res.headersSent) {
ctx.length = Buffer.byteLength(body);
}
res.end(body);
}
主要是将 ctx
上挂载的 body
通过 res.end
返回响应。
源码解析 - request.js
module.exports = {
/**
* Return request header.
*
* @return {Object}
* @api public
*/
get header() {
return this.req.headers;
},
/**
* Set request header.
*
* @api public
*/
set header(val) {
this.req.headers = val;
},
..............
}
可见前面在 createContext
的时候在 request
上面去挂载 req、res
的原因就在这里。
源码解析 - response.js
module.exports = {
/**
* Return the request socket.
*
* @return {Connection}
* @api public
*/
get socket() {
return this.res.socket;
},
/**
* Get response status code.
*
* @return {Number}
* @api public
*/
get status() {
return this.res.statusCode;
},
.......................
}
源码解析 - context.js
const proto = module.exports = {
.............
get cookies() {
if (!this[COOKIES]) {
this[COOKIES] = new Cookies(this.req, this.res, {
keys: this.app.keys,
secure: this.request.secure
});
}
return this[COOKIES];
},
set cookies(_cookies) {
this[COOKIES] = _cookies;
}
............
}
/**
* Response delegation.
*/
delegate(proto, 'response')
.method('attachment')
.method('redirect')
.method('remove')
.method('vary')
.method('has')
.method('set')
.method('append')
.method('flushHeaders')
.access('status')
.access('message')
.access('body')
.access('length')
.access('type')
.access('lastModified')
.access('etag')
.getter('headerSent')
.getter('writable');
/**
* Request delegation.
*/
delegate(proto, 'request')
.method('acceptsLanguages')
.method('acceptsEncodings')
.method('acceptsCharsets')
.method('accepts')
.method('get')
.method('is')
.access('querystring')
.access('idempotent')
.access('socket')
.access('search')
.access('method')
.access('query')
.access('path')
.access('url')
.access('accept')
.getter('origin')
.getter('href')
.getter('subdomains')
.getter('protocol')
.getter('host')
.getter('hostname')
.getter('URL')
.getter('header')
.getter('headers')
.getter('secure')
.getter('stale')
.getter('fresh')
.getter('ips')
.getter('ip');
这里的 proto
就是 context
,在自身定义了一个常用的方法,可通过 ctx.method
去访问,还有后面使用 delegate
,这个函数会把自 context
上面的 request、response
上面的一些属性定义到 proto
也就是 context
上面去,但是当使用 ctx.xxx
去访问的时候,其实是访问 request、response
上面的属性,这也是为什么需要将 request、response
挂载到 context
上面去。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。