Express源码解析

4,858 阅读7分钟

概述

NodeJS官方提供的最简单的服务器例子如下:

const server = http.createServer((req, res) => {
  res.statusCode = 200;
  res.setHeader('Content-Type', 'text/plain');
  res.end('Hello World!\n');
});

Express框架没有那么神奇,只是代理了http.createServer(requestHandler)中的requestHandler。并使用已经注册了的中间件和路由匹配响应传来的用户请求。

整体思路

通过阅读源码,我觉得可以把Express逻辑分成两段:启动服务和响应请求。

启动服务阶段指的是http.createServer(requestHandler)server.listener()两个API被调用前执行的一系列初始化工作。

响应请求阶段指的是服务器接收来自客户端请求时触发的request事件的handler。

启动服务阶段

启动服务最重要的部分就是注册中间件和路由了。

中间件和路由可以说是几乎所有服务器都会提供的功能。在Express框架里,中间件和路由都会抽象成layer对象,在这篇文章里,存储中间件layer对象的容器叫做中间件router对象,存储路由layer对象的容器叫做路由router对象

在Express框架里,中间件就是匹配路径就会执行的回调,而路由不仅要匹配路径还要匹配http method(如get、post之类)。所以对于中间件router对象,匹配路径之后会直接执行回调,但是路由router对象的匹配路径之后执行的回调统一为router.handle(req, res, next),里面的逻辑会继续匹配http method。

1. app.use方法

不论是注册中间件router对象还是路由router对象,我们都会使用app.use

app.use方法实质上是调用它自身的router对象的use方法:

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

2. 中间件router对象

当我们调用类似app.use('/', fn)这样的语句,其实就是注册中间件。

这里必须说明一下,每一个express app初始化的时候会使用app.lazyrouter()来实例化一个router对象,在这篇文章里,我们姑且叫它中间件router对象,因为它主要是负责储存中间件layer对象的,但是它还可以注册router对象,例如开发中我们会调用形如app.use('/test', testRouter)的语句。

中间件router对象维护这一个stack数组,用来装载Layer对象。

当router对象的use方法被调用的时候,就会把路径和回调封装成一个Layer对象,并放入stack数组中。

请注意:中间件router对象的layer对象的route是undefined,跟路由router对象的layer对象的route是不一样的。

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

layer.route = undefined;

this.stack.push(layer);

3. 路由router对象

当我们调用形如app.use('/test', testRouter)的语句,可以表述为注册了一个路由中间件,而这个中间件就是下面的router函数:

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

为了区别与中间件router对象,在这篇文章里,把注册在中间件router对象上的路由中间件定义为路由router对象。

到这里,我最想告诉大家的是,在express里,router对象是可以通过这种方式嵌套的。

就和前面提到的一样,路由也会被抽象成layer对象,并把router函数作为Layer构造函数的第三个参数传入。

4. HTTP Method方法和Route实例

HTTP Method指的是get、post、put、delete、header之类的http请求方法。

路由router对象不仅需要匹配路径还需要匹配HTTP Method。而负责匹配HTTP Method的功能是由Route实例来完成。

当我们在调用app[method]或者router[method]时,就是在调用router.route方法(就是下面的this.route(path)),如下:

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

router.route方法里面会生成一个新的layer对象,并把回调设置为route.dispatch.bind(route),这一点与前面提到的中间件router对象不同,而且layer的route不再是undefined,最后返回新的Route实例。代码如下:

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

那么返回的Route实例的作用是什么呢?先看看它的构造函数:

function Route(path) {
  this.path = path;
  this.stack = [];

  debug('new %o', path)

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

Route实例维护着一个stack数组,作用是收集Layer对象;还维护这一个methods对象,作用是指示该route对象可以匹配的http methods。

route收集的Layer对象维护着路由真正的回调,就是下面的handle:

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

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

5. Layer对象

一个Layer对象维护这一个路径和回调,它会把路径正则表达式化,用以在响应请求阶段匹配路径,先看看它的构造函数:

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对象:

Layer类别 route method
中间件Layer undefined undefined
路由Layer 非undefined undefined
route Layer undefined 非undefined

中间件Layer实例的回调是fn,也就是注册的中间件函数;路由Layer实例的回调都是function router(req, res, next);route Layer实例的回调都是route.dispatch.bind(route)

响应请求阶段

通过启动服务阶段,我们已经把服务器的准备工作完成 —— 注册了中间件和路由。

当应用执行到server.listener()时,就可以开始接受并处理客户端的请求,最后返回服务器响应。

1. 增强req对象和res对象

当一个请求到来的时候,NodeJS会把请求抽象成req(http.IncomingMessage的实例),把响应抽象成res(http.ServerResponse的实例),传入server的request事件的handler,但是在Express框架里,req对象和res对象被增强了。

增强内容可以参考express.js同目录下的request.js和response.js。

那么是怎么增强的呢?

app.lazyrouter方法里,已经添加了一个中间件,就是下面的middleware.init(this)

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

而在middleware.init(this)里,可以看到重新设置了req和res的原型:

exports.init = function(app){
  return function expressInit(req, res, next){
    if (app.enabled('x-powered-by')) res.setHeader('X-Powered-By', 'Express');
    req.res = res;
    res.req = req;
    req.next = next;

    setPrototypeOf(req, app.request)
    setPrototypeOf(res, app.response)

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

    next();
  };
};

2. 正则表达式匹配中间件和路由

由于在启动服务阶段,我们已经注册好了中间件和路由,并把它们都抽象成layer对象,所以在处理请求阶段的时候,就清晰明了了。

基本逻辑是: 遍历router维护的stack容器; 对于中间件layer(就是layer.route为undefined的),路径匹配成功后就可以执行中间件函数了; 对于路由layer(就是layer.route不是undefined的),路径匹配成功后还需要匹配http method才能执行路由函数。

这一过程,有如下的重要方法:

app.handle,express app处理请求的入口,实质上是调用了自身router的handle router.handle,遍历router维护的stack数组,找到匹配路径的layer对象 Route.prototype._handles_method,对于路由layer对象,还需要这个方法验证是否可以匹配http method Route.prototype.dispatch,遍历route维护的stack数组,找到匹配路径和http method的layer对象 Layer.prototype.match,路径匹配的关键 Layer.prototype.handle_request,匹配成功后执行回调

3. 模板引擎

模板引擎并不是express作者原创的,而是引入了别的第三方库,然后使用第三方库提供的API渲染出响应页面,并返回给客户端。

目前支持较多的是ejspug这两个模板引擎。

Express镶嵌

一个Express app是可以挂载到另一个Express app上的,因为本质上一个Express app就是为了维护起自身的router对象,所以挂载的方式其实就是在parent express app的上注册一个中间件,该中间件负责把req和res传递给child express app,并让它们建立起父子关系,源码如下:

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

参考

Express源码@4.16.3 Express中文文档