Node.js理论实践之《Koa原理浅析》

768 阅读6分钟

学习Koa框架之前,不得不提到Express。

Express是一个基于Node.js平台的极简、灵活的 web 应用开发框架,主要基于 Connect 中间件,并且自身封装了路由、视图处理等功能,使用人数众多。

Koa相对更为年轻,是 Express 原班人马基于 ES6/7 异步流程控制重新开发的框架, 框架自身不包含任何中间件,很多功能需要借助第三方中间件解决,解决了回调地狱和麻烦的错误处理问题。

  • koa2与koa1的最大区别是koa2实现异步是通过async/await,koa1实现异步是通过generator + co,而express实现异步是通过回调函数的方式。
  • koa2与express 提供的API大致相同,express是大而全,内置了大多数的中间件,更让人省心,koa2不绑定任何的框架,干净简洁,小而精,更容易实现定制化,扩展性好。
  • express是没有提供ctx来提供上下流服务,需要更多的手动处理,express本身是不支持洋葱模型的数据流入流出能力的,需要引入其他的插件。

源码结构

我们主要学习Koa2,首先看一下它的源码结构:

image
koa2的核心文件:application.jscontext.jsrequest.jsresponse.js

  1. application.js:Application(或Koa)负责管理中间件,以及处理请求

    • application.js是koa的入口文件,它向外导出了创建class实例的构造函数,它继承了events,这样就会赋予框架事件监听和事件触发的能力。 application还暴露了一些常用的api,比如toJSON、listen、use等等。
    • listen的实现原理其实就是对http.createServer进行了一个封装,重点是这个函数中传入的callback,它里面包含了中间件的合并,上下文的处理,对res的特殊处理。
    • use是收集中间件,将多个中间件放入一个缓存队列中,然后通过koa-compose这个插件进行递归组合调用这一些列的中间件。
  2. context.js:Context维护了一个请求的上下文环境

    • koa的应用上下文ctx,其实就一个简单的对象暴露,里面的重点在delegate,这个就是代理,这个就是为了开发者方便而设计的。
    • ctx 主要的功能是代理 request 和 response 的功能,提供了对 request 和 response 对象的便捷访问能力。 比如我们要访问ctx.response.status但是我们通过delegate,可以直接访问ctx.status访问到它。
  3. request.js:Request对req做了抽象和封装

  4. response.js:Response对res做了抽象和封装

    这两部分就是对原生的res、req的一些操作了,大量使用es6的get和set的一些语法,去取headers或者设置headers、还有设置body等等。

功能模块

koa框架需要实现四个大模块,分别是:

  1. 封装http模块的server、创建Koa类构造函数
  2. 构造request、response、context对象
  3. 中间件机制和洋葱模型的实现
  4. 错误捕获和错误处理

封装http模块的server、创建Koa类构造函数:

通过application.js源码得知,koa的服务器应用和端口监听,其实就是基于node的http.createServer原生代码进行了封装。我们来简单实现一下这个功能。

let http = require('http');

class Application{    
    constructor() {        
        this.callbackFunc = ()=>{};
    }
    //开启服务器实例并传入callback回调函数
    listen(port) {        
        let server = http.createServer(this.callback());
        server.listen(port);
    }
    //注册中间件和注册回调函数
    use(fn) {
        this.callbackFunc = fn;
    }
    callback() {
        return (req, res) => {
            this.callbackFunc(req, res);
        };
    }
}
module.exports = Application;

然后创建demo.js,引入application.js

let Koa = require('./application');
let app = new Koa();
app.use((req, res) => {
    res.writeHead(200);
    res.end('hello world');
});
app.listen(3000, () => {
    console.log('listening on 3000');
});

构造request、response、context对象:

request、response两个功能模块分别对node的原生request、response进行了一个功能的封装,使用了getter和setter属性,基于node的对象req/res对象封装koa的request/response对象。

  • request.js 封装了query、header、url、origin、path等,比如ctx.query就是返回url.parse(this.req.url, true).query。
  • response.js 封装了status、body、message等,比如ctx.status就是返回res.statusCode。
  • context.js 再将request和response挂载到了ctx上。
let http = require('http');
let context = require('./context');
let request = require('./request');
let response = require('./response');

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;
}

中间件机制和洋葱模型的实现

koa的中间件机制是洋葱模型,多个中间件通过use放进一个数组队列,符合先进后出的原则。然后从外层开始执行,遇到next后进入队列中的下一个中间件,所有中间件执行完后开始回帧,执行队列中之前中间件中未执行的代码部分。

image

示例:

const Koa = require('koa');
const app = new Koa();

//#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(3000);

访问http://localhost:3000,控制台打印:

中间件 1 进入
中间件 2 进入
中间件 3 
中间件 2 退出
中间件 1 退出

当程序运行到await next()的时候就会暂停当前程序,进入下一个中间件,处理完之后才会仔回过头来继续处理。 也就是说,当一个请求进入,#1会被第一个和最后一个经过,#2则是被第二和倒数第二个经过,依次类推。

通过use传进来的中间件是一个回调函数,回调函数的参数是ctx上下文和next,next其实就是控制权的交接棒,next的作用是停止运行当前中间件,将控制权交给下一个中间件, 执行下一个中间件的next()之前的代码,当下一个中间件运行的代码遇到了next(),又会将代码执行权交给下下个中间件,当执行到最后一个中间件的时候,控制权发生反转, 开始回头去执行之前所有中间件中剩下未执行的代码,当最终所有中间件全部执行完后,会返回一个Promise对象,因为我们的compose函数返回的是一个async的函数,async函数执行完后会返回一个Promise。

function _createNext(middleware, oldNext) {
    return async () => {
        await middleware(ctx, oldNext);
    }
}

compose() {
    return async (ctx) => {
        let next = async () => {
            return Promise.resolve();
        };
        for (let i = this.middlewares.length - 1; i >= 0; i--) {
            let currentMiddleware = this.middlewares[i];
            next = _createNext(currentMiddleware, next);
        }
        await next();
    };
}

错误捕获和错误处理

错误处理和捕获,分为中间件的异常Koa框架框架层的异常

前者在application.jscallback方法中,根据es7的规范知道,async返回的是一个promise的对象实例,我们如果想要捕获promise的错误,只需要使用promise的catch方法,就可以把所有的中间件的异常全部捕获到。 类似这样:

return fn(ctx).then(respond).catch(onerror);

后者,通过将koa的构造函数继承events模块,使其具备事件监听on函数和事件触发emit行为的能力。

let EventEmitter = require('events');
class Application extends EventEmitter {}

这样,当我们创建koa实例的时候,就可以加上on监听函数。

let app = new Koa();

app.on('error', err => {
    console.log('error happends: ', err.stack);
});