Koa 原理和源码解析

前言

在刚开始学习的时候就看过 Koa 相关代码,代码很简单整洁,这次完整记录一下。

基本使用

1
2
3
4
5
6
7
8
9
const Koa = require("koa");
const app = new Koa();

// response
app.use((ctx, next) => {
ctx.body = "Hello Koa";
});

app.listen(3000);

这是文档里面最基本的使用,具体表现是接受到请求就返回 Hello Koa,按照这个代码顺序看代码。

初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
constructor() {
super();

this.proxy = false;
this.middleware = [];
this.subdomainOffset = 2;
this.env = process.env.NODE_ENV || 'development';
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
if (util.inspect.custom) {
this[util.inspect.custom] = this.inspect;
}
}

new Koa()里面初始化了一堆变量,其中最为重要的是contextrequestresponse,里面的作用等会再看,先看app.use()代码。

app.use()

1
2
3
4
5
6
7
8
9
10
11
12
use(fn) {
if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
if (isGeneratorFunction(fn)) {
deprecate('Support for generators will be removed in v3. ' +
'See the documentation for examples of how to convert old middleware ' +
'https://github.com/koajs/koa/blob/master/docs/migration.md');
fn = convert(fn);
}
debug('use %s', fn._name || fn.name || '-');
this.middleware.push(fn);
return this;
}

这边代码很简单,就是检查入参,是不是方法,然后兼容Generator,最后只是简单的pushmiddleware里面。

app.listen(3000)

1
2
3
4
5
listen(...args) {
debug('listen');
const server = http.createServer(this.callback());
return server.listen(...args);
}

app.listen(3000)只是简单的http.createServer启动,然后放入this.callback()创建回调,下面看看this.callback()代码

this.callback()

1
2
3
4
5
6
7
8
9
10
11
12
callback() {
const fn = compose(this.middleware);

if (!this.listenerCount('error')) this.on('error', this.onerror);

const handleRequest = (req, res) => {
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
};

return handleRequest;
}

这边有一个compose(this.middleware)操作,看看里面是干嘛的

compose(this.middleware)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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);
}
}
};
}

可以看到返回一个方法,里面其实是递归执行,从dispatch(0)开始执行,然后到return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));不断递归,假设已经middleware.length最后一个,那么就返回一个Promise.resolve();,也就是这段代码实现了洋葱圈模型,也就是app.use里面的next参数实际上是执行之后的middleware,这里有个官方 gif 图讲述这个过程。

middleware

 实际上类似的模型我们已经在React Redux 原理分析和部分源码解析看过,基本上中间件现在都是这样的模式

this.handleRequest

context刚刚没进去看,主要是一些默认context的方法,然后继承他,还有就是delegate属性responserequest暴露出来,然后到了创建context

createContext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
createContext(req, res) {
const context = Object.create(this.context);
const request = context.request = Object.create(this.request);
const response = context.response = Object.create(this.response);
context.app = request.app = response.app = this;
context.req = request.req = response.req = req;
context.res = request.res = response.res = res;
request.ctx = response.ctx = context;
request.response = response;
response.request = request;
context.originalUrl = request.originalUrl = req.url;
context.state = {};
return context;
}

没啥好说的,就是创建contextrequestresponse,这里面本来都是简单的方法,方便你调用,比如headers等,再配合contextdelegate使常用的操作直接暴露出来。

handleRequest

1
2
3
4
5
6
7
8
handleRequest(ctx, fnMiddleware) {
const res = ctx.res;
res.statusCode = 404;
const onerror = err => ctx.onerror(err);
const handleResponse = () => respond(ctx);
onFinished(res, onerror);
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}

这边就是执行中间件,然后最后是handleResponse,里面方法是检查respond 是否正确,比如 204,304 这种就直接返回,不需要 body,等等检查。

没了?

就是没了…Koa 的代码十分简单,只有核心代码,如果要实现router,那么就接入koa-router,实现原理也是中间件,如果我需要解析json,那么就接入
koa-json,这一切,都基于中间件,在中间件里面处理解析,router 分发等等。

比如我需要增加 cors 允许跨越,那么就可以写一个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module.exports = async function(ctx, next) {
ctx.set({
"Access-Control-Allow-Origin": "*",
"Access-Control-Max-Age": "86400",
"Access-Control-Allow-Headers":
"Content-Type, Content-Length, Authorization, Accept, X-Requested-With, Token",
"Access-Control-Allow-Methods": "PUT, POST, GET, DELETE, OPTIONS"
});

if (ctx.method === "OPTIONS") {
return (ctx.status = 200);
}

await next();
};

就可以了。

总结

Koa 只实现了核心功能,提供了中间件的扩展的洋葱圈模型,其他高级功能都需要中间件去实现,实现方式有很多玩法,吾辈曾经配合测试和中间件实现了单元测试输出文档的功能,其核心也就是检查ctx某个自定义配置属性,根据配置输出 api 的requestresponse等,做到一个基本的 api 文档输出。

同时因为 Koa 只提供了核心功能,在组合其他 👖 的时候容易出现方式不一致,从而诞生出Egg.js这种增强 👖,提高代码规范, 大多数我也是使用Egg.js