精华 express源码分析之Router
发布于 8 年前 作者 leijianning 25699 次浏览 来自 分享

express作为nodejs平台下非常流行的web框架,相信大家都对其已经很熟悉了,对于express的使用这里不再多说,如有需要可以移步到www.expressjs.com自行查看express的官方文档,今天主要是想说下express的路由机制。

最近抽时间看了下express的源码,看完源码体会最深刻的还是express的路由机制,感觉搞懂了express的路由就算是基本搞懂了express,而express的路由机制都是router模块来实现,所以在这里对express的router模块实现进行一下简单的整理,所有理解都来自自己对源码的理解,如有不对的地方,还请各位多多拍砖。

好了,废话不多说了,进入正题,首先先了解一下express源码的目录结构,如下图: express.jpg application.js为express的主文件,express.js对application.js进行了包装,对外提供各种API,这里我们不多做说明,我们今天要说的就是router目录下的内容,express关于路由的具体实现都是由这个目录完成。我们先看一个简单的express路由的例子:

var app = express();
app.get('/hello', function(req,res){
    res.send('hello everyone!!!'); 
}); 

上边就是一个最简单的express路由的例子,将path为 ‘/hello’ 的请求路由到当前的处理函数,并返回 ‘hello everyone!!!’ ,那么我们来一起看看,app.get()何实现的,通过查看代码我们发现源码里并没有app.get()的实现,但仔细找找你会在application.js中发现如下的代码:

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

(⊙o⊙)哦,隐藏的好深,原来express对get,post等方法的添加都是动态的,methods来自methods这个模块,他提供了和nodejs http.METHODS 相似的东西,返回了http协议的所有method,这样一个循环搞定了所有method函数的定义,赞一个。

接下来我们主要分析下函数内部的实现,首先判断如果method等于get,并且参数的长度是1,则直接返回this.set(path),大家查看express官网的API就可以发现,app.get()函数其实实现了两种功能,如果参数长度是1,则返回app.set()定义的变量,如果参数长度大于1,则进行路由处理。

继续往下看,this.lazyrouter(),从名字来看,好像是懒加载router,那我们看看源码:

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不存在,就new一个Router出来,而这个Router就是我们刚才在目录结构中看到的router目录,也就是今天的主角Router模块。继续上边的代码,加载完_router之后,执行了**this._router.route(path)**这样一行代码,那这行代码有做了什么呢,我们再继续往下挖,我们在router目录下的index.js中找到了它的实现:

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

我们可以看到,这里new了一个Route对象,并且new了一个Layer对象,然后将Route对象赋值给layer.route,最后将这个Layer添加到stack数组中。在这里我们先不对Layer进行说明,后边会有专门的介绍,我们先来看看Route,那这个Route又是什么呢,它和Router模块有什么关系呢,我来说下我的理解:

Route模块对应的是route.js,主要是来处理路由信息的,每条路由都会生成一个Route实例。而Router模块对应的是index.js,Router是一个路由的集合,在Router模块下可以定义多个路由,也就是说,一个Router模块会包含多个Route模块。通过上边的代码我们已经知道,每个express创建的实例都会懒加载一个_router来进行路由处理,这个_router就是一个Router模块。

理解了Route和Router的关系,感觉一下子清爽了有木有,O(∩_∩)O哈哈~~~

好了,我们接着看代码,拿到route对象之后,通过apply的方式调用了route的对应method函数,假如我们现在使用的是get函数,那现在method就等于get。看到这里大家就会发现,express实例在处理路由的时候,会先创建一个Router对象,然后用Router对象和对应的path来生成一个Route对象,最后由Route对象来处理具体的路由实现。

好了,那接下来我们继续深入研究,看看route.method究竟做了什么,我们找到route.js文件,发现如下的代码:

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 callback functions but got a ' + type;
        throw new Error(msg);
      }

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

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

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

    return this;
  };
});

啊啊啊,原来route和application运用了同样的技巧,通过循环methods来动态添加method函数,我们直接看函数内部实现,首先通过入参获取到handles,这里的handles就是我们定义的路由中间件函数,这里我们可以看到是一个数组,所以我们可以给一个路由添加多个中间件函数。接下来循环handles,在每个循环中利用handle来创建一个Layer对象,然后将Layer对象push到stack中去,这个stack其实是Route内部维护的一个数组,用来存放所有的Layer对象。现在你一定想这道这个Layer到底是什么东西,那我们继续往下看,看看layer.js的源代码:

function Layer(path, options, fn) {
  if (!(this instanceof Layer)) {
    return new Layer(path, options, fn);
  }

  debug('new %s', 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);

  if (path === '/' && opts.end === false) {
    this.regexp.fast_slash = true;
  }
}

上边是Layer的构造函数,我们可以看到这里定义handle,params,path和regexp等几个主要的属性:

  1. 其中最重要的就是handle,它就是我们刚刚在route中创建Layer对象传入的中间件函数。
  2. params其实就是req.params,至于如何实现的我们可以以后再做探讨,今天先不做说明。
  3. path就是我们定义路由时传入的path。
  4. regexp对于Layer来说是比较重要的一个属性,因为下边进行路由匹配的时候就是靠它来搞定的,而它的值是由pathRegexp得来的,其实这个pathRegexp对应的是一个第三方模块path-to-regexp,它的功能是将path转换成regexp,具体用法大家可以自行查看。

看完属性,我们再来看看Layer有什么方法:

Layer.prototype.match = function match(path) {
  if (path == null) {
    // no path, nothing matches
    this.params = undefined;
    this.path = undefined;
    return false;
  }

  if (this.regexp.fast_slash) {
    // fast path non-ending match for / (everything matches)
    this.params = {};
    this.path = '';
    return true;
  }

  var m = this.regexp.exec(path);

  if (!m) {
    this.params = undefined;
    this.path = undefined;
    return false;
  }

  // store values
  this.params = {};
  this.path = m[0];

  var keys = this.keys;
  var params = this.params;

  for (var i = 1; i < m.length; i++) {
    var key = keys[i - 1];
    var prop = key.name;
    var val = decode_param(m[i]);

    if (val !== undefined || !(hasOwnProperty.call(params, prop))) {
      params[prop] = val;
    }
  }

  return true;
};

match函数主要用来匹配path的,当我们向express发送一个http请求时,当前请求对应的是哪个路由,就是通过这个match函数来判断的,如果path中带有参数,match还会把参数提取出来赋值给params,所以说match是整个路由中很重要的一点。

Layer.prototype.handle_error = function handle_error(error, req, res, next) {
  var fn = this.handle;

  if (fn.length !== 4) {
    // not a standard error handler
    return next(error);
  }

  try {
    fn(error, req, res, next);
  } catch (err) {
    next(err);
  }
};

这个是错误处理函数,专门用来处理错误的。

Layer.prototype.handle_request = function handle(req, res, next) {
  var fn = this.handle;

  if (fn.length > 3) {
    // not a standard request handler
    return next();
  }

  try {
    fn(req, res, next);
  } catch (err) {
    next(err);
  }
};

从上边的代码我们可以看到调用了fn,而这个fn就是layer的handle属性,就是我们定义路由时传入的路由中间件,到这里我们总算找到了我们的路由中间件被执行的地方,是不是很兴奋。好了,到这里我们已经看完了Layer的代码,但Layer到底是做什么的呢,它和Route之间又有什么千丝万缕的联系呢,说说我的理解:

每一个Layer对应一个中间件函数,Layer存储了每个路由的path和handle等信息,并且实现了match和handle的功能。而从前边我们已经知道,每个Route都会维护一个Layer数组,所有可以发现Route和Layer是一对多的关系,每个Route代表一个路由,而每个Layer对应的是路由的每一个中间件函数。

讲完了Route和Layer的关系,我们再来回头看看Router和Layer的关系,我们再来看看index.js中prop.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;
};

从代码我们可以看出来Router每次添加一个route,都会把route包装到layer中,并且将layer添加到自己的stack中,那为什么要把route包装到layer中呢,前边我们已经仔细研究了Layer模块的代码,我们发现Layer具有match和handle的功能,这样我们就可以通过Layer的match来进行route的匹配了。这里有一个关键点我们需要特别讲解下,上边的代码中在创建Layer对象的时候传入的handle函数为route.dispatch.bind(route),我们来看看route.js中的route.dispatch:

Route.prototype.dispatch = function dispatch(req, res, done) {
  var idx = 0;
  var stack = this.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) {
    if (err && err === 'route') {
      return done();
    }

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

我们发现dispatch中通过next()获取stack中的每一个layer来执行相应的路由中间件,这样就保证了我们定义在路由上的多个中间件函数被按照定义的顺序依次执行。到这里我们已经知道了单个路由是被如何执行的,那我们定义的多个路由之间又是如何被依次执行的呢,现在我们来看看index.js中的handle函数:

proto.handle = function handle(req, res, out) {

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

  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 (match !== true) {
        continue;
      }

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

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

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

上边的代码我进行了处理,删除了一些逻辑,只留下关键部分。从上边的代码我们可以看出,这里也是利用next(),来处理stack中的每一个Layer,这里的stack是Router的stack,stack中存贮了多个route对应的layer,获取到每个layer对象之后,用请求的path与layer进行匹配,此处匹配用的是layer.match,如果能匹配到对应的layer,则获得layer.route,如果route不为空则执行对应的layer.handle_request(),如果route为空说明这个layer是通过use()添加的非路由中间件,需要特别说明的是,如果通过use()添加的非路由中间件没有指定path,则会在layer.match中默认返回true,也就是说,没有指定path的非路由中间件会匹配所有的http请求。

到这里,我们基本已经说明了router相关的所有内容,想必看到这里你一定会有点晕,我们接下来来重新梳理一下。看看express究竟是如何对http请求进行路由的。

当客户端发送一个http请求后,会先进入express实例对象对应的router.handle函数中,router.handle函数会通过next()遍历stack中的每一个layer进行match,如果match返回true,则获取layer.route,执行route.dispatch函数,route.dispatch同样是通过next()遍历stack中的每一个layer,然后执行layer.handle_request,也就是调用中间件函数。直到所有的中间件函数被执行完毕,整个路由处理结束。

17 回复

很好很好

mark一下,马上回来

分析得不错,先去看看源码再来看一遍

写的很好,正好解决了我的一些疑惑。

非常不错,虽然水平不够,看得晕晕地。。。T_T

谢楼主分享,学习学习

没看完 不过还是谢谢楼主

已经了解了关键思想

受启发撸了一个scheme版本

https://github.com/guenchi/Ballista

good~非常感谢

回到顶部