Koa 源码解读

img

koa 是一个新的 web 框架,致力于成为 web 应用和 API 开发领域中的一个更小、更富有表现力、更健壮的基石。但是每当说起 koa ,都可以想到中间件或洋葱模型,那如何理解它是一个中间件或洋葱模型,今天就带大家通过源码的方式理解下。

监听端口号

通过 http.createServer 开启服务监听特定端口号,考虑到参数可能是多个,所以用了 ...args

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

创建上下文

koa 通过 createContext 方法创建上下文。可以注意到 context 上挂载了原始的 reqres,同时也挂载了我们自定义的 contextrequestresponse。在自定义内部,我们通过 delegates 便捷操作属性,其本质上是通过 getset 实现。

// 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');
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

我们深入挖掘下 delegatedelegate 是引用一个名为 delegates 的三方包,它提供了 methodaccessgettersetterfluent 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
    7

    person 上访问 getName 方法,相当于访问 person.like.getName。之所以输出赵晓,是因为 method 内部使用 applythis 又指向为 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 绑定了 getset

中间件

function add(x, y){
    return x + y;
}
function square(x){
    return x ** 2;
}
1
2
3
4
5
6

我们想顺序调用 addsquare 方法来计算数据,那我们的调用函数为 square(add(1, 2))

我们改变一下场景,如果调用的函数又多了一个 double 函数,那我们的调用函数为 double(square(add(1, 2)))

function double(x){
    return 2 * x;
}
1
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)

1
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)
})
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

我们每一次的 next 都为下一个异步函数,且每一个函数内部均有 await,所以当我们的函数顺序执行完毕之后,便会反方向依次调用剩下代码,所以这一段代码输出为 123654

说到这里,是不是就可以理解为什么我们说 koa 像一个洋葱,从上到下一个一个执行,然后从下往上再一层一层回去。

深入阅读

最后更新: 8/10/2019, 7:38:12 PM