【koa】原理

Nodejs | 2021-03-15 23:20:27 1569次 2次

koa 是一个简易实用的框架,尤其是中间件的设计思想,我并说不出哪里好或者不好,因为我没有实际的使用。大致体会下,就是上一个中间件可以控制下一个,但是顺序紧密结合,从而导致的问题是没有一个好的流程跳转。

当然,这个中间件设计思路对我的影响是有意义的,比如在写业务的时候,设计了一个事件模型,假如有一个事件,在基础代码中预置了逻辑,暴露给业务使用时,提供一个此事件前、后回调并且可以决定是否执行预置逻辑,那么就可以通过 next 机制实现,伪代码如下:

// async or common
Save(next, props){
    // before
    next()
    // after
}

在了解 koa 这个主要的思想之后,下面看下它的大致流程,文件结构划分很简单,只有四个文件,代码量也很少:

application

context

request

response


入口 application

首先是 createServer 启动一个服务,紧接着创建 createContext 关系,再次是中间件调度执行,最后做出响应,按照这个步骤可以实现一套简单的代码:

let http = require('http');
let context = require('./context');
let request = require('./request');
let response = require('./response');

class Koa{
    constructor(){
        this.context = context;
        this.request = request;
        this.response = response;
        this.middlewares = [];
    }
    use(cb){}
    createContext(req, res){
        // 多个示例 避免数据共享
        let ctx = Object.create(this.context);
        ctx.request = Object.create(this.request);
        ctx.response = Object.create(this.response);
        ctx.req = ctx.request.req = req;
        ctx.res = ctx.response.res = res;
        return ctx;
    }
    compose(ctx, middlewares){}
    handleRequest(req, res) {
        res.statusCode = 404;
        let ctx = this.createContext(req, res)
        let composeMiddleware = this.compose(ctx, this.middlewares); //执行后ctx.body会被修改
        ...
    }
    listen(...arg){
        let server = http.createServer(this.handleRequest.bind(this))
        server.listen(...arg)
    }
}

module.exports = Koa;

创建 context 关系时,通过 Object.create 避免多个 koa 实例中 ctx 数据共享。

关于中间件其实没那么神秘,当 use 的时候,收集中间件方法进入一个队列,通过 compose 方法来执行,之前在写 js 进阶部分中有介绍 redux 中的 compose 实现 ,但是这里的中间件执行和 redux 中是不一样的,这是一种异步洋葱模型,原理如下:

    // middlewares 队列包含所有的中间件方法
    compose(ctx, middlewares){
        let dispatch = (index) => {
            // 到达最后一个
            if(index === middlewares.length){
                return Promise.resolve();
            }
            let mid = middlewares[index];
            // next  
            return Promise.resolve(mid(ctx, () => dispatch(index + 1)))
        }
        return dispatch(0);
    }


request/response

对 http 原生方法的一层封装,源码中实现很全面,大致意思如下,

let parse = require('parseurl');

let request = {
    get url(){
        return this.req.url;
    },
    get path(){
        return parse(this.req).pathname;
    }
}

let response = {
    set body(value){
        this.res.statusCode = 200;
        this._body = value;
    },
    get body(){
        return this._body;
    }
}


context

这个方法主要是对 request/response 中方法的劫持,直接操作 ctx.xxx 即可,无需 ctx.response.xxx,所以思路也很清晰,大致原理如下,就是对 request/response 添加一层代理

class Delegator{
    constructor(proto, target){
        this.proto = proto;
        this.target = target;
    }

    getters(name) {
        let { proto, target }  = this;
        Object.defineProperty(proto, name, {
            get() {
                return this[target][name];
            },
            configurable: true
          });
        return this;
    }

    setters(name) {
        let { proto, target }  = this;
        Object.defineProperty(proto, name, {
            set(v) {
                this[target][name] = v;
            },
            configurable: true
        });
        return this;
    }
}

let proto = {}

new Delegator(proto, 'request')
    .getters('url')
    .getters('path')

new Delegator(proto, 'response')
    .getters('body')
    .setters('body')

module.exports = proto;


错误机制 

如上的方法中介绍了 koa 一个大致的思路,还有一个很重要的点,那就是错误处理机制。koa 类继承自 event 模块,可以监听错误;错误的处理在 application 和 context 中都有,他们的区别是前者处理的是用户没有进行 app.on('error') 时,则默认帮助监听错误,实现原理如下:

    application.js:
    
    onerror(err){
        //非原生错误拦截掉,否则会正常启动服务 app.onerror('some error')
        const isNativeError = Object.prototype.toString.call(err) === '[object Error]' || err instanceof Error;
        if (!isNativeError){
            throw new TypeError('non-error thrown: ' + err)
        };

        // 原生错误输入日志提示
        const msg = err.stack || err.toString();
        console.error(`\n${msg.replace(/^/gm, '  ')}\n`);
    }
    
    handleRequest(){
        // 如果用户没有 app.on('error') 监听,进行内置的处理
        if (!this.listenerCount('error')){
            this.on('error', this.onerror)
        };
    }


再去看下 context 中的错误处理,它的目的是在中间件中有错误丢出时,可以帮助你处理这个错误,并通过 event 的 emit 通知,这样也可以监听,原理如下:

let proto = {
    throw(...arg){
        const err = new Error();
        err.status = arg[0];
        err.message = arg[1];
        throw err;
    },
    onerror(err) {
        if (null == err) return;
        this.app.emit('error', err, this);
        const { res } = this;
        if (typeof res.getHeaderNames === 'function') {
            res.getHeaderNames().forEach(name => res.removeHeader(name));
        }
        // respond
        this.type = 'text/plain;charset=utf-8';
        this.status = err.status;
        res.end(err.message);
    }
}

测试用例:

app.use(async (ctx, next)=>{
    // 通过这个方法丢出错误
    ctx.throw(403, '暂无权限')
    await next()
})

在上面的 onerror 中仅仅是根据错误作出响应,那么如何捕获到中间件中的错误,这个就需要回到 compose 方法,因为中间件中有错误(执行 ctx.throw 时丢出的),所以可以通过 try catch 进行捕获,并且在执行中间件的时候进行 catch:

    compose(ctx, middlewares){
        let dispatch = (index) => {
            ...
            // next  
            try{
                return Promise.resolve(mid(ctx, ()=> dispatch(index + 1)))
            }catch(err){
                return Promise.reject(err)
            }
        }
        return dispatch(0);
    }
    
    handleRequest(req, res) {
        ...
        const onerror = err => ctx.onerror(err);
        // 通过 catch 调用 context 中的错误处理
        this.compose(ctx, this.middlewares).then(...).catch(onerror)
    }

koa 的设计中主要还是围绕 response/request 的处理,从而展开的一些轻量封装。


2人赞

分享到: