koa源码学习记录

koa 源码设计的巧妙而又短小精悍,学习 koa 源码能够理解 koa 的设计思想,而又不会像学习其他框架源码一样过于庞大的代码让人头晕目眩。

洋葱模型

koa 洋葱模型主要依赖 koa-compose 这个包。这个依赖包就一个文件:

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
28
29
function compose(middleware) {
//传入middleware数组
if (!Array.isArray(middleware))
throw new TypeError("Middleware stack must be an array!"); //判断middleware是否为数组
for (const fn of middleware) {
//过一遍middleware,判断每个成员是否为函数
if (typeof fn !== "function")
throw new TypeError("Middleware must be composed of functions!");
}

return function (context, next) {
//返回一个函数
let index = -1; //index计数
return dispatch(0); //调用dispatch,传入0
function dispatch(i) {
if (i <= index)
return Promise.reject(new Error("next() called multiple times")); //i小于index,证明在中间件内调用了不止一次的next(),抛出错误
index = i; //更新index的值
let fn = middleware[i]; //middleware中的函数,从第1个开始
if (i === middleware.length) fn = next; //如果i走到最后一个的后面,就让fn为next,此时fn为undefined
if (!fn) return Promise.resolve(); //那么这时候就直接resolve
try {
return Promise.resolve(fn(context, dispatch.bind(null, i + 1))); //将其包装成一个Promise resolve态,主要作用是区分reject
} catch (err) {
return Promise.reject(err); //catch错误,并reject
}
}
};
}

首先直接返回一个函数,函数内部返回一个 dispatch 函数,dispatch 调用并传入 0,现在我们 fn 取到了 middleware 的第一个中间件,然后返回被 Promise 包裹的、fn 的执行结果。fn 在调用时传入两个参数。

  • context 也就是中间件函数里的 ctx
  • dispatch.bind(null, i + 1) 下一个中间件函数,用 bind 把 this 指向 null,也就是中间件函数里的 next

所以,调用 next,就可以把函数的执行权交给下一个中间件,待其执行完,在回过头继续执行自身,这样代码形成回形针式的级联。这也就是老生常谈的洋葱模型

自己实现一个类似的洋葱模型

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
const EventEmitter = require("events");
const http = require("http");
class Application extends EventEmitter {
constructor() {
super();
this.middlewares = [];
}

use(middleware) {
this.middlewares.push(middleware);
}

listen(...arg) {
const server = http.createServer(this.callback());
server.listen(...arg);
}

compose() {
return async (ctx) => {
function createNext(middleware, oldNext) {
return async () => {
await middleware(ctx, oldNext);
};
}

let len = this.middlewares.length;
let next = () => Promise.resolve();
for (let i = len - 1; i >= 0; i--) {
let currentMiddleware = this.middlewares[i];
next = createNext(currentMiddleware, next);
}
await next();
};
}

callback() {
return (req, res) => {
let fn = this.compose();
let ctx = {};
return fn(ctx).then(() => {
res.end("hello world");
});
};
}
}

module.exports = Application;

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;
}

这里做了 3 件事:

  • 每一个 app 都有其对应的 context、request、response 实例,每一个请求,都会基于这些实例去创建自己的实例。在这里就是创建了 context、request、response。
  • 将 node 原生的 res、req 以及 this 挂载到 context、request、response 上面。还有一些其他为了方便访问做得一些挂载,不过前面三个的挂载是必须的。
  • 将创建的 context 返回,传给所有中间件的第一个 ctx 参数,作为这个请求的上下文。
    这样我们可以在 context 上访问到 request 和 response。

    一个 ctx 即可获得所有 koa 提供的数据和方法,而 koa 会继续将这些职责进行进一步的划分,比如 request 是用来进一步封装 req 的,response 是用来进一步封装 res 的,这样职责得到了分散,降低了耦合,同时共享所有资源使得整个 context 具有了高内聚的性质,内部元素互相都能够访问得到。

context.js

context.js 的核心就是通过 delegates 这一个库, 将 request, response 对象上的属性方法代理到 context 对象上。
主要代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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");

delegate method 代码如下:

1
2
3
4
5
6
7
8
9
Delegator.prototype.method = function (name) {
var proto = this.proto;
var target = this.target;
this.methods.push(name);
proto[name] = function () {
return this[target][name].apply(this[target], arguments);
};
return this;
};

target.name 包装一层函数赋值给 proto.name,也就是将 target 上的函数也能让 proto 去调用。

getter 通过defineGetter劫持 proto 的 get,转而去访问 target:

1
2
3
4
5
6
7
8
9
Delegator.prototype.getter = function (name) {
var proto = this.proto;
var target = this.target;
this.getters.push(name);
proto.__defineGetter__(name, function () {
return this[target][name];
});
return this;
};

access 取到值再去调用 setter 设置,setter 代码也是通过defineGetter劫持:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Delegator.prototype.access = function (name) {
return this.getter(name).setter(name);
};

Delegator.prototype.setter = function (name) {
var proto = this.proto;
var target = this.target;
this.setters.push(name);

proto.__defineSetter__(name, function (val) {
return (this[target][name] = val);
});

return this;
};
查看评论