Express 源码解析

首先看下 express 源码结构:

image.png

回想一下 express 的使用:

Javascript
var express = require('express')
var app = express()
app.use ...
app.get ...
app.listen ...

可以看出 express 做的其实相当于对原生 nodejs http.createserver 的一系列封装,封装成 app,并提供一系列拓展api(中间件、请求调用)来处理用户请求。

express 入口

再看看express 的入口 express.js ,看下它做了什么:

Javascript
/**
 * Expose `createApplication()`.
 */

var proto = require('./application');

exports = module.exports = createApplication;

/**
 * Create an express application.
 *
 * @return {Function}
 * @api public
 */

function createApplication() {
  var app = function(req, res, next) {
    app.handle(req, res, next);
  };

  mixin(app, EventEmitter.prototype, false);
  mixin(app, proto, false);

  // expose the prototype that will get set on requests
  app.request = Object.create(req, {
    app: { configurable: true, enumerable: true, writable: true, value: app }
  })

  // expose the prototype that will get set on responses
  app.response = Object.create(res, {
    app: { configurable: true, enumerable: true, writable: true, value: app }
  })

  app.init();
  return app;
}

export 出一个 createApplication  方法,对应着我们使用 express 的使用方式: var app = express() 

可以看出这个createApplication  返回了一个 app 函数,这个app函数接收 req、res、next 用来处理用户的请求,怎么处理呢,交给 app.handle 处理。

app 函数会经过两次 mixin 处理,可以看出 app 继承了 eventemitter 和 proto (也就是 application)

并且增加了 request、response 两个分别基于 req和 res拓展的对象,并给他们增加到 app 的引用

application

我们首先来看 application 是怎样一个对象。

最常用的 listen 方法,即对 http.createServer 的封装:

Javascript
app.listen = function listen() {
  var server = http.createServer(this);
  return server.listen.apply(server, arguments);
};

app.use

另一个核心方法 app.use , 是我们注册中间件的入口。

Javascript
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));

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

  // setup router
  this.lazyrouter();
  var router = this._router;

  fns.forEach(function (fn) {
    // non-express app
    if (!fn || !fn.handle || !fn.set) {
      return router.use(path, fn);
    }

    debug('.use app under %s', path);
    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);
      });
    });

    // mounted an app
    fn.emit('mount', this);
  }, this);

  return this;
};

我们通常有两种形式注册中间件:app.use(fn),app.use(‘path’, fn), 不配置 path 则默认在到跟路径 ’/‘,所以在 7-21行先做了一轮入参处理

随后调用了一次  this.lazyrouter(); 实例化一个 router 对象,并在接下来,42-49行可以看到实际上是调用了 router 对象的 use 方法。

所以来看一下 this.lazyrouter(); 做了啥:

Javascript
app.lazyrouter = function lazyrouter() {
  if (!this._router) {
    this._router = new Router({
      caseSensitive: this.enabled('case sensitive routing'),
      strict: this.enabled('strict routing')
    });

    this._router.use(query(this.get('query parser fn')));
    this._router.use(middleware.init(this));
  }
};

可以看到 router 对象是通过 Router 构造的, 他是如何构造 router 对象的,我们待会再看。

而第8-9行也是调用了 router 对象的 use方法注册了两个中间件

  • query 中间件,基于 parseurl 和 qs 处理请求的 query
  • middleware.init 返回的中间件,做的事是重新设置了 req和 res 的原型,分别指向 app.request 和 app.response。

app.route

除了 app.use , 我们很快还能发现另一个核心方法 app.route 。以及对 apphttp.VERB 这一类方法的代理。

Javascript
app.route = function route(path) {
  this.lazyrouter();
  return this._router.route(path);
};

/**
 * Delegate `.VERB(...)` calls to `router.VERB(...)`.
 */

methods.forEach(function(method){
  app[method] = function(path){
    if (method === 'get' && arguments.length === 1) {
      // app.get(setting)
      return this.set(path);
    }

    this.lazyrouter();

    var route = this._router.route(path);
    route[method].apply(route, slice.call(arguments, 1));
    return this;
  };
});

可以看到他们的实质都是是调用  this.lazyrouter(); 实例化一个 router 对象,随后调用了 router 对象的 route 方法。所以当我们使用 app.route app.get app.post 这一类路由中间件的时候,都是在调用 router 对象的 route 方法。

Router

所以接下来我们看看 Router 构造函数,看看 router对象是如何构造的,以及router 对象的 use 和 route 方法是怎样的。

Router 构造函数的入口在 router/index.js 

Javascript
var proto = module.exports = function(options) {
  var opts = options || {};

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

  // mixin Router class functions
  setPrototypeOf(router, proto)

  router.params = {};
  router._params = [];
  router.caseSensitive = opts.caseSensitive;
  router.mergeParams = opts.mergeParams;
  router.strict = opts.strict;
  router.stack = [];

  return router;
};

我们知道中间件就是匹配到路径就会执行的回调函数,可以看到 router 会把这件事直接交给 router.handle  做,再进一步看看 handle 方法。handle 方法很长,但是可以很快找到重点:

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

  if (layerError) {
    // routes do not match with a pending error
    match = false;
    continue;
  }

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

// no match
if (match !== true) {
  return done(layerError);
}

// store route for dispatch on change
if (route) {
  req.route = route;
}

function matchLayer(layer, path) {
  try {
    return layer.match(path);
  } catch (err) {
    return err;
  }
}

可以看到是在遍历一个 stack 数组,为了找到命中路径的 layer对象(第8行),拿到命中路径的 layer 对象之后,会调用 layer.handle_request(req, res, next); 来处理中间件。

再往下还有一段:

Javascript
// create Router#VERB functions
methods.concat('all').forEach(function(method){
  proto[method] = function(path){
    var route = this.route(path)
    route[method].apply(route, slice.call(arguments, 1));
    return this;
  };
});

出现了几个 route 相关变量,具体是什么先不管,直接看注释我们大概可以明白这是把代理了 apphttp.VERB 的 router.route 方法又交给了 route[method] 处理。

Layer

上面出现了一个新概念 layer 对象,留意一下 Router 目录,其中有一个 layer.js  文件。

image.png

Javascript
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;
  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.prototype.handle_request = function handle(req, res, next) {
  var fn = this.handle;
	...
  try {
    fn(req, res, next);
  } catch (err) {
    next(err);
  }
};

可以看到 layer 做的就是维护一个路径 path 和注册的回调函数的关系,同时会把 path 转成正则在请求阶段匹配路径调用注册的回调函数。layer.handle_request所最终执行的函数即使 layer 对象实例化时传入的函数 fn,注意这里还回把 next 传给 fn

回头再看看 Router 构造函数的入口 router/index.js 在哪些地方 实例化了 layer 对象,两个地方:

  • Router.prototype.use
  • Router.prototype.route

这样绕了一圈,我们又回到了最初来看 Router 构造函数的目的:来看 use 和 route 这两个方法。

在 Router.prototype.use 中,是这样构造 layer 对象的:

Javascript
proto.use = function use(fn) {
  ...
  var layer = new Layer(path, {
    sensitive: this.caseSensitive,
    strict: false,
    end: false
  }, fn);

  layer.route = undefined;

  this.stack.push(layer);

  return this;
};

在 Router.prototype.route 中,则是这样构造 layer 对象的:

Javascript
  proto.route = function route(path) {
  var route = new Route(path);

  var layer = new Layer(path, {
    sensitive: this.caseSensitive,
    strict: this.strict,
    end: true
  }, route.dispatch.bind(route));

  layer.route = route;

  this.stack.push(layer);
  return route;
};

比较一下两种方式可以发现:

  • Router.prototype.use 创建的 layer,注册的回调函数是传入的 fn,layer.route 是 undefined
  • Router.prototype.route 创建的 layer,注册的回调函数是一个 route 实例的 route.dispatch,layer.route 是这个 route实例

Route

上面这个 route 实例是通过 Route 创建的,我们再留意一下 Router 目录,其中还有一个 route.js  文件。

image.png

为什么要这么设计呢,我们首先来看一下 route.dispatch:

Javascript
Route.prototype.dispatch = function dispatch(req, res, done) {
  ...
  req.route = this;

  next();

  function next(err) {
		...
    var layer = stack[idx++];
    
    if (err) {
      layer.handle_error(err, req, res, next);
    } else {
      layer.handle_request(req, res, next);
    }
  }
};

可以看到他的做法跟 router.handle 的做法类似,遍历 stack 数组取出 layer对象(第9行),调用 layer.handle_request(req, res, next); 来处理中间件。

注意他遍历 stack 数组的做法不是直接循环,而是通过 idx++ 和 next 传递,可以回头看看layer.handle_request的实现里是把 next 传给了定义layer 对象实例时注册的回调函数,这里的回调函数就是 route.dispatch.bind(route),也就是 route.dispatch 自己,从而实现了遍历 stack 数组。

接下来我们就需要看看这个 layer 对象又是从哪来的了,可以很快找到这个 route 创建 layer 的地方:

Javascript
Route.prototype[method] = function(){
  var handles = flatten(slice.call(arguments));

  for (var i = 0; i < handles.length; i++) {
    var handle = handles[i];

    if (typeof handle !== 'function') {
      var type = toString.call(handle);
      var msg = 'Route.' + method + '() requires a callback function but got a ' + type
      throw new Error(msg);
    }

    debug('%s %o', method, this.path)

    var layer = Layer('/', {}, handle);
    layer.method = method;

    this.methods[method] = true;
    this.stack.push(layer);
  }

  return this;
};

可以看出他实际是维护了是一个统一 path (’/‘)对应多种 method handle 的关系,而 handle 也正是来自我们之前在 Router 构造函数入口 router/index.js 看到的把代理了 apphttp.VERB 的 router.route 方法又交给了 route[method] 的处理。

application 总结

到此我们终于把从 application 启动服务、注册和调用中间件的源码全部过完了,有关 router、route、layer的概念比较绕,接下来梳理一下。

一个 application 做的事包括三部分:

  1. 启动服务

    1. 调用 http.createServer 
  2. 注册中间件,application(express)通过 layer 对象维映射路由 path 和中间件回调函数 handle 的关系,并维护了一个 layer stack 数组:

    1. 自己调用 app.lazyrouter 注册两个默认中间件,最终会回归到 router.use 处理:
      1. middleware.init(this) 重设 res 和 req 的原型
      2. query 处理请求参数
    2. 调用方通过 app.use , router.use 注册的中间件,最终会回归到 router.use 处理:
      1. router.use 会创建一个 layer,注册的回调函数是传入的 fn,layer.route 是 undefined
    3. 调用方通过 app.all , app[http.VERB] , app.route , router.all , router[http.VERB] , router.route 注册中间件,最终回归到 router.route 处理:
      1. router.route 会创建一个 layer,这个 layer.route 是这个 route 实例
      2. 这个 route 实例会根据 http.VERB 再创建一系列 layer ,这些layer注册的回调函数是跟 http.VERB 对应的 fn handle
    4. 有两种处理的原因就在于,路由相关中间件不仅需要匹配路径还需要匹配 http.VERB
  3. 响应请求

    1. 调用 server.listen
    2. 中间件已经注册好,遍历 layer stack 数组,根据注册的区别执行中间件回调函数
      1. 回归到 router.use 处理的中间件,
        1. router.handle 路径匹配后拿到 layer
        2. layer.handle_request执行这个 layer 的 fn
      2. 回归到 router.route 处理的中间
        1. router.handle 路径匹配后拿到 layer,这个 layer 中取到 route,这个route 也是一个 layer 实例
        2. 用这个 route 的 route._handles_method 判断 http.VERB 是否匹配
        3. 随后 route.dispatch 遍历 route layer 实例的 layer stack,找到匹配 的 layer
        4. layer.handle_request执行这个 layer 的 fn

express 处理请求和注册调用中间件的核心原理即是如此。源码里还有 request.js  response.js 两个文件,内容相对简单,看起来比较快。

request

首先 req 这个对象是拓展自 http.IncomingMessage.prototype 的

Javascript
var req = Object.create(http.IncomingMessage.prototype)

先看一些通用方法:

req.get

  • request.get 即 req.header 获取请求头的方法,这里会做一些入参大小写兼容处理(Content-Type、content-type),并且兼容了 referer (Referer的正确英语拼法是referrer。由于早期HTTP规范的拼写错误,为了向下兼容就保留了 https://zh.wikipedia.org/wiki/HTTP_referer

image.png

req.accepts

  • req.accepts 检查是否支持某种 MIME type,入参兼容数组,逗号连接字符串,不支持会返回 undefined(这时候应用中应该返回 406)

req.range

  • req.range 
    • 两个参数 一个 size,一个 options(包含一个参数combine标识 重叠数据是否要合并)。
    • 返回的结果
      • -2 size 语法错误
      • -1 size 无效,对应状态码 416
      • undefined,请求头不带 range,没有size
      • 一个数组,形式为 [{ start, end }, ...], 并包含一个属性 type === 'bytes'
    • 不过这些都是靠 range-parser 这个第三方模块实现的

关于range

http1.1中新增字段,如果server支持range,需要在响应头中添加 Accept-ranges: bytes(不支持返回 Accept-ranges: none),之后client才可以发起带range的请求。

server 通过判断请求头中的 Range: bytes=0-xxx,判断是不是要做 range请求

如果 xxx存在且有效,返回文件内容,状态码206(partial content),设置 content-range

如果 xxx 无效,状态码 416(request range not satisfiable),

如果请求头不带range,正常响应,不设置 content-range

  • req.param 支持从 url params、body、query取值,排序即为优先级
  • req.is 判断请求是不是符合的 Content-Type ,基于 type-is 库实现

只读属性

同时还定义了一系列只读属性,记录一些值得关注的:

  • protocal 
    • 通过 this.connection.encrypted 判断是不是 https
    • 如果有 trust proxy,会用 trust proxy 获取 protocal,直接返回
    • 如果设置了 X-Forwarded-Proto header,会根据 这个 header 取值,如果有多个(,分隔)只取第一个
  • ip 和 ips 
    • ip 根据 trust proxy 获取请求的 ip 地址
    • ips 根据trust proxy 获取从最近的一个代理地址到请求 ip地址的列表(不包含 socket 地址) addrs.reverse().pop()  :[client, proxy1, proxy2]
    • addrs 的获取通过 proxy-addr 库实现
  • hostname
    • 如果有 trust proxy,取 trust proxy 的 host
    • 如果有 X-Forwarded-Host,取 X-Forwarded-Host 配置的第一个
    • 支持 ipv6 地址,增加了对协议头[] 的判断
  • fresh 和 stale
    • 对于get请求,当 2xx或 304的时候,判断 Etag 和 Last modified 有没有变化(基于fresh库实现)

response

res 对象拓展自 http.ServerResponse.prototype 

Javascript
var res = Object.create(http.ServerResponse.prototype)

几个比较重要的方法:

res.send

  • res.send 
    • 老接口支持 res.send(body, status) 的方式设置返回值,所以首先通过判断 arguments 做了一系列兼容性处理
    • 随后根据入参数据类型,设置 content-type, 其中对于 object 类型的数据,如果是 buffer,content-type: bin,如果是 json,会交给 this.json 方法特殊处理
    • 随后计算 content-length
    • 随后设置 etag,如果 204和 304,content-type content-length,transfer-encoding会被去除
    • 设置 this.end

res.json

  • res.json 

对json做特殊处理,类似 Json.stringify。增加了对是否需要 escape 的配置,避免某些字符会导致 html 异常

Javascript
function stringify (value, replacer, spaces, escape) {
  var json = replacer || spaces
    ? JSON.stringify(value, replacer, spaces)
    : JSON.stringify(value);

  if (escape) {
    json = json.replace(/[<>&]/g, function (c) {
      switch (c.charCodeAt(0)) {
        case 0x3c:
          return '\\u003c'
        case 0x3e:
          return '\\u003e'
        case 0x26:
          return '\\u0026'
        default:
          return c
      }
    })
  }
  return json
}

0x3c (<)0x3e (>)0x26 (&) 转成 unicode 编码,避免被 html当做特殊标签字符解析

res.sendFile

  • res.sendFile 

我们调用 sendFile的方式是 res.sendFile(path, options, callback) (options 和 callback 很多时候不配直接用默认值)
如果 opts.root 没有设置,那么path一定要是一个 绝对路径

Javascript
if (!opts.root && !isAbsolute(path)) {
  throw new TypeError('path must be absolute or specify root to res.sendFile');
}

接下来是读取文件的关键,通过 send(经典的 send包 ) 创建一个文件流

Javascript
var file = send(req, path, opts);

随后监听文件流的各个事件:

Javascript
  file.on('directory', ondirectory);
  file.on('end', onend);
  file.on('error', onerror);
  file.on('file', onfile);
  file.on('stream', onstream);
  onFinished(res, onfinish);

其中 onFinished 会在下一个事件循环开始调用 我们的 callback

Javascript
setImmediate(function () {
  ...
  if (done) return;
  done = true;
  callback();
});

最后通过pipe传给 res

Javascript
  file.pipe(res);

res.download

  • res.download 

download 的 实现和 sendFile 很接近 不同点在于会设置 ‘content-disposition’ 的响应头 (res.attachment 也会)

随后再调用 res.send 

res.redirect

  • res.redirect 入参是目标 url
    • 第一步设置 响应头 header 中的 location field
Javascript
// Set location header
address = this.location(address).get('Location');

res.location = function location(url) {
  var loc = url;

  // "back" is an alias for the referrer
  if (url === 'back') {
    loc = this.req.get('Referrer') || '/';
  }

  // set location
  return this.set('Location', encodeUrl(loc));
};
  • 调用 res.format 格式化 res.body
  • 设置 content-length 和状态码