超详细的《Express》核心原理解析(上)

1,455

你好,我是愣锤。

欢迎走进Express源码的世界。express.js是一款基于 Node.js 平台,快速、开放、极简的 Web 开发框架。本文将基于4.17.3的版本详细讲解Express核心原理的实现,包括主体架构以及最核心的中间件架构模型讲解。

作为相对底层的Web服务框架,Express比其他底层框架来说,在功能模型上相对复杂了一些提供的语法功能也比较多,其中间件架构模型的实现也比其他的服务框架复杂了一些。但是也不用担心,通过本文的阅读,一定可以对其原理实现有不错的掌握。还是老规矩,先看下基本的使用吧:

const Express = require('express');

const app = new Express();

app.get('/', (req, res) => {
  res.send('Hello Express!')
});

app.listen(3000, () => {
  console.log('express server running at port 3000.');
});

Express在使用上也还算是比较简单的,就是初始化一个应用,然后添加中间件调用,最后监听端口启动服务。知道了怎么用之后,下面我们看在上述的这个基本的DEMO背后,Express做了什么事情吧。

Express暴露的API解析

通过源码根目录下的index.js暴露的内容得知,express的入口文件在./lib/express中,lib/express的基本结构如下:

exports = module.exports = createApplication;

/**
 * 创建express应用
 *
 * @return {Function}
 * @api public
 */
function createApplication() {
  // ...
}

/**
 * 导出一些原型对象
 */
exports.application = proto;
exports.request = req;
exports.response = res;

/**
 * 导出一些构造函数
 */
exports.Route = Route;
exports.Router = Router;

/**
 * 导出一些中间件
 */
exports.json = bodyParser.json
exports.query = require('./middleware/query');
exports.raw = bodyParser.raw
exports.static = require('serve-static');
exports.text = bodyParser.text
exports.urlencoded = bodyParser.urlencoded

从上述可以看到express主要是对外暴露一个createApplication函数用于创建express应用,同时暴露出内置的路由对象、一些中间件等。express对外暴露的内容下图所示:

image.png

4xexpress一些内置的中间件已经改变或移除,因此对于旧的中间件使用则会给出提示报错,让使用者自行添加或修改中间件。如下所示:

/**
 * 已经被移除的对外暴露的api列表,
 * 当尝试方法已经移除的api时给适当的错误提示
 */
var removedMiddlewares = [
  'bodyParser',
  'compress',
  'cookieSession',
  'session',
  'logger',
  'cookieParser',
  'favicon',
  'responseTime',
  'errorHandler',
  'timeout',
  'methodOverride',
  'vhost',
  'csrf',
  'directory',
  'limit',
  'multipart',
  'staticCache'
]
removedMiddlewares.forEach(function (name) {
  Object.defineProperty(exports, name, {
    get: function () {
      throw new Error('Most middleware (like ' + name + ') is no longer bundled with Express and must be installed separately. Please see https://github.com/senchalabs/connect#middleware.');
    },
    configurable: true
  });
});

createApplication实现

对于express有两种不同的用法,其一是把它当作一个http服务使用,如下所示:

const app = express();

app.listen(3000, () => {});

其二是把它当作一个已有的http/https服务的中间件模型使用,也就是此时的express仅仅承担一个已有服务的扩展功能,使已有服务支持中间件模型。使用例子如下所示:

const http = require('http');

const app = express();
app.use(middlewareA);
app.use(middlewareB);

const server = http.createServer(app);

server.listen(3000, () => {});

image.png

通过导出的内容可以看到,express()的背后就是调用的createApplication()函数,那么createApplication背后做了什么呢?是如何支持两种不同的使用模式呢?接下来我们带着问题看下createApplication的源码实现:

/**
 * 创建express应用
 *
 * @return {Function}
 * @api public
 */
function createApplication() {
  /**
   * app被设计成一个兼容http/https服务callback格式的函数
   *
   * - 把express作为http服务使用
   *   const app = express();
   *   app.listen(3000, () => {});
   *
   * - 把express作为http服务的callback使用
   *   const http = require('http');
   *   const app = express();
   *   const server = http.createServer(app);
   *   server.listen(3000, () => {});
   */
  var app = function(req, res, next) {
    app.handle(req, res, next);
  };

  // 通过mixin的方式 “继承” EventEmitter
  mixin(app, EventEmitter.prototype, false);
  /**
   * 通过mixin的方式 “继承” application.js
   * 从而将express的真正实现抽离到application.js中
   */
  mixin(app, proto, false);

  /**
   * 给app上挂载request和response对象
   * request和response对象上分别添加一个指向app的引用
   */
  app.request = Object.create(req, {
    app: { configurable: true, enumerable: true, writable: true, value: app }
  })
  app.response = Object.create(res, {
    app: { configurable: true, enumerable: true, writable: true, value: app }
  })

  // 调用init方法完成默认配置的初始化
  app.init();

  // 返回app
  return app;
}

我们看到createApplication函数内容主要逻辑是:

  • 创建了一个app函数并并返回app函数。
  • 通过mixin来 “继承” events触发器的功能,就是支持发布订阅功能。
  • 通过mixin的方式将所有功能的时候抽离到application.js
  • app函数上挂载requestresponse对象
  • 最后调用app.init();进行初始化参数

首先需要注意的是app函数的入参是按照中间件函数的格式定义的。这时候要先了解下中间件的编写要求了。引用一个官方的例子:

image.png

中间件是要求接收三个参数,第一个参数是网络请求对象,第二个参数是网络响应对象,第三个参数next是个函数,被调用后将执行下一个中间件。

这里的中间件妙在和http/https服务的callback是兼容的,例如看下http服务的callback是怎样的:

const http = require('http');

const server = http.createServer((req, res) => {
  res.end('Hello Http Server!');
});

server.listen(3000, () => {});

此时我们再回到app函数的实现,可以看到app函数内就是app.handle(req, res, next);app.handle的作用就是调用中间件,稍后会详细讲解。此时便不难理解为什么express可以给已有http/https服务扩展中间件能力了。因为把express实例作为已有http/https服务的callback使用时,callback被调用时实际上调用就是上述的app呀,也就是调用的app.handle来执行已有的中间件。

image.png

现在我们思考下express是如何独立作为http服务的呢?这时候我先回忆下,我们把express独立使用时,是需要调用express实例的listen方法来监听端口然后启动服务的:

const app = express();

app.listen(3000, () => {
  console.log('[express] server running at port 3000.');
});

所以我们要看下app上listen方法的实现。listen方法是在上述mixin的时候从application.js中导出的对象上拷贝到app上的,因此其实现是在application.js中。如下所示的listen的代码实现:

/**
 * Application prototype.
 *
 * 可以简单理解为是express的原型对象的实现
 */
var app = exports = module.exports = {};

/**
 * 监听连接
 *
 * @return {http.Server} 返回node的http.Server服务
 * @public
 */
app.listen = function listen() {
  var server = http.createServer(this);
  return server.listen.apply(server, arguments);
};

listen方法自动帮我们创建了一个http服务,并把当前express实例作为服务的callback,因此在callback调用时也是通过之前讲到的app.handle执行的中间件。

要注意的是为什么这里的this指代的是当前express实例,因为虽然listen方法是在application.js中的导出的app对象上实现的,但是createApplication内通过mixinlisten方法拷贝到了createApplication内的app上,可以理解为listen方法就是在createApplication内的app上实现的。因此此处的this指向的当前express实例。

最后,提一下mixin做了什么?mixin其实是引用的merge-descriptors库,该库的作用是拷贝目标对象所有属性的descriptors来实现merge的。其源码核心实现如下所示:

// 
function merge (dest, src, redefine) {
  Object.getOwnPropertyNames(src).forEach(function forEachOwnPropertyName (name) {
    // 如果属性已存在且不允许覆盖,则不拷贝
    if (!redefine && hasOwnProperty.call(dest, name)) {
      return;
    }

    // 拷贝 descriptor
    var descriptor = Object.getOwnPropertyDescriptor(src, name);
    Object.defineProperty(dest, name, descriptor);
  });

  return dest;
}

通过descriptor方式的拷贝和普通的拷贝有什么区别呢?看下面的例子就知道了:

const mergeDescriptor = require('merge-descriptors');

const obj = { a: 0 }

Object.defineProperty(obj, 'a', {
  value: 1,
  writable: false,
});

function merge(dest, src) {
  for (let key in src) {
    if (Object.prototype.hasOwnProperty.call(src, key)) {
      dest[key] = src[key];
    }
  }
  return dest;
}

function logDescriptor(obj, key) {
  console.log(Object.getOwnPropertyDescriptor(obj, key));
}

// {a: 1}
console.log(merge({}, obj));
// {a: 1}
console.log(mergeDescriptor({}, obj));
// { value: 1, writable: true, enumerable: true, configurable: true }
logDescriptor(merge({}, obj), 'a');
// { value: 1, writable: false, enumerable: true, configurable: true }
logDescriptor(mergeDescriptor({}, obj), 'a');

可以看到最主要的一个区别在于普通的merge无法拷贝已修改的descriptor相关属性,只拷贝了值。这里就不过多赘述了。

总结一下:

  • express在初始化的时候并没有做太多的事情,仅仅支持了两种的使用模式和挂载请求对象。
  • 中间件等逻辑并没有在一开始进行初始化,相关的功能都在真正使用的时候才会lazy加载的,这部分源码实现会在下面讲解。
  • express整体的代码结构有着很好的分层和模块化思想,但是代码的组织没有真正的OOP范式,而是利用函数本质是对象进行扩展函数功能的伪OOP的感觉。这里如果利用传统JSOOP方式舒服一些:
var http = require('http');
var req = require('./request');
var res = require('./response');

function Express() {
  if (!(this instanceof Express)) {
    return new Express();
  }
  // ... 省略部分实现
  this.request = Object.create(req, {});
  this.response = Object.create(res, {});
}

Express.prototype.listen = function() {
  return http.createServer(this).listen.apply(server, arguments);
}

Express.prototype.callback = function() {
  return function handler(req, res, next) {
    this.handle(req, res, next);
  }
}

// 把Express作为已有服务的callback使用
var app = new Express();
var server = http.createServer(app.callback());
server.listen(3000);

// 把Express单独作为http服务
const app2 = new Express();
app2.listen(3200);

app.use()添加中间件原理

express很核心的一个功能模型就是中间件,express本身更多的是实现了一个中间件架构模型,很多功能都是通过添加中间件来扩展的。使用中间件的方式也很简单,就是通过app.use()进行添加:

const express = require('express');

const app = express();

// 添加全局的中间件
app.use((req, res) => {
  console.log('use a global middleware.');
  next();
});

// 添加局部的中间件
app.use('/admin', (req, res, next) => {
  console.log('middleware with target path');
  next();
});

app.listen(3000, () => {
});

app.use所依赖的中间件模型并没有在express初始化时创建,而是在使用app.use时才lazy式的初始化。在分析app.use的源码实现之前,我们先看一张相关的代码结构组织图:

image.png

上图梳理app.use所依赖的各个模块的依赖图,从图中可以看到app.use是在application.js文件中定义的。下面直接看app.use的源码实现:

/**
 * app.use()添加中间件
 * 最终是代理到router.use()添加中间件
 * 这部分的使用文档可以查阅官网的`Router#use()`部分
 *
 * 如果fn参数是一个express应用(而非中间件),则会被挂载在指定的_route_上
 * @public
 */
app.use = function use(fn) {
  var offset = 0;
  var path = '/';

  // default path to '/'
  // disambiguate app.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;
    }
  }

  // 获取所有的中间件函数
  var fns = flatten(slice.call(arguments, offset));

  // app.use()没有传入中间件时给出报错
  if (fns.length === 0) {
    throw new TypeError('app.use() requires a middleware function')
  }

  // lazy路由,仅在没初始化过路由时才初始化路由
  this.lazyrouter();
  // 路由用于添加管理中间件等
  var router = this._router;

  fns.forEach(function (fn) {
    /**
     * 处理fn是中间件而不是express应用的情况
     * 注意的是express是支持主子应用的
     * app实例是包含handle方法和set方法的,这里利用了鸭式辨型的思想
     */
    if (!fn || !fn.handle || !fn.set) {
      // 将中间件添加到路由router的中管理
      return router.use(path, fn);
    }

    debug('.use app under %s', path);

    // 处理挂载的是express应用的情况
    fn.mountpath = path;
    fn.parent = this;

    // restore .app property on req and res
    router.use(path, function mounted_app(req, res, next) {
      var orig = req.app;
      fn.handle(req, res, function (err) {
        setPrototypeOf(req, orig.request)
        setPrototypeOf(res, orig.response)
        next(err);
      });
    });

    // 触发一个子应用挂载的事件
    fn.emit('mount', this);
  }, this);

  // 让app.use支持链式调用
  return this;
};
  • app.use中首先根据参数情况,获取中间件以及中间件生效的path路径
  • 利用this.lazyrouter();进行初始化路由对象router
  • 迭代传入的中间件,判断是中间件还是express应用
    • 是中间件,则交由router.use新增中间件
    • express应用,则指明父应用和挂载的路径等,同时触发一个子应用挂载的事件。判断是express子应用的逻辑是只有该fn参数包含handleset属性则认为是express应用,利用的鸭式辨型的思想。

可以看到app.use本身是没有处理中间件逻辑的,只是处理了参数和区分中间件与子应用,最终中间件的处理还是代理到Router类了。这里逻辑如下图所示:

image.png

router对象也是通过手动调用this.lazyrouter进行lazy式的创建的。lazyrouter方法的实现也是在application.js中,我们看下源码实现:

/**
 * 如果尚未添加路由,则初始化路由
 *
 * 注意:路由没在defaultConfiguration时初始化,
 * 原因是会读取这些默认,但是默认配置可能会在程序启动后改变
 *
 * @private
 */
app.lazyrouter = function lazyrouter() {
  // router尚未被初始化则创建router
  if (!this._router) {
    /**
     * 实例化路由类,用于管理路由中间件(中间件架构就在该类中实现)
     *
     * Router类的参数最终传递给path-to-regexp库
     * - caseSensitive用于指定解析url参数时是否忽略大小写
     * - strict用于指定解析url参数时是否允许匹配结尾的分界符
     */
    this._router = new Router({
      caseSensitive: this.enabled('case sensitive routing'),
      strict: this.enabled('strict routing')
    });

    /**
     * 默认添加query中间件和expressInit中间件
     * - query中间件作用是解析url中的query参数为键值对集合
     * - middleware.init中间件作用是对res和req对象做一些初始化配置和挂载一些引用
     *
     * 注意:this.get的实现是在methods.forEach()逻辑中,该点比较隐晦,
     * this.get实际触发的是this.set('query parser fn')得到的get效果
     */
    this._router.use(query(this.get('query parser fn')));
    this._router.use(middleware.init(this));
  }
};

Router实例化之后会在app上挂载一个_router属性,因此也是判断当_router不存在的时候才创建。Router本身的作用是创建管理中间件,这里在实例化之后,默认添加了两个中间件:

  • query 中间件解析url中的query参数为键值对集合
// 文件在lib/middleware/query.js
/**
 * query解析中间件
 * 将url中的query参数解析成key/value键值对集合
 *
 * @param {Object} options
 * @return {Function}
 * @api public
 */
module.exports = function query(options) {
  var opts = merge({}, options)
  var queryparse = qs.parse;

  // 如果用户自定义了query解析函数,则优先使用用户传入的解析函数
  if (typeof options === 'function') {
    queryparse = options;
    opts = undefined;
  }

  if (opts !== undefined && opts.allowPrototypes === undefined) {
    // back-compat for qs module
    opts.allowPrototypes = true;
  }

  // 返回query中间件函数
  return function query(req, res, next){
    /**
     * 如果req请求对象中已经包含了query字段,则该中间件不做任何处理,
     * 说明此时req对象已经被处理过了,此时便把req的query控制权交与使用者
     */
    if (!req.query) {
      // 否则,使用parseurl和qs库解析url中的查询参数解析成键值对
      var val = parseUrl(req).query;
      req.query = queryparse(val, opts);
    }

    next();
  };
};
  • middleware.init中间件对resreq对象做一些初始化配置和挂载一些引用
// 文件在lib/middleware/init.js
/**
 * 初始化中间件,将req和res互相暴露给对象,
 * 并添加一个非标准的X-Powered-By响应头
 *
 * @param {Function} app
 * @return {Function}
 * @api private
 */
exports.init = function(app){
  return function expressInit(req, res, next){
    // 设置非标准的X-powered-by响应头为Express
    if (app.enabled('x-powered-by')) res.setHeader('X-Powered-By', 'Express');
    // req增加res引用
    req.res = res;
    // res增加req引用
    res.req = req;
    req.next = next;

    /**
     * 扩展req和res对象,支持定义在request.js和response.js中的所有功能,
     * 做法是:
     *  - 设置req的原型对象为app.requset
     *  - 设置res的原型对象为app.response
     * 需要注意的是:虽然修改了req和res的原型对象,但是req和res并未丢失原来的原型对象,
     * 原因是在app.request和app.response的实现中是基于正确的原型对象创建的:
     * - var req = Object.create(http.IncomingMessage.prototype)
     * - var res = Object.create(http.ServerResponse.prototype)
     */
    setPrototypeOf(req, app.request)
    setPrototypeOf(res, app.response)

    res.locals = res.locals || Object.create(null);

    next();
  };
};

下面我们看下Router类的源码实现,Router的构造函数和express有些类似,都是定义一个兼容中间件函数格式的函数并返回,因为router在业务代码中是可以单独导入使用的。这里最主要的是定一个了stack属性,用于存放所有的中间件。下面代码中省略了大部分的参数处理代码:

/**
 * 根据options参数初始化路由对象
 * Router类主要用于管理路由,比如路由对应的中间件管理和执行
 */
var proto = module.exports = function(options) {
  var opts = options || {};

  function router(req, res, next) {
    router.handle(req, res, next);
  }

  // 省略部分参数...

  // 存放中间件的集合
  router.stack = [];

  return router;
};


/**
 * use方法添加中间件
 */
proto.use = function use(fn) {
  var offset = 0;
  var path = '/';

  // 省略部分参数的处理,和app.use部分有些类似...

  // 获取path对应的所有中间件
  var callbacks = flatten(slice.call(arguments, offset));

  // 遍历app.use时添加的url对应的中间件集合
  for (var i = 0; i < callbacks.length; i++) {
    var fn = callbacks[i];

    // 对中间件使用Layer类进行包裹一层
    var layer = new Layer(path, {
      sensitive: this.caseSensitive,
      strict: false,
      end: false
    }, fn);

    // layer.route用于标记该layer(中间件)是否是路由处理程序
    layer.route = undefined;

    // 将包裹后的中间件添加到中间件栈中(数据结构本质是队列)
    this.stack.push(layer);
  }

  // 返回this,支持链式调用
  return this;
};

router.use的实现中,对中间件使用了Layer类进行了一层包裹之后才添加到stack集合中的。下面我们看Layer做了哪些事情:

/**
 * 对中间件进行一次包裹,
 * 添加path、handle等属性,并对path的动态参数进行解析
 */
function Layer(path, options, fn) {
  if (!(this instanceof Layer)) {
    return new Layer(path, options, fn);
  }

  debug('new %o', path)
  var opts = options || {};

  this.handle = fn;
  this.name = fn.name || '<anonymous>';
  this.params = undefined;
  this.path = undefined;
  /**
   * 通过path-to-regexp库解析path中的路由参数
   * 将解析后的参数存放在this.keys中
   */
  this.regexp = pathRegexp(path, this.keys = [], opts);

  // set fast path flags
  this.regexp.fast_star = path === '*'
  this.regexp.fast_slash = path === '/' && opts.end === false
}

Layer类的主要目的就是对中间件进行一次包裹,包裹成统一的数据格式,并且对path的动态参数部分利用path-to-regexp解析成键值对集合,方便后续使用。有关path-to-regexp的使用和源码解析可以翻阅我的这篇博文 “面试官:Vue-Router是如何解析URL路由参数的?小明:卒......”。

app[method]()添加中间件原理

除了app.use()放使用中间件外,我们还可以通过app[method]()的方式添加路由中间件,例如下面的使用方式:

const app = express();

// 针对指定path的路由中间件
app.get('/api/v1/user/:id', function() {});
app.post('/api/v1/user', function() {});

app[method]()的原理究竟是怎样实现的,它又和app.use()的实现方式有什么区别呢?接下来我们首先找到app[method]()的源码位置,在application.js中有这样一段代码:

var methods = require('methods');

/**
 * Delegate `.VERB(...)` calls to `router.VERB(...)`.
 * 将app[method]调用委托到router.route上调用
 */
methods.forEach(function(method){
  app[method] = function(path){
    // 如果是app.get调用且参数只有一个,则作为获取配置方法使用
    if (method === 'get' && arguments.length === 1) {
      // app.get(setting)
      return this.set(path);
    }

    this.lazyrouter();

    // 调用router.route方法获取route对象
    var route = this._router.route(path);
    // 将app[method]方法调用委托到route[method]
    route[method].apply(route, slice.call(arguments, 1));
    return this;
  };
});

这就意味着application.js文件加载时通过一个循环给app对象上挂载了get、post、put、delete等所有的请求方法,methods库的原理就是通过http.METHODS获取所有的请求方法名称的,同时兼容了Node.js0.10版本,有兴趣的可以翻着看看。

app[method]的方法内部,主要做了如下几件事情:

  • 如果是get方法且参数只有一个,则把app.get当作获取配置参数的方法使用
  • 通过调用this.lazyrouter();确保router对象被加载
  • 调用router对象的route方法创建一个route对象用于当前app[method]行为的委托
  • 最后app[method]实际调用的是route[method],完成了委托行为

image.png

接下来我们看this._router.route(path);是如何通过调用routerroute方法来创建route对象的:

var Route = require('./route');

/**
 * 根据指定的path创建一个新的Route对象
 * 每个route包含一个独立的中间件存储栈
 *
 * @param {String} path
 * @return {Route}
 * @public
 */
proto.route = function route(path) {
  // 初始化一个route对象
  var route = new Route(path);

  /**
   * 初始化一个Layer对象包裹中间件
   * 该Layer绑定的中间件是route.dispatch
   */
  var layer = new Layer(path, {
    sensitive: this.caseSensitive,
    strict: this.strict,
    end: true
  }, route.dispatch.bind(route));

  // 在layer上添加route属性指向route对象
  layer.route = route;

  // 将layer添加到router的栈中
  this.stack.push(layer);

  // 返回的是route
  return route;
};

这里可以看到也是首先是实例化Route对象,然后实例化Layer对象并把route对象的dispatch方法作为layer绑定的中间件,并在得到的layer对象对添加route属性指向route对象,接着后将layer对象添加到router的栈中,最后返回route对象。逻辑图如下:

image.png

但是这里有两点要特被注意:

  • 第一点是app[method]的过程创建了layer并添加到了router的栈中,app.use也创建了layer并添加到了router的栈中。这样意味着不管是什么类型的中间件都会创建layer并添加到router栈中

  • 第二点是layer对象上挂载的rotue属性是有值的,指向的就是route对象,而app.use()过程中创建的layer对象route属性是undefined,这就在router栈中区别开了是app.use添加的layer还是app[method]添加的layer

  • 第三点是创建layer对象时把route对象的dispatch方法绑定为了layer的中间件函数,这里了先粗略提一下原因,app[method]借助route[method]将中间件添加到route独立的栈中,route对象通过dispatch执行所有的中间件。

接下来我们看Route类是什么,它做了那些事情?Route类的源码实现均在lib/router/route.js中:

/**
 * 根据指定的path初始化Route
 *
 * @param {String} path
 * @public
 */
function Route(path) {
  this.path = path;
  this.stack = [];

  debug('new %o', path)

  // route handlers for various http methods
  this.methods = {};
}

可以看到Route类其实就是维护了一个独立的stack栈,用于存放属于该route的所有中间件。

但是通过上述我们知道,Route类上是定义了get、post等一系列方法的,这部分是如何实现的呢?在该文件的最后有这样一段源码:

var methods = require('methods');

/**
 * 给Route类定义所有的请求方法
 * 例如:route.get、route.post等
 */
methods.forEach(function(method){
  Route.prototype[method] = function(){
    var handles = flatten(slice.call(arguments));

    // 迭代app[method](path, [...middleware])中所有的中间件添加到route的栈中
    for (var i = 0; i < handles.length; i++) {
      var handle = handles[i];

      // 省略部分参数校验代码

      /**
       * app[method]的调用本质是委托的route[method]
       * 注意:app[method](path, [...middlewares])中所有的中间件都是对该path生效的
       * 所以此处的Layer的path是'/'
       */
      var layer = Layer('/', {}, handle);
      layer.method = method;

      this.methods[method] = true;
      // 将中间件包裹成layer添加到route的栈中
      this.stack.push(layer);
    }

    // 返回this支持链式调用
    return this;
  };
});

这里也是一个循环往Route原型上挂载了所有的方法,在方法的实现中遍历了所有的传入参数(注意参数都是中间件),然后对每一个中间件调Layer进行包裹添加到route自己独立的栈中。

因此我们可以梳理出app[method]添加中间件的流程图如下所示:

image.png

express中间件总结

  • 中间件栈模型总结

通过上述中间件的添加逻辑我们可以知道,不管是路由中间件(app[method]得到的layer<route>)还是非路由中间件(app.use得到的layer(middleware)),都是会使用Layer类包裹并添加到router对象(app._router指向的Router实例)的栈(stack属性)中,只不过区别在于路由中间件的处理程序是交由route对象管理的,因此中间件栈的模型如下图所示:

image.png

  • router和route的区别

router对象是在实例化express应用时创建一个,在一个是唯一的,如果创建子应用则在子应用内也会创建一个router对象,在每个应用内添加的中间件都会存入隶属于当前应用的router实例的stack栈中。

route对象是在创建路由中间件时,根据当前路由path实例化的,它也有一个独立的stack栈存储当前路由path对应的多个中间件。但是创建路由中间件时会首先在router对象的栈中添加一个layer,只不过该layer的中间件运行后实际是调用对应route对象维护的所有中间件。

route对象在express实例内可以存在多个,个数由路由中间件创面的次数决定。

中间件调用原理

众所周知express中间件执行逻辑其实就是当接收到请求时依次执行所有匹配的中间件。但是具体的执行逻辑还是有些复杂的,要处理的情况很多,下面先放出来梳理的逻辑图,有了整体的概念之后再理解源码会更顺畅一些:

image.png image.png

app.listen中的实现我们得知,express应用在接收到请求之后触发的是app函数调用,而app函数内部又是调用的app.handle,代码如下:

var app = function(req, res, next) {
  app.handle(req, res, next);
};
  
app.listen = function listen() {
  // // 此处的this就是app函数
  var server = http.createServer(this);
  return server.listen.apply(server, arguments);
};

下面我们看application.jsapp.handle的处理逻辑:

/**
 * 将req和res分发给应用,进行管道式的处理
 *
 * 如果没有calllback提供,那么默认使用finalhandler作为错误处理程序,
 * 并根据中间件栈中出现的错误进行response响应
 * @private
 */
app.handle = function handle(req, res, callback) {
  var router = this._router;

  /**
   * final处理程序
   * 利用finalhandler处理final的情况
   */
  var done = callback || finalhandler(req, res, {
    env: this.get('env'),
    onerror: logerror.bind(this)
  });

  /**
   * 处理空路由情况,路由没有创建则直接给出报错
   * EG:没有使用任何中间件或者路由,则直接响应一个404的Cannot GET的HTML内容
   */
  if (!router) {
    debug('no routes defined on app');
    done();
    return;
  }

  // 将请求交由router系统处理
  router.handle(req, res, done);
};

这里只是做的执行中间件的前置操作:

  • 自定义默认的错误处理程序,因为需要在中间件执行错误或者没有任何匹配的中间件时响应默认的404500
  • 如果此时还没有路由对象的实例,说明没有注册中间件,则无法进行处理,直接调用done响应404
  • 最后将中间件调用执行的逻辑委托给router对象执行

想了解finalhandler的用法源码分析的可以查看我的这篇文章《详解《finalhandler》源码中NodeJs服务错误响应原理》

有一点必须要提一下有利于后续内容的理解,finalhandler是根据响应码、错误状态码等值进行不同的响应内容,但是在finalhandler之前如果已经有响应了,则finalhandler不会做任何处理。

接下来,我们看router.handle做了什么事情,下面代码省略部分的参数边界情况的判断,只看核心的中间件部分:

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

  // middleware and routes
  var stack = self.stack;

  /**
   * restore利用闭包缓存req上baseUrl、next、params的原始值
   * restore返回的done作用执行后作用就是就是恢复原始值,并调用out,即开始调用下个中间件
   */
  var done = restore(out, req, 'baseUrl', 'next', 'params');
  
  // 在req上挂载调用下一个中间件的引用
  req.next = next;

  next();
  
  function next() {
    // ... 先省略next实现
  }
}

首先可以看到主体逻辑就是获取存放所有中间件layer的栈,然后调用next开始执行中间件。下面我们看next内部做了什么事情,下面代码也省略了部分参数的处理情况,只关心核心的中间件部分:

function next(err) {
    var layerError = err === 'route'
      ? null
      : err;

    // 如果错误是'router'则直接done响应404
    if (layerError === 'router') {
      setImmediate(done, null)
      return
    }

    /**
     * 迭代到最后一个中间件了,此时直接done
     * 注意:done内部执行的是finalhandler进行兜底的404返回,
     * 那如果在此前面的中间件已经调用过res.end怎么办?
     * 其实不用担心,因为finalhandler源码中已经判断如果已有响应请求发出则不再继续404响应
     */
    if (idx >= stack.length) {
      setImmediate(done, layerError);
      return;
    }

    // 获取请求的pathname
    var path = getPathname(req);

    // 无效的请求路径时直接done
    if (path == null) {
      return done(layerError);
    }

    var layer; // 当前包裹中间件的layer
    var match; // 当前中间件是否和请求路径匹配
    var route; // 是否是路由中间件,指向路由中间件的引用

    // 依次迭代栈中的layer<middleware>
    while (match !== true && idx < stack.length) {
      layer = stack[idx++];
      // 判断当前中间件是否和path匹配
      match = matchLayer(layer, path);
      route = layer.route;

      // match不是布尔类型时,说明matchLayer解析出错了
      if (typeof match !== 'boolean') {
        /**
         * 如果之前的中间件已经有错误产生了,则依旧使用之前的错误,否则使用matchLayer的错误
         * 此举保证将第一个出错的layer产生的错误传递到最后返回
         */
        layerError = layerError || match;
        // 其实这里直接continue就可以了
      }

      // 当前path和layer(中间件)不匹配,则跳过当前layer
      if (match !== true) {
        continue;
      }

      /**
       * 如果是非路由中间件则直接跳出while循环开始后续的调用逻辑,
       * 因为非路由中间件不需要检查方法类型
       * 下文都是针对路由中间件的方法类型是否匹配的判断
       */
      if (!route) {
        continue;
      }

      /**
       * 如果存在错误,则跳过当前中间件
       * 因为一旦产生错误,后续的中间件都不需要执行了,而是一直把错误往后传递,
       * 这样就要求错误处理中间件必须在最后一个中间件
       */
      if (layerError) {
        // routes do not match with a pending error
        match = false;
        continue;
      }

      /**
       * 下面一小段逻辑主要是处理当前路由中间件是否能匹配上实际的请求方法,
       * 如果无法匹配则跳到下一个中间件
       * 例如,req.method是get,但是只定义了app.post的中间件,那么是无法匹配的
       */
      var method = req.method;
      var has_method = route._handles_method(method);

      // build up automatic options response
      if (!has_method && method === 'OPTIONS') {
        appendMethods(options, route._options());
      }

      // don't even bother matching route
      if (!has_method && method !== 'HEAD') {
        match = false;
        continue;
      }
    }

    /**
     * 整个while下来之后match不是true,说明没有任何匹配的layer,
     * 没有任何layer与path能匹配,则直接结束本次请求
     */
    if (match !== true) {
      return done(layerError);
    }

    // this should be done for the layer
    self.process_params(layer, paramcalled, req, res, function (err) {
      /**
       * 如果产生错误了,则直接next往后跑,并将错误传递下去
       */
      if (err) {
        return next(layerError || err);
      }

      /**
       * 如果是路由中间件,则直接调用路由中间件
       * 这里直接调用就可以的原因是:
       *  - 路由中间件只是桥接,其真正的中间件在route对象的stack中
       *  - 这里layer.handle_request调用中间件也只不过是触发的route.dispatch()
       *  - 调用route.dispatch()后才是依次执行route.stack中的中间件
       */
      if (route) {
        return layer.handle_request(req, res, next);
      }

      // 校验参数后再调用中间件
      trim_prefix(layer, layerError, layerPath, path);
    });
}

next()的逻辑是中间件执行最核心的实现,整体逻辑如下:

  • 首先判断中间件执行是否存在错误,如果存在错误且错误为router,则直接done响应404,停止后续的中间件执行。注意初次next调用的错误默认不存在

  • 如果已经迭代完栈中所有的中间件,则直接done响应,响应的内容由中间件执行的错误决定,如果已经有中间件响应了,done本身不会做任何处理,这个在上面已经提到了

  • 如果请求路径不存在,也直接调用done进行错误响应

  • 紧接着利用while循环开始迭代中间件

    • 判断中间件是否和当前请求路径匹配,如果不匹配则继续迭代下一个中间件
    • 如果是非路由中间件则直接跳出while循环开始后续的调用逻辑,因为非路由中间件不需要检查方法类型,下文都是针对路由中间件的方法类型是否匹配的判断
    • 如果存在错误,则跳过当前中间件。因为一旦产生错误,后续的中间件都不需要执行了,而是一直把错误往后传递,这样就要求错误处理中间件必须在最后一个中间件
  • 如果整个while下来之后没有匹配到中间件,则直接done响应错误处理

  • 如果是路由中间件,则直接调用路由中间件。

  • 如果是非路由中间件,则先调用trim_prefix进行参数校验

    • 路径校验不通过直接done响应错误
    • 校验通过,根据是否已有错误产生决定进行普通中间件调用还是错误中间件调用

下面看中间件调用的逻辑吧:

function trim_prefix(layer, layerError, layerPath, path) {
  if (layerPath.length !== 0) {
    // ...省略路径参数校验不通过直接done的部分
  }

  /**
   * 判断有没有错误存在
   * - 有错误则调用handle_error方法处理
   * - 没有错误则调用handle_request方法处理
   */
  if (layerError) {
    layer.handle_error(layerError, req, res, next);
  } else {
    layer.handle_request(req, res, next);
  }
}

这部分可以看到针对非路由中间件的处理就是判断是否已存在错误,存在的化调用handle_error处理,不存在的话调用handle_request处理。下面我们看handle_error内部做的什么:

/**
 * 处理layer包裹的错误处理中间件调用
 */
Layer.prototype.handle_error = function handle_error(error, req, res, next) {
  var fn = this.handle;

  /**
   * 如果传入的中间件处理程序的形参个数不是4,说明不是标准的错误处理中间件格式
   * 此时则把它作为普通中间件处理,直接调用next(error)将错误传递下去
   */
  if (fn.length !== 4) {
    return next(error);
  }

  try {
    // 调用错误处理中间件
    fn(error, req, res, next);
  } catch (err) {
    // 如果错误处理中间件调用出错了,则将错误继续往后传递
    // 直到触发最后的done的error响应
    next(err);
  }
};

这里的做法是如果有错误,但是该中间件却不是错误处理中间件,则直接next(error)将错误继续传递下去,是错误处理中间件则调用错粗处理中间件。然后next调用的控制权则移交给中间件内部控制,如果中间件执行出错,比如主动抛出错误的形式,则利用catch捕获后用next传递下去。

补充:express有普通中间件和错误处理中间件,两者参数格式不一样

  • 普通中间件形参个数为3function middleware(req, res, next) {}
  • 错误处理中间件形参个数为4function errorMiddleware(err, req, res, next) {}

handle_request的处理方式也类似,代码如下所示,就不过多讲解了:

/**
 * 处理layer包裹的中间件调用
 */
Layer.prototype.handle_request = function handle(req, res, next) {
  var fn = this.handle;

  /**
   * 如果用户的中间件参数个数大于3,说明不是标准的中间件程序
   * 则直接next()忽略,进入到下一个中间件的处理中
   */
  if (fn.length > 3) {
    return next();
  }

  try {
    /**
     * 执行当前中间件,
     * 然后在中间件内部由中间件控制next调用跳到下一步
     */
    fn(req, res, next);
  } catch (err) {
    // 如果执行中间件的过程中出错了,调用next(err)将错误传递下去
    // 比如中间件会在错误的时候throw Error出来
    next(err);
  }
};

至此,一个中间件调用流程就结束了,但是有个很重要的细节大家可能注意到了,就是路由中间件(route对象)其实只是一个路由中间件集合的调用桥梁,路由中间件layer.handle绑定的中间件只是route.dispatch方法,真正的路由中间件执行其实是route.dispatch逻辑。

下面我们来看看route.dispatch到底做的什么事情:

/**
 * 将req和res分发给route执行
 * @private
 */
Route.prototype.dispatch = function dispatch(req, res, done) {
  var idx = 0;
  var stack = this.stack;
  // 不存在路由中间件,则直接next到上层的router.stack中的下一个中间件
  if (stack.length === 0) {
    return done();
  }

  var method = req.method.toLowerCase();
  if (method === 'head' && !this.methods['head']) {
    method = 'get';
  }

  req.route = this;

  next();

  function next(err) {
    // signal to exit route
    if (err && err === 'route') {
      return done();
    }

    // signal to exit router
    if (err && err === 'router') {
      return done(err)
    }

    // 如果已经迭代完毕,则next到上层的router.stack中的下一个中间件
    var layer = stack[idx++];
    if (!layer) {
      return done(err);
    }

    // 如果请求类型不匹配,则执行下一个路由中间件
    if (layer.method && layer.method !== method) {
      return next(err);
    }

    // 根据有无错误进行不同的处理
    if (err) {
      layer.handle_error(err, req, res, next);
    } else {
      layer.handle_request(req, res, next);
    }
  }
};

route.dispatch的逻辑主要如下:

  • 如果route.stack中不存在路由中间件,则直接next到上层的router.stack中的下一个中间件
  • 迭代route.stack中的所有路由中间件
    • 存在错误为route,直接next()到上层router.stack中的下一个中间件
    • 存在错误为router,直接next(err)到上层router.stack中的下一个中间件
    • 如果已经迭代完毕,则next(err)到上层的router.stack中的下一个中间件
    • 如果请求类型与当前中间件不匹配,则执行下一个路由中间件
    • 根据有无错误进行不同的处理

至此一个完成的express中间件执行的完整闭环原理就讲完了。下面我们将抽离一个最最基本的express中间件执行逻辑,实现最基本的功能模型。

简易Express实现

实现一个简化版的Express,支持app.use()中间件模型,代码如下:

const http = require('http');
const finalhandler = require('finalhandler');

class Express {
  use(path, handler) {
    if (!arguments.length) {
      throw Error('miss arguments');
    }
    if (arguments.length === 1) {
      handler = path;
      path = '/';
    }
    if (!this.router) {
      this.router = new Router();
    }
    this.router.use(path, handler);
  }

  listen() {
    const server = http.createServer(this._handle.bind(this));
    return server.listen.apply(server, arguments);
  }

  _handle(req, res) {
    const done = finalhandler(req, res);
    if (!this.router) {
      done();
      return;
    }
    this.router.handle(req, res, done);
  }
}

class Router {
  constructor() {
    this.stacks = [];
  }

  use(path, handler) {
    const layer = new Layer(path, handler)
    this.stacks.push(layer);
  }

  handle(req, res, done) {
    let index = 0;
    const stacks = this.stacks;
    const self = this;

    next();

    function next(error) {
      // 迭代完所有中间件后执行done逻辑
      if (index >= stacks.length) {
        done(error);
        return;
      }

      let layer;
      let isMatch;

      while(!isMatch && index < stacks.length) {
        layer = stacks[index++];
        isMatch = self.matchMiddleware(req.url, layer.path);
      }

      // 迭代完发现没有任何匹配的中间件则直接done
      if (!isMatch) {
        done(error);
        return;
      }

      // 调用中间件处理函数
      if (error) {
        layer.handleError(error, layer.handle, req, res, next);
      } else {
        layer.handleRequest(layer.handle, req, res, next);
      }
    };
  }

  // 最基本的中间件是否匹配的逻辑
  matchMiddleware(url, path) {
    return url.slice(0, path.length) === path;
  }
}

class Layer {
  constructor(path, fn, ops) {
    this.path = path;
    this.handle = fn;
    this.ops = ops || {};
  }

  // 调用错误处理中间件
  handleError(error, fn, req, res, next) {
    // 如果不是错误处理中间件则跳过
    if (fn.length !== 4) {
      next();
      return;
    }
    try {
      fn(error, req, res, next);
    } catch (error) {
      next(error);
    }
  }

  // 调用请求处理中间件
  handleRequest(fn, req, res, next) {
    // 如果不是普通中间件则跳过
    if (fn.length !== 3) {
      next();
      return;
    }
    try {
      fn(req, res, next);
    } catch (error) {
      next(error);
    }
  }
}

有了上面的实例,我们可以运行一个demo实例查看一下效果验证中间件的基本使用:

const app = new Express();

// /a路由处理
app.use('/a', (req, res, next) => {
  res.end(req.reqTime);
  next();
});

// /b的路由处理
app.use('/b', (req, res, next) => {
  throw Error('/b error');
});

// 错误处理中间件
app.use((error, req, res, next) => {
  res.writeHead(error.status || 500);
  res.end('server error');
});

app.listen(3000, () => {
  console.log('express is running at port 3000');
});

结束语

百尺竿头、日进一步,我是愣锤。

希望通过本文的阅读大家能够快速了解Express中间件架构模型的核心原理,也可以动手实现一个简易的Express中间件架构。

下篇文章将会剖析Express中请求和响应的部分重要API的实现原理,文章地址《超详细的《Express》核心原理解析(下)》,欢迎点赞收藏。