啊啊啊,好久没更博客了。

前言

Koa – 基于 Node.js 平台的下一代 web 开发框架

之前看到阿里的企业级node框架egg.js的发布。egg.js可以说是以koa为底层框架,进行的一次扩展。作为一个曾用过koa2写过几个小项目的前端,为了更好的理解这些node的框架,我开始阅读koa2的源码。

koa2

总的来说,koa2源码主要是的中间件context的属性代理两部分。

中间件

let app=new Koa();

app.use(async (ctx,next)=>{
    ctx.body = 'Hello Koa';
    await next();
})

app.listen(3000);

中间件的使用

这张洋葱图很好的说明了koa2的中间件模式,app.use(fn);fn就是我们说的中间件,一个完整的中间件会有两个参数 ctx、next,ctx上挂载了 Request 和 Response 两个对象,我们可以在任意一个中间件上查看request的内容以及 修改response的内容。next可以认为是后面的中间件,就像那个洋葱图一样我们可以通过

doSomeThingA();
await next();
doSomeThingB();

先做A然后执行其他的中间件之后再执行B。

中间件的实现

//为了讨论的方便,省略了部分代码

class Application extends Emitter {
  constructor() {
    super();
    this.middleware = [];
    //中间件数组
    this.context = Object.create(context);
    this.request = Object.create(request);
    this.response = Object.create(response);

    //...
    //一些其他的配置,暂时可以忽略
  }
}

use(fn) {
    //...
    //fn类型判断等等,暂时忽略

    this.middleware.push(fn);
    return this;
}

listen(...args) {
  //callback返回一个参数为req,res的函数
  const server = http.createServer(this.callback()); 
  return server.listen(...args);
}

callback() {
  //compose为中间件的重点
  const fn = compose(this.middleware);

  if (!this.listeners('error').length) this.on('error', this.onerror);

  const handleRequest = (req, res) => {
    res.statusCode = 404;
    const ctx = this.createContext(req, res);
    const onerror = err => ctx.onerror(err);
    const handleResponse = () => respond(ctx);
    onFinished(res, onerror);
    return fn(ctx).then(handleResponse).catch(onerror);
  };

compose (middleware) {
  //...类型检查

  return function (context, next) {
    // index为调用的最后一个中间件的下标
    let index = -1
    return dispatch(0)
    function dispatch (i) { 
      //通过index和i的大小比较,判断是否多次调用next()
      if (i <= index) return Promise.reject(new Error('next() called multiple times'))
      index = i
      let fn = middleware[i]
      if (i === middleware.length) fn = next
      //递归终点,返回一个Promise.resolve()
      if (!fn) return Promise.resolve()
      try {
        return Promise.resolve(fn(context, function next () {
          return dispatch(i + 1)//递归调用
        }))
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}

总的来说,最巧妙的应该是compose函数,通过递归调用使得中间件像那个洋葱图一般的执行。
同时,我们也可以总结出写koa2中间件的技巧.

  1. 如果我们的中间件fn没有next或没有执行await next()的话,将不会执行后面的中间件,我们可以用来处理异常出错的情况。

    app.use(async(ctx,next)=>{
        try{
            await next();
        }catch(err){
            ctx.body={message:err.message};
            stx.status=err.status||500
        }
    })
    
  2. next()只能在一个中间件中至多出现一次。

context的属性代理

看源码的话,代码里有一个delegate(委托)的模块。
其实,它所达到的目的就是

当我们修改ctx.body=’Hello Koa’的时候可以完成ctx.response.body=’Hello Koa’。
简化了开发者的使用,由于这里有大量的设置get set的代码,熟悉getter和setter的同鞋可能马上就能明白,就不展开说明了(ES5里通过Object.defineProperty来实现)。和这里的知识有关的还有现如今的许多前端框架的数据双向绑定,同时,ES6里还有Reflect、Proxy,有机会再讨论一下。

本文地址: http://www.dzglalala.cn/2017/04/03/koa源码阅读/