Koa 源码解读
koa
是一个新的 web
框架,致力于成为 web
应用和 API
开发领域中的一个更小、更富有表现力、更健壮的基石。但是每当说起 koa
,都可以想到中间件或洋葱模型,那如何理解它是一个中间件或洋葱模型,今天就带大家通过源码的方式理解下。
监听端口号
通过 http.createServer
开启服务监听特定端口号,考虑到参数可能是多个,所以用了 ...args
。
// application.js
listen(...args) {
const server = http.createServer(this.callback());
return server.listen(...args);
}
2
3
4
5
创建上下文
koa
通过 createContext
方法创建上下文。可以注意到 context
上挂载了原始的 req
与 res
,同时也挂载了我们自定义的 context
、 request
和 response
。在自定义内部,我们通过 delegates
便捷操作属性,其本质上是通过 get
和 set
实现。
// application.js
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;
}
// context.js
delegate(proto, 'response')
.method('attachment')
.method('redirect')
.method('remove')
.method('vary')
.method('set')
.method('append')
.method('flushHeaders')
.access('status')
.access('message')
.access('body')
.access('length')
.access('type')
.access('lastModified')
.access('etag')
.getter('headerSent')
.getter('writable');
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
我们深入挖掘下 delegate
,delegate
是引用一个名为 delegates
的三方包,它提供了 method
、access
、getter
、setter
、fluent
5 个方法。
method
example
let person = { boy: '张三', like: { girl: '赵晓', getName: function (){ console.log(this.girl) } } } delegate(person, 'like').method('getName'); person.getName(); // 赵晓
1
2
3
4
5
6
7
8
9
10
11释义
Delegator.prototype.method = function(name){ ... proto[name] = function(){ return this[target][name].apply(this[target], arguments); }; ... };
1
2
3
4
5
6
7person
上访问getName
方法,相当于访问person.like.getName
。之所以输出赵晓,是因为method
内部使用apply
把this
又指向为person.like
。
access
example
let person = { boy: '张三', like: { girl: ' 赵晓' } } delegate(person, 'like').access('girl'); console.log(person.girl); // 赵晓 person.girl = '小红'; console.log(person.girl); // 小红 console.log(person.like.girl); // 小红
1
2
3
4
5
6
7
8
9
10
11释义
person
上访问girl
,相当于访问person.like.girl
,所以会输出赵晓。紧接着,我们又重设了person.girl
的值为小红,发现person.like.girl
也输出了小红,所以access
绑定了get
和set
。
中间件
function add(x, y){
return x + y;
}
function square(x){
return x ** 2;
}
2
3
4
5
6
我们想顺序调用 add
、square
方法来计算数据,那我们的调用函数为 square(add(1, 2))
。
我们改变一下场景,如果调用的函数又多了一个 double
函数,那我们的调用函数为 double(square(add(1, 2)))
。
function double(x){
return 2 * x;
}
2
3
如果调用的函数个数不固定呢?那我们则需把调用的函数给合并成一个。
let middleware = [add, square, double]
function compose(middleware){
return middleware.reduce((prevFn, currentFn)=>(...args)=>currentFn(prevFn(...args)))
}
let composeFn = compose(middleware)
composeFn(1, 2)
2
3
4
5
6
7
8
9
10
我们来调用下 compose
函数,可以发现输出了 18
,证明这个函数式正确的。compose
函数在这里就充当了中间件,但是我们的中间件是同步的,而 koa
是异步中间件,那异步中间件该如何去做?
// 异步中间件
function compose(middleware){
return function(context, next){
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);
}
}
}
}
// koa 示例
app.use(async (ctx, next)=>{
console.log(1)
await next();
console.log(4)
})
app.use(async (ctx, next)=>{
console.log(2)
await next();
console.log(5)
})
app.use(async (ctx, next)=>{
console.log(3)
await next();
ctx.body = 'Hello, Koa';
console.log(6)
})
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
我们每一次的 next
都为下一个异步函数,且每一个函数内部均有 await
,所以当我们的函数顺序执行完毕之后,便会反方向依次调用剩下代码,所以这一段代码输出为 123654。
说到这里,是不是就可以理解为什么我们说 koa
像一个洋葱,从上到下一个一个执行,然后从下往上再一层一层回去。