源码共读05:koa源码 洋葱模型

114 阅读4分钟

koa Github

本文参加了由公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。

这是学习源码整体架构系列第七篇,链接: juejin.cn/post/684490…

Koa-compose 中间件

image.png

在koa中,请求和响应都放在中间件的第一个参数context对象中

引用Koa中文文档中的一段:

如果您是前端开发人员,您可以将 next(); 之前的任意代码视为“捕获”阶段,这个简易的 gif 说明了 async 函数如何使我们能够恰当地利用堆栈流来实现请求和响应流:

  1. 创建一个跟踪响应时间的日期
  2. next()
  3. 创建另一个跟踪响应时间的日期
  4. next()
  5. next()
  6. 处理请求,响应体为'Hello World';
  7. 计算持续时间
  8. 输出日志行
  9. 设置X-Response-Time头字段
  10. 交给 Koa 处理响应

new Koa()结果app是什么?

image.png

app 实例、context、request、request 官方API文档

Koa主流程简化梳理


class EventEmitter {
    // nodejs内置模块
    constructor(options?: EventEmitterOptions) {};
}
class Application extends Emitter {
    constructor(options) {
         super()
        options = options || {}
        this.middleware = []
        // context from require('./context')
        /**
        * inspect()
        * response
        * request
          */
        this.context = Object.create(context)
    }
    use(fn){
        this.middleware.push(fn);
        return this;
    }
    listen(){
        // callback() {}
        // handleRequest() {}
        const  fnMiddleware = compose(this.middleware);
        const ctx = this.context;
        const handleResponse = () => respond(ctx);
        const onerror = function(){
            console.log('onerror');
        };
        fnMiddleware(ctx).then(handleResponse).catch(onerror);
    }
}
function respond(ctx){
    console.log('handleResponse');
    console.log('response.end', ctx.body);
}

主流程的关键代码在listen函数中的compose这个函数,接下来分析compose函数

koa-compose 源码(洋葱模型实现)

通过app.use()添加若干函数,但要把它们串联起来执行。compose函数:传入一个数组,返回一个函数。对传入的参数做类型校验数组的每一项是不是函数。

// simpleKoaCompose
// 可以简单理解为
const [fn1, fn2, fn3] = this.middleware;
const fnMiddleware = function(context) {
    return Promise.resolve(
        fn1(context, function next() {
            return Promise.resolve(
                fn2(context, function next() {
                    return Promise.resolve(
                        fn3(context, function next() {
                            return Promise.resolve();
                        })
                    )
                })
            )
        })
    );
};
fnMiddleware(ctx).then(handleResponse).catch(onerror);

文字解释:

koa-compose返回的是一个Promise,Promise中取出第一个函数(app.use添加的中间件),传入context和第一个next函数来执行。

第一个next函数里返回的是一个Promise,Promise中取出第二个函数(app.use添加的中间件),传入context和第二个next函数来执行。

第二个next函数里返回的是一个Promise,Promise中取出第三个函数(app.use添加的中间件),传入context和第三个next函数来执行。

第三个...

以此类推。最后一个中间件有调用next函数,则返回Promise.resolve。如果没有,则不执行next函数。这样就把所有的中间件串联起来。这就是常说的洋葱模型。

错误处理

文档中写了三种捕获错误的方式:

  • ctx.onerror 中间件中的错误捕获
  • app.on('error', (err) => {})最外层实例事件监听形式
  • app.onerror => (err) => {} 重写onerror自定义形式

ctx.on error

module.exports = {
    onerror(){
        // delegate
        // app 是在new Koa() 实例
        this.app.emit('error', err, this);
    }
}
app.use(async (ctx, next) => {
    try {
        await next();
    } catch (err) {
        err.status = err.statusCode || err.status || 500;
        throw err;
    }
});

try catch 错误或被fnMiddleware(ctx).then(handleResponse).catch(onerror);这里的onerrorctx.onerror

ctx.onerror 函数中有调用了this.app.emit('error', err, this);,所以在最外围的app.on('error',err => {})可以捕获中间件链中的错误。因为koa继承自events模块,所以有emiton等方法。

koa2 和 koa1的简单对比

中文文档中描述了 koa2 和 koa1 的区别

koa1中主要是generator函数,koa会自动转换generator函数

// koa 将转换
app.use(function *(next) {
    const start = Date();
    yield next;
    const ms = Date.now() - start;
    console.log(`${this.method} ${this.url} - ${ms}ms`);
    
})

koa-convert 源码

class Koa extends Emitter {
    use(fn) {
        if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
        // 判断是否为generator函数,如果是则使用koa-convert暴露的方法convert来转换重新赋值,再存入middleware,后续再使用。
        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;    
}

koa-convert 核心源码

function convert() {
    return function (ctx, next) {
        return co.call(ctx, mw.call(ctx, createGenerator(next)))
    }
    function * createGenerator(next) {
        return yield next()
    }
}

最后还是通过co转换的。

co源码

源码共读04:CO源码

Koa与express对比

总结

koa-compose是将app.use添加到middleware数组中的中间件(函数),通过Promise串联起来,next()返回的是一个Promise。

koa-convert判断app.use传入的函数是否为generator函数,如果是则用koa-convert来转换,最后还是调用co函数。

co原理:其实就是通过不断调用generator函数的next函数,来达到自动执行generator函数的效果(类似于async、await函数的自动执行)。

koa框架总结:主要就是四个核心概念,洋葱模型(把中间件串联起来),http请求上下文,http请求对象,http响应对象。

此文章为01月Day1源码共读,每一次脑海里闪过努力的念头,都是未来的你在向你求救。