Express与Koa源码分析

前言

 对于Express与Koa他们的Api很相似,但是其执行机制还是有所不同的,有时间的同学还是推荐去读一些源码,Express源码感觉比较复杂的,Koa的源码相对比较精简,那么下面我们主要从两个方面去来看一下。

中间件

Express中间件机制

Express 是线性的,那么看一下下面的代码:

const Express = require('express')
const app = new Express();
const sleep = () => new Promise(resolve => setTimeout(function(){
    
    resolve(1)}, 2000))
const port = 3000

function f1(req, res, next) {
    
    
  console.log('f1 start ->');
  next();
  console.log('f1 end <-');
}

function f2(req, res, next) {
    
    
  console.log('f2 start ->');
  next();
  console.log('f2 end <-');
}

async function f3(req, res) {
    
    
  await sleep();
  console.log('f3 service...');
  res.send('Hello World!')
}

app.use(f1);
app.use(f2);
app.use(f3);
app.get('/', f3)
app.listen(port, () => console.log(`Example app listening on port ${
      
      port}!`))

输出结果:

f1 start ->
f2 start ->
f2 end <-
f1 end <-
f3 service...

那么可以得出,Express 中间件实现是基于 Callback 回调函数同步的,它不会去等待异步(Promise)完成,这也解释了为什么加入了异步操作顺序就被改变了。
初始化时的源码如下:

proto.use = function use(fn) {
    
    
  var offset = 0;
  var path = '/';

  // default path to '/'
  // disambiguate router.use([fn])
  if (typeof fn !== 'function') {
    
    
    var arg = fn;

    while (Array.isArray(arg) && arg.length !== 0) {
    
    
      arg = arg[0];
    }

    // first arg is the path
    if (typeof arg !== 'function') {
    
    
      offset = 1;
      path = fn;
    }
  }
  // 定义callbacks
  var callbacks = flatten(slice.call(arguments, offset));

  if (callbacks.length === 0) {
    
    
    throw new TypeError('Router.use() requires a middleware function')
  }

  // 依次读取callbacks数组中的函数
  for (var i = 0; i < callbacks.length; i++) {
    
    
    var fn = callbacks[i];

    if (typeof fn !== 'function') {
    
    
      throw new TypeError('Router.use() requires a middleware function but got a ' + gettype(fn))
    }

    // add the middleware
    debug('use %o %s', path, fn.name || '<anonymous>')

    // 将取出的fn函数,传入到Layer中,进行new一个实例
    var layer = new Layer(path, {
    
    
      sensitive: this.caseSensitive,
      strict: false,
      end: false
    }, fn);

    layer.route = undefined;

    // 将layer压入到stack栈中
    this.stack.push(layer);
};

对于Express中源码的执行是在proto.handle中执行的,我们分析下源码:

proto.handle = function handle(req, res, out) {
    
    
  var self = this;
  ...
  next();

  function next(err) {
    
    
    ...
    // find next matching layer
    var layer;
    var match;
    var route;

    while (match !== true && idx < stack.length) {
    
    
      layer = stack[idx++]; // 取出中间件函数
      match = matchLayer(layer, path);
      route = layer.route;

      if (typeof match !== 'boolean') {
    
    
        // hold on to layerError
        layerError = layerError || match;
      }

      if (match !== true) {
    
    
        continue;
      }

      if (!route) {
    
    
        // process non-route handlers normally
        continue;
      }

      ...
    }
    
    ...
    // this should be done for the layer
    self.process_params(layer, paramcalled, req, res, function (err) {
    
    
      if (err) {
    
    
        return next(layerError || err);
      }

      if (route) {
    
    
        return layer.handle_request(req, res, next);
      }
      
      trim_prefix(layer, layerError, layerPath, path);
    });
  }
  
  function trim_prefix(layer, layerError, layerPath, path) {
    
    
    ...
    if (layerError) {
    
    
      layer.handle_error(layerError, req, res, next);
    } else {
    
    
      // 这里进行函数调用,且递归
      layer.handle_request(req, res, next);
    }
  }
};

proto.handle 方法的核心实现定义了 next 函数递归调用取出需要执行的中间件,因为没有promise等可以支持异步操作的,所以express的中间件不会等待异步的操作。

Koa中间件机制

下面我们看一下中间件是如何挂载的,当我们调用use函数去注册中间件的时候,此时会将对应的函数加入到middleware这个数组中,然后在callback这个函数中使用compose传入这个middleware。callback这个函数是在listen的这个函数进行调用的,将this.callback传入到http.createServer的这个函数中。下面我们看下源码:

  // 开启服务
  listen (...args) {
    
    
    debug('listen')
    const server = http.createServer(this.callback())
    return server.listen(...args)
  }
  // 注册中间件
  use (fn) {
    
    
    if (typeof fn !== 'function') throw new TypeError('middleware must be a function!')
    debug('use %s', fn._name || fn.name || '-')
    // 将函数加入到中间件数组中
    this.middleware.push(fn)
    return this
  }

  /**
   * Return a request handler callback
   * for node's native http server.
   *
   * @return {Function}
   * @api public
   */

  callback () {
    
    
  	// 将这个中间件数组使用this.compose,实际是使用了koa-compose这个库
    const fn = this.compose(this.middleware)

    if (!this.listenerCount('error')) this.on('error', this.onerror)

    const handleRequest = (req, res) => {
    
    
      const ctx = this.createContext(req, res)
      return this.handleRequest(ctx, fn)
    }

    return handleRequest
  }

那么我们主要去研究下中间件的机制,所以我们看一下koa-compose的机制,源码如下:

源码地址:https://github.com/koajs/compose/blob/master/index.js


module.exports = compose

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!')
  }

  /**
   * @param {Object} context
   * @return {Promise}
   * @api public
   */
  return function (context, next) {
    
    
    // last called middleware #
    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)
      }
    }
  }
}

 通过此段源码分析到,每次中间件调用的next(),实际是调用的是dispatch.bind(null, i + 1),利用闭包和递归的性质,一个个执行,并且每次执行都是返回promise,所以这也就是我们为什么可以在中间件中使用异步了。

总结

 Express的中间件机制是基于Callback回调函数同步的,他不会等待异步操作的完成,Koa的中间件中的next返回的是一个Promise,我们可以通过async和await用同步的方式去写异步操作。

猜你喜欢

转载自blog.csdn.net/liu19721018/article/details/125676749