0%

koa-compose原理

最近看了看koa的源码,源码很简洁,组织的很清爽。

这里面最重要的一块是中间件的实现,koa通过koa-compose串联到了一起。

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
const Koa = require('koa');

const app = new Koa();
const PORT = 3000;

// #1
app.use(async (ctx, next)=>{
console.log(1)
await next();
console.log(1)
});
// #2
app.use(async (ctx, next) => {
console.log(2)
await next();
console.log(2)
})

// #3
app.use(async (ctx, next) => {
console.log(3)
})

app.listen(PORT);
console.log(`http://localhost:${PORT}`);

上述代码的执行顺序,按照如下的箭头方向依次执行,每一个圈都是一个中间件,每个中间件,都可以选择前置或者后置执行对应逻辑,这就是洋葱模型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

+---------------------------------+
| |
| |
|#1: 1 #1: 1 |
| +-------------------+ |
| | | |
| |#2: 2 #2: 2| |
| | +-------+ | |
| | | #3: 3 | | |
| | | | | |
+------------------------------------------->
| | | | | |
| | +-------+ | |
| | | |
| +-------------------+ |
| |
| |
+---------------------------------+

实现

首先是注册中间件,它通过use方法,将中间件放入app实例中的middleware数组

1
app.use(fn);

接下来就是执行中间件,来看其中一部分源码

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

...

callback() {
// 组合中间件
const fn = compose(this.middleware);

const handleRequest = (req, res) => {
const ctx = this.createContext(req, res);
// 当请求过来的时候,交由handleRequest处理
return this.handleRequest(ctx, fn);
};

return handleRequest;
}

...

handleRequest(ctx, fnMiddleware) {
const res = ctx.res;
res.statusCode = 404;
const onerror = err => ctx.onerror(err);
const handleResponse = () => respond(ctx);
onFinished(res, onerror);
// 在这里将ctx传入中间件组合函数,运行所有中间件后,交由handleResponse处理
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}

中间件在callback中经过compose处理返回了一个promise函数,然后在handleRequest函数中执行处理所有的中间件,最终返回结果交由handleResponse处理。

那么compose到底做了什么?

来看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
function compose (middleware) {
...
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]
// compose函数支持扩展中间件,通过next,在最后插入其他中间件。
if (i === middleware.length) fn = next
if (!fn) return Promise.resolve()
try {
// 在这里执行中间件,并将下一个中间件的dispatch函数绑定后传入,由其控制。
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
// 中间件运行报错
return Promise.reject(err)
}
}
}
}

compose会返回一个触发(dispatch)函数,每一次运行dispatch,就会执行一个middleware,同时它记录了一个middleware的执行座标i,通过i来获取middleware数组中当前执行的中间件函数,同时也根据这个座标巧妙的判断当前中间件是否执行了多次。

比较重要的是,下一个中间件dispatch函数会被绑定后传入当前执行的中间件函数,把下一个中间件dispatch函数控制权交给了当前中间件内的逻辑,以此实现了前置和后置执行的洋葱模型。