【原创】express3.4.8源码解析之路由中间件

前言

今晚是大项目通宵上线的节奏,作为公司唯一的js前端人员,“荣幸”地被要求留守下来,于是乎,听听音乐写写博客打发时间吧。

跟大家聊一个中间件,叫做路由中间件,它并非是connect中内置的中间件,而是在express中集成进去的。

显而易见,该中间件的用途就是 ------ 路由分发 ,表面上看它的路由机制有点像Backbone.Router,实际上它的实现要比Backbone.Router复杂的多,功能也相应地要强很多。

它可以做到:

  1. 根据http采用不同的请求方法来采用不同的路由(如:get,post等)
  2. 根据请求的url与path生成的正则表达式进行匹配的结果进行不同路由(如:/user/:id => /^\/user\/(?:([^\/]+?))\/?$/)
  3. 根据path生成的表达式获取请求参数,并传递给路由处理函数(如:/user/:id => /user/322 => req.params.id为322)
  4. 根据不同的情况,选择性决定是否执行下一个路由处理函数(通过调用next()),或者是下一个路由(通过调用next('route'))

例子

var express = require('express');

var app = express();



app.set('port', 3000);



// 加载路由中间件

app.use(app.router);



// 当作数据库

var users = [

    { id: 0, name: 'lovesueee'}

];



function loadUser(req, res, next) {



    var user = users[req.params.id];

    if (user) {

        req.user = user;

        next();

    } else {

        next(new Error('Failed to load user ' + req.params.id));

    }

}



// 第一个get路由,两个回调

app.get('/user/:id', loadUser, function(req, res){

    res.send('Viewing user ' + req.user.name);

});



// 第二个get路由,一个回调,path和第一个路由相同

app.get('/user/:id', function(req, res){

    res.send('Looking user ' + req.user.name);

});



app.listen(app.get('port'), function () {

     console.log('server listening...');

});

上面是一个简单的例子(文件名为test.js),我们在终端执行:node test.js,并打开浏览器输入地址:http://127.0.0.1:3000/user/0,此时我们看到会看到页面显示:'Viewing user lovesueee'。

ok,我们理一下思路,程序是这样执行的:

  1. 服务端通过app.use(app.router);添加路由中间件,通过app.get(..)添加了两个path相同,回调函数不同的路由。
  2. 然后开始监听3000端口,客户端便输入http://127.0.0.1:3000/user/0发起请求
  3. 服务端接受请求,程序必然是先经过中间件的处理,app.router作为路由中间件也不例外(ps: 注意app.router是一个中间接接口函数)
  4. 当程序经过app.router中间件的时候,会进行路由匹配,显然这里是匹配上了,那么将会顺序执行loadUser以及其后面的匿名函数。

值得注意的几点:

  1. app.get(..)方法能够接受多个回调作为路由处理函数,就像第一个路由那样(有loadUser函数以及跟着后面的匿名函数)。
  2. 在loadUser函数中的req.params.id为0,显然是'/user/:id'生成的正则表达式/^\/user\/(?:([^\/]+?))\/?$/匹配后得到的,然后传递给req.params.id的。
  3. 最后能在浏览器中输出结果:'Viewing user lovesueee',显然是第一个路由的第二个回调函数得以执行才造成的,那么该函数的执行我们自然会联想到是loadUser函数中的next()方法的调用触发的(就像之前的中间件一样),而且req.user也是loadUser函数通过req变量传递过来的。

几个思考?

  1. 如果去掉loadUser中的next();,会怎样? --- 显然是第二个回调函数不再被调用,客户端长久处于pending状态,最后超时。
  2. 如果访问url是http://127.0.0.1:3000/user/1,会怎样? --- 向客户端返回错误信息,根据环境的(即process.env.NODE_ENV为development还是production)的不同显示不同的错误信息。
  3. 如果我们执行的不是next()而是next('route');,会怎样? --- 这时浏览器显示的将是:'Looking user lovesueee',程序没有执行第一个路由的第二个回调,而是立马执行了第二个路由的回调。

源码分析

这里主要讲解两个函数:

  • app.get(name) | app.get(path, fn1, [fn2, fn3]) --- 注册路由
  • app.router --- 路由执行入口

app.get

这个函数有两个作用:

  1. 当参数只有一个时,用来获取配置信息,与app.set(..)相对应。
  2. 当参数大于一个是,用来为get请求注册路由,也就是该方法注册的路由匹配条件有两个:1. get请求   2. url匹配path生成的正则。

注意:这里我们以app.get()方法为例,其他的路由方法同样原理,像app.post()

ok,我们首先当然得找到app.get()方法的源代码在哪里,如下(application.js中):

// 给app赋值所有的http method方法

methods.forEach(function(method){

  app[method] = function(path){



    // get时且参数唯一个时,表示到settings里取配置

    if ('get' == method && 1 == arguments.length) return this.set(path);



    // deprecated

    if (Array.isArray(path)) {

      console.trace('passing an array to app.VERB() is deprecated and will be removed in 4.0');

    }



    // if no router attached yet, attach the router

    // 如果没有调用路由中间件,这里自动调用

    if (!this._usedRouter) this.use(this.router);



    // setup route

    // 调用Router实例,创建一个路由route

    // 这里的参数可以是这样(path, fn1, [fn2, fn3], fn4,...)

    // fn代表callback

    this._router[method].apply(this._router, arguments);

    return this;

  };

});

这里通过循环methods数组,给app添加http的一系列方法,如:get, post ,delete等。

当我们调用app.get()时,

  1. if (!this._usedRouter) this.use(this.router);这样的一句,是为了防止我们在使用app.get()之前,没有先注册路由中间件(即app.use(app.router);),帮我们注册一下。
  2. 程序最后,调用this._router[method].apply(..);,(method这里为'get')告诉我们真正的路由注册其实在于this._router所引用的对象。

ok,于是我很好奇地在application.js中找到了this._router的定义,如下:

app.defaultConfiguration = function(){



  ...



  // 初始化Router实例

  // 用于路由请求

  this._router = new Router(this);



  // 搜集的路由的映射

  // 将map挂载到this对象上,那么可以通过app.routes访问

  // 结构如下:

  // {

  //   get : [

  //       Route1,

  //       Route2,

  //       ...

  //   ],

  //   post : [..],

  //   ...

  // }

  this.routes = this._router.map;



  // 定义route的getter

  // 用户通过调用app.use(app.router),启动路由中间件

  this.__defineGetter__('router', function(){



    // 标识路由中间件已启用

    // 并通过从app.setttings中的配置设置router的相应配置

    // 如:路由大小写敏感和路由严格模式

    // 最后返回路由中间件的接口函数

    this._usedRouter = true;

    this._router.caseSensitive = this.enabled('case sensitive routing');

    this._router.strict = this.enabled('strict routing');

    return this._router.middleware;

  });



  ...



};

这里我们会有所发现:

  1. this._router = new Router(this);是初始化了一个Router实例,也就是说之前的app.get()其实是调用Router实例的get方法,我们待会再看。
  2. 这里定义了app.router的一个getter,在调用app.router时this._usedRouter被设置为true,表示调用过了;另外返回的this._router.middleware;就是路由中间件的接口函数,即路由的入口处。
  3. 我上面的中文注释暴露了this._router.map的结构,这个我们接下来说。

到这里,我们的目光应该同一放到这个Router实例上,我找到了它的类,在express/lib/router/index.js,如下:

function Router(options) {

  options = options || {};

  var self = this;

  this.map = {};

  this.params = {};

  this._params = [];

  this.caseSensitive = options.caseSensitive;

  this.strict = options.strict;

  this.middleware = function router(req, res, next){

    self._dispatch(req, res, next);

  };

}

很醒目,这里其实就是定义了一些对象属性,我们更关注的其实应该是this.middleware = function router(req, res, next){..},果然不出所料时中间件接口函数,当请求走到这里时,路由中间件调用自己_dispatch方法开始进行路由分发,具体的我们在讲app.router时再说,赶紧回到我们要深追的app.get()上。

刚才说到Router实例的get方法,我们在这个文件里找到:

methods.forEach(function(method){

  Router.prototype[method] = function(path){

    var args = [method].concat([].slice.call(arguments));

    this.route.apply(this, args);

    return this;

  };

});

同样是遍历methods赋予Router原型一系列http方法,归根结底还是调用this.route(..)方法,并将传入method(这里是'get'),顺着代码往上找,找到:

Router.prototype.route = function(method, path, callbacks){



  // flatten操作

  // [fn1, [fn2, fn3], fn4] => [fn1, fn2, fn3, fn4]

  var method = method.toLowerCase()

    , callbacks = utils.flatten([].slice.call(arguments, 2));



  // ensure path was given

  // 确保路由route是有路径path可依据的

  if (!path) throw new Error('Router#' + method + '() requires a path');



  // ensure all callbacks are functions

  // 保证所有callback都是函数

  callbacks.forEach(function(fn){

    if ('function' == typeof fn) return;

    var type = {}.toString.call(fn);

    var msg = '.' + method + '() requires callback functions but got a ' + type;

    throw new Error(msg);

  });



  // create the route

  debug('defined %s %s', method, path);

  // 创建Route实例

  var route = new Route(method, path, callbacks, {

    sensitive: this.caseSensitive,

    strict: this.strict

  });



  // add it

  // 向当前Router实例的map映射里添加该route

  (this.map[method] = this.map[method] || []).push(route);

  return this;

};

哈哈,终于到了柳暗花明的地方了,和中间件的注册一样,注册暂时先存储,看看上面的代码和注释,我们可以看到:

  1. 其实可以像app.get(path, fn1,[fn2, fn3]);这样调用,因为有这一句utils.flatten([].slice.call(arguments, 2));
  2. 每次调用app.get()都会产生一个Route实例(即便它们的path是一样的,就像上面例子中的第一个和第二个路由的path一样,却是两个Route实例)
  3. Router实例中最终维护了一个map映射,它的结构如下: { get : [ Route实例1, Route实例2, ... ], post : [..], ... } ,建议大家可以看看我之前画的结构图

  4. 最终焦点就都到了Route实例中去了,我们可以想到Router实例存储着多个Route实例,那么Route实例自然会存储着method,path,callbacks这些信息。

最后,找到express/lib/router/route.js,如我所说:

function Route(method, path, callbacks, options) {

  options = options || {};

  this.path = path;

  this.method = method;

  this.callbacks = callbacks;



  // 将path转换为正则

  this.regexp = utils.pathRegexp(path

    , this.keys = []

    , options.sensitive

    , options.strict);

}



// 通过正则进行路由匹配

Route.prototype.match = function(path){...};

总结下就是:

就是存在app._router,它是一个Router实例,Router实例有一个map,可以理解为路由映射,里面存储着各种http方法的路由集合,

每个集合的元素其实是一个Route实例,每个Route实例包含一个正则,一个method用来匹配url,一些callbacks,用来回调处理逻辑。

我这里还是给一张图吧,假设路由是这样的:

// 第一个get路由,两个回调

app.get('/user/:id', loadUser, function(req, res){

    res.send('Viewing user ' + req.user.name);

});



// 第二个get路由,一个回调,path和第一个路由相同

app.get('/user/:id', function(req, res){

    res.send('Looking user ' + req.user.name);

});



// 第三个get路由,path和第一个路由不同

app.get('/post/:pid', function(req, res){

    res.send('Viewing post');

});



// 第四个post路由

app.post('/post/:pid', function(req, res){

    // ...

});

对应的存储的map为:

app.router

前面已经提到app.router其实返回的是一个中间件处理函数function router(req, res, next) {self._dispatch(req, res, next);},那么我们就应该把焦点放到self._dispatch(req, res, next);这个函数上。

我们可以试想一下,前面我们通过app.get()注册了路由,也就是将相关的信息存储起来了,那么_dispatch(..)函数所做的就应该是遍历map映射查找是否与当前url匹配的Route实例,如果有,那么执行Route实例存储的callbacks来执行业务逻辑,并且可以在任何callback里决定是否要执行接下来的callback或者是匹配接下来的路由,听到这里是不是感觉跟中间件的实现很像?没错,答案是肯定的。

我们同样找到代码的位置:

Router.prototype._dispatch = function(req, res, next){

  var params = this.params

    , self = this;



  debug('dispatching %s %s (%s)', req.method, req.url, req.originalUrl);



  // route dispatch

  // 路由分发

  // i是索引,表示从第i个route开始匹配

  // err用于传递路由时的错误信息

  // 第一次自调用pass

  (function pass(i, err){

    var paramCallbacks

      , paramIndex = 0

      , paramVal

      , route

      , keys

      , key;



    // match next route

    // 匹配下一个合适的路由

    function nextRoute(err) {

      pass(req._route_index + 1, err);

    }



    // match route

    // 匹配路由

    req.route = route = self.matchRequest(req, i);



    // implied OPTIONS

    if (!route && 'OPTIONS' == req.method) return self._options(req, res, next);



    // no route

    // 如果匹配不到,那么直接执行下一个中间件

    if (!route) return next(err);

    debug('matched %s %s', route.method, route.path);



    // we have a route

    // start at param 0

    // params匹配到的变量值,如{id : 3, ...}

    // keys变量名数组,通过解析路由是获取而来,如(:id -> id)

    // param函数中的将会用到i进行索引,所以这里i必须重置为0,

    req.params = route.params;

    keys = route.keys;

    i = 0;



    // param callbacks

    function param(err) {



      // 重置为0

      paramIndex = 0;



      // 依次获取params的key,如 id

      // 获取params对应key的value

      // 获取params对应key的回调

      key = keys[i++];

      paramVal = key && req.params[key.name];

      paramCallbacks = key && params[key.name];



      try {



        // err为'route',进入下一个route匹配

        // 我们正常会通过调用next('route')走这一步

        if ('route' == err) {

          nextRoute();



        // 如果报错,那么

        // 首先先重置i为0(因为这里都引用同一个i)

        // 然后执行路由的回调函数(处理错误的)

        } else if (err) {

          i = 0;

          callbacks(err);



        // 如果存在paramCallbacks(即通过app.param('id', function() {...});)注册的

        // 且paramVal有值

        // 那么执行paramCallbacks

        } else if (paramCallbacks && undefined !== paramVal) {

          paramCallback();



        // 如果存在key,那么迭代执行param()

        } else if (key) {

          param();



        // 最后,再执行完paramCallbacks后,最后执行路由的callbacks

        } else {

          i = 0;

          callbacks();

        }

      } catch (err) {

        param(err);

      }

    };



    // 第一次调用param

    param(err);



    // single param callbacks

    // 调用每一个param 的回调函数列表

    // 通过自调用,循环函数列表

    // 如果出错,或者循环完毕,继续处理下一个param

    function paramCallback(err) {

      var fn = paramCallbacks[paramIndex++];

      if (err || !fn) return param(err);

      fn(req, res, paramCallback, paramVal, key.name);

    }



    // invoke route callbacks

    // 调用路由回调函数

    // 依然是将当前函数作为next传给回调fn,进行下一个函数的调用

    // 这里又是迭代

    function callbacks(err) {

      var fn = route.callbacks[i++];

      try {



        // 结束路由函数的调用,进入到下一个路由匹配

        if ('route' == err) {

          nextRoute();



        // 如果报错,且fn存在

        // 参数个数 < 4,进行下一个路由函数的调用

        // 否则,调用fn处理当前错误(错误处理函数)

        } else if (err && fn) {

          if (fn.length < 4) return callbacks(err);

          fn(err, req, res, callbacks);



        // 没有错误,且存在回调

        // 参数个数 < 4,处理正常函数逻辑(逻辑处理函数)

        // 否则,忽略回调,进行下一个路由回调调用

        } else if (fn) {

          if (fn.length < 4) return fn(req, res, callbacks);

          callbacks();



        // 最后如果不存在回调了,那么进行一个路由匹配

        } else {

          nextRoute(err);

        }

      } catch (err) {

        callbacks(err);

      }

    }

  })(0);

};

上面的代码稍微长了点,不过有了中文注释应该可以理解~~

好困好困,这里我就简单说一下:

  1. pass函数主要就是进行下一个路由(Route)的匹配,当我们在callback里面调用next('route'),最终就会迭代地执行pass函数。
  2. paramCallback函数与app.param()相关联,主要用于处理匹配到的请求参数(这里就不细说了),处理完后调用next(),让callbacks能够访问到处理过的请求参数,从而进行一定的逻辑处理。
  3. callbacks函数,就是调用Route实例中存储的一系列回调函数,同样是通过next(),调用下一个回调,如果有错误,也可以next(new Error('xxx')),最终反馈到中间件处理那边。

最后

希望文章对读者,欢迎多多交流。

额,我已经扛不住了,还没上完线啊。。快倒下了~~

你可能感兴趣的:(express)