Koa2.0源码解析-中间件的设计

剖析connect.js的中间件设计,利于咱们更好的理解Koa2.0中间件设计原理。git

1、前言

    首先对于这两个框架最起码要看过文档或者是敲过入门的小demo,由于咱们读源码并不仅是为了装X,更重要的是帮助咱们理解为何要这样用?为何这里会有坑?github

    回忆一下如何用Node建立http服务:数组

const http = require('http')
    const app = http.createServer((req, res) => {
        // 处理一个个http请求
        res.end('hello world')
    })
    
    app.listen(3000)
复制代码

    而这里对于如何优雅的处理每一次请求就成了一个值得思考的问题,而在connect.js中你能够这样处理:bash

const http = require('http')
const connect = require('connect')
const app = connect()

app.use((req, res, next) => {
  // 中间件1
  next()
})

app.use((req, res, next) => {
  // 中间件2
  next()
})

app.use((re, res, next) => {
  // 中间件3
  // 响应结束
  res.end('hello world')
})

http.createServer(app).listen(3000)
复制代码

2、connect中间件原理

    理解connect中间件实现原理,咱们须要从这四个方法入手:app

  • createServer: 如何定义处理请求方法?
  • use: 怎样注册咱们的中间件?
  • handle: 中间件的执行流程是怎样的?
  • call: 执行中间件方法须要注意什么?
一、createServer
function createServer() {
      function app(req, res, next){ app.handle(req, res, next); } 
      merge(app, proto);
      merge(app, EventEmitter.prototype); 
      app.route = '/'; 
      app.stack = []; 
      return app;
    }
复制代码

    createServer是connect的入口方法,它返回一个处理请求的方法,内部再调用handle来处理这些注册的中间件,也就是中间件的处理流程。框架

    connect并无采用构造函数的方式,而将须要用到的属性方法拷贝到app对象上使用,而对于Koa2.x中则是采用ES6的class实现。koa

    这里的route是中间件的默认路由(这里的路由与咱们理解的路由有所差异,后面会提到),stack主要用来存放中间件。async

二、use
function use(route, fn) {
      var handle = fn;
      var path = route;
    
      // 不传入route则默认为'/',这种基本是框架处理参数的一种套路
      if (typeof route !== 'string') {
        handle = route;
        path = '/';
      }
    
      ...
      // 存储中间件
      this.stack.push({ route: path, handle: handle });
      
      // 以便链式调用
      return this;
    }
复制代码

    use方法中的核心就是将用户传入的参数整合成咱们后续要用的layer对象包含路由和执行方法,而且将一个个layer对象存储在stack中,从这里咱们能够猜想出中间件注册的顺序十分重要。函数

三、handle与call

    这里咱们须要将handle与call结合起来理解,它们能够说是connect的灵魂。ui

function handle(req, res, out) {
      var index = 0;
      var stack = this.stack;
      ...
      function next(err) {
        ...
        // 依次取出中间件
        var layer = stack[index++]
    
        // 终止条件
        if (!layer) {
          defer(done, err);
          return;
        }
    
        var path = parseUrl(req).pathname || '/';
        var route = layer.route;
    
        // 路由匹配规则
        if (path.toLowerCase().substr(0, route.length) !== route.toLowerCase()) {
          return next(err);
        }
        ...
        call(layer.handle, route, err, req, res, next);
      }
    
      next();
    }
复制代码

    handle方法的关键点在于经过next方法依次检测当前中间件是否应该执行。而next方法中的路由匹配规则可让咱们清楚的明白这里并非彻底相等的匹配而是一种包含的关系:

app.use('/foo', (req, res, next) => next())
    app.use('/foo/bar', (req, res, next) => next())
复制代码

    因此当你访问/foo/bar路由时,这两个中间件都会执行。

    若是不匹配当前中间件,那么会自动调用next方法将进行下一个中间件的检测。

    当路由匹配无误,那么就会调用call方法来执行当前中间件的处理函数:

function call(handle, route, err, req, res, next) {
      var arity = handle.length;
      var error = err;
      var hasError = Boolean(err);
    
      try {
        if (hasError && arity === 4) {
          // 错误处理中间件
          handle(err, req, res, next);
          return;
        } else if (!hasError && arity < 4) {
          // 请求处理中间件
          handle(req, res, next);
          return;
        }
      } catch (e) {
        // 记录错误
        error = e;
      }
    
      // 将错误传递下去
      next(error);
    }
复制代码

    这里能够看到call内部经过调用try/catch捕获中间件错误,而且经过参数个数和有无错误来决定执行错误处理中间件仍是请求处理中间件,其它的状况则是自动调用next方法去检查下一个中间件。若是try/catch捕获到错误以后,会一直将这个错误传递下去,直到遇到错误处理中间件。

    因此这里咱们能够发现这个handle是有点朴实的,它会一直去检查中间件数组直到数组遍历完或者是next调用链断掉(也就是你在中间件中没有手动调用next),这里咱们能够经过流程图看一下hanle与call的处理过程:

四、小结

    这时咱们能够发现connect的几个特色

  • 当中间件发生错误时,handle函数并非当即进入错误处理状态,而是将错误逐层传递,直到找到错误处理中间件,而且你的错误中间件必须是四个参数;
  • 中间件的执行流程是经过next连接的;
  • 咱们须要手动调用res.end结束响应;
  • 当咱们使用ES8的async方法时,没法捕获到错误。

3、Koa2.0中间件

    Koa2.0中间件的实现与connect中间件原理基本类似,主要区别就在于中间件执行流程上的细节处理。

    首先咱们要知道async函数返回的是一个Promise对象,因此当async内部发生错误,这个Promise对象就会将状态转换为reject。这也是为何try/catch没法捕获它的状态,因此捕获async函数的内部错误,实际上就是Promise对象的错误处理,接下来咱们看Koa2.0中next方法的实现:

function compose (middleware) {
      if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
      for (const fn of middleware) {
        if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
      }
    
      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)
          }
        }
      }
    }
复制代码

    从上述代码中能够看出Koa2.0中间件处理的流程比connect更加简单,首先Koa2.0中没有路由,无需在传递的过程当中匹配路由。

    而咱们经过层层传递Promise对象,造成了一条Promise链,一旦出现reject状态,那么会当即进入catch方法,这也正好解决了connect中须要将错误层层传递到错误中间件的缺点。

    而当咱们调用next方法时,就是调用dispatch.bind(null, i + 1),直白一点,就是:

function next () {
        return dispatch(i + 1)
    }
复制代码

    而对于这条Promise链,Koa2.0中最后这样处理:

fnMiddleware(ctx).then(handleResponse).catch(onerror)
复制代码

    经过handleResponse方法帮助咱们自动调用res.end(),这就是为何在Koa中咱们这样设置返回值:

app.use(ctx => {
      ctx.body = 'Hello Koa'
    })
复制代码

    而且这里经过系统自带的onerror方法帮助咱们处理错误,而且在onerror内部使用:

this.app.emit('error', err, this);
复制代码

    从而为用户提供监听error来集中处理错误的功能。

    从connect到koa2.0,但愿能够帮助你彻底理解中间件的实现原理。

4、写在最后

    这里可能有人不解,难道讲Koa都不提一下洋葱模型吗?其实看到这里,我相信你已经明白next的执行流程实际上就是一个函数递归执行的过程,这也就是为何咱们会用洋葱模型来形容它。


    喜欢本文的小伙伴,能够gay一下或者关注个人订阅号,ε=ε=ε=(~ ̄▽ ̄)~

相关文章
相关标签/搜索