Express源码的一些理解

506 阅读5分钟

Express框架很早就有接触,一开始接触的时候觉得Express框架晦涩难懂绕来绕去有点头晕。最近血来潮准备再次翻看下Express源码,一方面也是为了自己做个笔记,方便以后翻阅查看,另一方面是为了和大家分享,希望得到各位大牛的指导指正。

示例代码

在开始前对于如何启动express项目在这里就不再赘诉了,不过一般情况都有以下几句常见代码
var express = require('express');
// index下的路由规则
var index = require('./routes/index');
// user下的路由规则
var users = require('./routes/users');
var app = express();

app.use('/', index);
app.use('/users', users);

// 监听3000端口
app.listen(3000, () => {
  console.log("listening.... ")
});

这里重点讲下导入的users.js依赖包,因为这个关系到get请求的路由,代码如下

var express = require('express');
var router = express.Router();

/* GET users listing. */
router.get('/', function (req, res, next) {
    res.json({
        name: 1
    });
    next();
},
function (req, res, next) {
    console.log("next aaaa")
});

router.get('/aaa', function (req, res, next) {
    next();
}, function (req, res, next) {
    res.json({
        a: 1
    });
    next();
}, function (req, res) {
    console.log("next", 2222);
});


module.exports = router;

源码分析

从示例代码里我们不难看最重要的代码是app.use,use内部存放的都是middleware,当路由地址匹配的时候,会依次流过这些middleware。不过在进行中间件的use的时候,首先是对路由的初始化设置,lazyrouter内部就进行了路由的初始化设置,并且自动加入了两个新的中间件。

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对象【1】, 为以下形式

/**
router方法对象内部属性有:
  router.params = {};
  router._params = [];
  router.caseSensitive = opts.caseSensitive;
  router.mergeParams = opts.mergeParams;
  router.strict = opts.strict;
  router.stack = [];
**/
function router(req, res, next) {
    router.handle(req, res, next);
}

在这里值得一提的是express内部的巧妙实现

methods.concat('all').forEach(function(method){
  proto[method] = function(path){
    var route = this.route(path)
    // 这里将method内部的属性方法的this改为route对象,并传值route.get等方法的传参中除了第一个path参数之外的所有回调函数
    route[method].apply(route, slice.call(arguments, 1));
    return this;
  };
});
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;
};

通过遍历所有的method,往proto内部新增get方法等method内部的属性,首先需要搞清楚this指谁,this指代的是当前的router对象即为express.Router(),内部包含【1】中的router对象一样的数据结构,由于是get请求,所有layer.route即为构造的子路由对象,并且将每个get等请求路由加入到Layer对象中,每一个get,post等请求对应于一个Layer对象,最终push到新的stack中。

来看下内部的Layer对象,我认为最重要的是设置路由正则规则以及存放handle函数

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
}

之后第二个神奇巧妙的地方就是var route = new Route(path);,装载内部的子路由的时候,就像多米勒牌的第一张被触发了以后出现排山倒海般纸牌组成的美景一样,内部的实现文件一被碰到就自动实现好了。

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

  debug('new %o', path)

  // route handlers for various http methods
  this.methods = {};
}
methods.forEach(function(method){
  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;
  };
});

通过遍历所有的method,往Route内部新增get方法等method内部的属性,首先需要搞清楚this指谁,this指代的是new Route(path)构造出来的当前子路由对象, handle方法即为get方法的第一个参数之后的回调函数,可以有多个,多个回调函数是惰性的,内部将next方法作为参数传入了回调函数,需要使用next函数来进行调用回调函数。

初始化好了router,接下来就是需要将不同route装载进入express设定的数据结构中,express采用了数组的形式来进行存放,首先让我们来看下app.use的内部部分源码,app.use可以有两个参数,第一个为非必须参数path,第二个为fn,在最开始的时候会进行路由的初始化操作,最终app.use会调用router文件内部的router.use,这里会将每个中间件的fn存入到Layer中,一个中间件对应于一个Layer,由于是中间件,所有Layer对象下面的route设置为undefined,区别get等其他请求,最终会将Layer对象push到stack。

app.use = function use(fn) {
  // ...
  // 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);
    }
  }, this);

  return this;
};
proto.use = function use(fn) {
  for (var i = 0; i < callbacks.length; i++) {
    var fn = callbacks[i];

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

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

    layer.route = undefined;

    this.stack.push(layer);
  }

  return this;
};

最终组装好的第一层的形式如下

最终效果的打印结果

从这里我们看到内部嵌套的还是比较多的,内部的stack至少有三层,一开始看到时候还是比较头晕。

关于路由匹配之后会补上》》》》》to be continued...

总结

总之,要理解这么多层嵌套的关键是需要搞清楚,三层中this的指代,第一层是初始化路由, this指代router对象,第二层是内部自动组装的get等一些子路由请求,this指代的是express.Router(); 第三层是也是内部自动组装,不过这一层会放上真正的请求地址,stack放置在layer.route下,this指代的是Route对象(new Route(path);),每一层都有一个stack数组进行Layer的存放,之后就会通过解析正则表达式的路由规则和监听的请求地址进行匹配,最终会调用Layer内部的handle函数,从而进行页面交互和渲染。

学习源码的过程是痛苦的,但理解完以后会对整个机制有更加深刻的理解,最后如果有什么不足之处还需要各位大牛的指正。