express.js 路由实现解读

关于express.js的实现源码解读,版本为 4.14。主要为路由部分。

一个Web框架最重要的模块是路由功能,该模块的目标是:能够根据method、path匹配需要执行的方法,并在定义的方法中提供有关请求和回应的上下文。

模块声明

express中的路由模块由Router完成,通过完成调用Router()得到一个router的实例,router既是一个对象,也是一个函数,原因是实现了类似C++中的()重载方法,实质指向了对象的handle方法。router的定义位于router/index.js中。

// router/index.js - line 42
var proto = module.exports = function(options) {
  var opts = options || {};

  // like operator() in C++
  function router(req, res, next) {
    router.handle(req, res, next);
  }
  //...
}

接口定义

router对外(即开发者)提供了路由规则定义的接口:getput等对应于HTTP method类别,函数签名都是$method(path, fn(req, res), ...),接口的方法通过元编程动态定义生成,可以这样做的根本原因是方法名可以使用变量的值定义和调用,Java中的反射特性也可间接实现这点,从而大量被应用于Spring框架中。

// router/index.js - line 507
// create Router#VERB functions
// --> ['get', 'post', 'put', ...].foreach
methods.concat('all').forEach(function(method){    
  // so that we can write like 'router.get(path, ...)'
  proto[method] = function(path){     
    // create a route for the routing rule we defined
    var route = this.route(path)      
    // map the corresponding handlers to the routing rule
    route[method].apply(route, slice.call(arguments, 1));
    return this;
  };
});

路由定义

在规则定义的接口中,路由规则的定义需要router保存路由规则的信息,最重要的是方法、路径以及匹配时的调用方法(下称handler),还有其他一些细节信息,这些信息(也可以看做是配置)的保存由Route对象完成,一个Route对象包含一个路由规则。Route对象通过router对象的route()方法进行实例化和初始化后返回。

// router/index.js - line 491
proto.route = function route(path) {
  // create an instance of Route.
  var route = new Route(path);    
  // create an instance of Layer.
  var layer = new Layer(path, {
    sensitive: this.caseSensitive,
    strict: this.strict,
    end: true
  }, route.dispatch.bind(route));
  // layer has a reference to route.
  layer.route = route;
  // router has a list of layers which is created by 'route()'
  this.stack.push(layer);
  return route;
};

Route的成员变量包括路径path,以及HTTP method的路由配置接口集,这里和router中一样的技巧提供了method所有类别的注册函数,此处无关紧要,只要route能够得到路由配置的method值即可,将method作为一个参数传入或者作为方法名调入都可以。

route()方法除了实例化一个Route外,还是实例化了一个Layer,这个的Layer相当于是对应Route的总的调度器,封装了handlers的调用过程,先忽略。

真正将handlers传入到route中发生在510行,也即上述route提供的注册函数。由于一条路由设置中可以传入多个handler,因此需要保存有关handler的列表,每一个handler由一个Layer对象进行封装,用以隐藏异常处理和handler调用链的细节。因此,route保存了一个Layer数组,按handler在参数中的声明顺序存放。这里体现Layer的第一个作用:封装一条路由中的一个handler,并隐藏链式调用和异常处理等细节

// router/route.js - line 190    
for (var i = 0; i < handles.length; i++) {
  var handle = handles[i];
  /* ... */
  // create a layer for each handler defined in a routing rule
  var layer = Layer('/', {}, handle);
  layer.method = method;

  this.methods[method] = true;
  // add the layer to the list.
  this.stack.push(layer);
}

返回到router中,最初实例化一个route的方法route中,还实例化了一个Layer,并且router保存了关于这些Layer的一个列表,由于我们可以在router定义多个路由规则,因此这是Layer的第二个作用:封装一条路由中的一个总的handler,同样也封装了链式调用和异常处理等细节。这个总的handler即是遍历调用route下的所有的handler的过程,相当于一个总的Controller,每一个handler实际上是通过对应的小的Layer来完成handler的调用。

route()方法可知,总的handler定义在routedispatch()方法中,该方法中,的确在遍历route对象下的Layer数组(成员变量stack以及方法中的idx++)。

// router/index.js - line 491
proto.route = function route(path) {
  var route = new Route(path);    
  
  var layer = new Layer(path, {
    sensitive: this.caseSensitive,
    strict: this.strict,
    end: true
  // the 'big' layer's handler is the method 'dispatch()' defined in route 
  }, route.dispatch.bind(route));

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

路由匹配

整理路由配置过程,思考每个路由配置信息的保存位置,有:

  • 路由规则,一条对应于一个Route中,并包装一个Layer

  • 所有路由规则保存在Router中的stack数组中。

  • 对于一个路由规则:

    • 路径在RouteLayer的成员变量path

    • HTTP method在Route下每个handler对应的Layer中的method成员变量,以及Route下的成员变量methods标记了各个method是否有对应的Layer

    • handler,每一个都包装成一个Layer,所有的Layer保存在Route中的stack数组中。

有了如上信息,当一个请求进来需要寻找匹配的路由变得清晰。路由匹配过程定义在Routerhandle()方法中(router/index.js 135行)(回顾Router()方法实际上调用了handle()方法。)

handle()方法中,不关注解析url字符串等细节。从214行可发现,不考虑异常情况,寻找匹配路由的过程其实是遍历所有Layer的过程:

  1. 对于每个Layer,判断req中的path是否与layer中的path匹配,若不匹配,继续遍历(path匹配过程后述);

  2. 若path匹配,则再取req中的method,通过routemethods成员变量判断在该route下是否存在匹配的method,若不匹配,继续遍历。

  3. 若都匹配,则提取路径参数(形如/:userId的通配符),调用关于路径参数的handler。(通过router.param()设置的中间件)

  4. 调用路由配置route的handlers,这又是遍历route下的小的Layer数组的过程。

  5. 决定是否返回1继续遍历。返回到stack的遍历是通过尾递归的方式实现的,注意到next被传入layer.handle_request的方法中,handle_request中处理事情最后向handler传入next,从而是否继续遍历取决于handler的实现是否调用的next()方法。express的实现大量使用尾递归尾调用的模式,如process_params()方法。

简化版的路由匹配过程如下所示:

   // router/index.js - line 214
   proto.handle = function handle(req, res, out) {
    
     // middleware and routes
     var stack = self.stack;
     next();
     
     // for each layer in stack     
     function next(err) {  
       // idx is 'index' of the stack
       if (idx >= stack.length) {            
         setImmediate(done, layerError);
         return;
       }
       // get pathname of request
       var path = getPathname(req);
       
       // find next matching layer
       var layer;
       var match;
       var route;
       while (match !== true && idx < stack.length) {
         layer = stack[idx++];
         
         // match the path ?
         match = matchLayer(layer, path);    
         route = layer.route;      
         if (match !== true) {
           continue;
         }
         
         // match the method ?
         var method = req.method;
         var has_method = route._handles_method(method);
         if (!has_method && /**/) {          
           match = false;
           continue;
         }           
       }

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

       // Capture one-time layer values
       // get path parameters.
       req.params = /*...*/;      
       
       // this should be done for the layer
       // invoke relative path parameters middleware, or handlers 
       self.process_params(layer, paramcalled, req, res, function (err) {
         if (route) {
           // invoke all handlers in a route
           // then invoke the 'next' recursively
           return layer.handle_request(req, res, next);    
         }
       });
     }
   }

特殊路由

在路由匹配的分析中,省略了大量细节。

  • 通过Router.use()配置的普通中间件:默认情况下,相当于配置了一个path'/'的路由,若参数提供了path,则相当于配置了关于path的全method的路由。不同的是,handlers不使用route封装,每一个handler直接使用一个大的Layer封装后加入到Routerstack列表中,Layer中的routeundefined。原因是route参杂了有关http method有关的判断,不适用于全局的中间件。

  • 通过Router.use()配置的子路由, use()方法可以传入另一个Router,从而实现路由模块化的功能,处理实际上和普通中间件一样,但此时传入handler为Router,故调用Router()时即调用Routerhandle()方法,使用这样的技巧实现了子路由的功能。

    // router/index.js - line 276
    // if it is a route, invoke the handlers in the route. 
    if (route) {
        return layer.handle_request(req, res, next);
    }
    // if it is a middlewire (including router), invoke Router().
    trim_prefix(layer, layerError, layerPath, path);

子路由功能还需要考虑父路径和子路径的提取。这在trim_prefix方法(router/index.js 212行),当routeundefined时调用。直接将req的路径减去父路由的path即可。为了能够在子路由结束时返回到父路由,需要从子路径恢复到带有父路径的路径(信息在req中),结束时调用done()done指向restore()方法,用于恢复req的属性值。

// router/index.js - line 602
// restore obj props after function
function restore(fn, obj) {
  var props = new Array(arguments.length - 2);
  var vals = new Array(arguments.length - 2);
  // save vals.
  for (var i = 0; i < props.length; i++) {
    props[i] = arguments[i + 2];
    vals[i] = obj[props[i]];
  }

  return function(err){
    // restore vals when invoke 'done()'
    for (var i = 0; i < props.length; i++) {
      obj[props[i]] = vals[i];
    }

    return fn.apply(this, arguments);
  };
}
  • 通过app配置的应用层路由和中间件,实际上由app里的成员变量router完成。默认会载入initquery中间件(位于middleware/下),分别用于初始化字段操作以及将query解析放在req下。

  • 通过Router.param()配置的参数路由routerparams成员变量存放param映射到array[: handler]的map,调用路由前先调用匹配参数的中间件。

路径参数

现在考虑带有参数通配符的路径配置和匹配过程。细节在Layer对象中。

路径的匹配实际上是通过正则表达式的匹配完成的。将形如

'/foo/:bar'

转为

/^\/foo\/(?:([^\/]+?))\/?$/i

正则的转换由第三方模块path-to-regex完成。解析后放在req.params中。

链式调用和异常处理

在handler的调用中都使用了尾调用尾递归模式设计(也可以理解为责任链模式、管道模式),包括:

  • Router中的handle方法调用匹配路由的总handler和中间件。

  • Router中的路径参数路由(params)的调用过程。

  • Routedispatch方法处理所有的handlers和每一个Layer中的handle配合。

链式调用示意图:

  • 每一个节点都不了解自身的位置以及前后关系,调用链只能通过next()调用下一个,若不调用则跳过,并调用done()结束调用链。

  • 调用链的一个环节仍可以是一个调用链,形成层次结构(思考上述提到的大Layer和小Layer的关系

  • 子调用链中的done()方法即是父调用链中的next()方法。

  • 出现异常则:

    1. 若能够接受继续进行,不中断调用链,则可以继续调用next方法,带上err参数,即next(err)。最终通过done(err)将异常返回给父调用链。

    2. 若不能接受,需要中断,则调用done方法,,带上err参数,即done(err)

图片描述

-- Fin --


进阶

  • 视图渲染模块 render实现,在applications.js 和 view.js 中。

  • reqres的扩展,header处理。

  • express从0.1、1.0、2.0、3.0、4.0的变化与改进思路。

  • 与koa框架的对比

感想

  • express的代码其实不多。

  • 路由部分其实写得还是比较乱,大量关于细节的if、else判断,仍是过程式的风格,功能的实现并没有特别的算法技巧,尤其是路由,直接是一个一个试的。框架的实现并不都是所想的如此神奇或者高超。

  • 一些不当的代码风格,如route.get等API中没有在函数签名中写明handler参数,直接通过argument数组取slice得到,而且为了实现同一函数名字的不同函数参数的重载,不得不在函数中判断参数的类型再 if、 else 。(js不支持函数重载)

转自https://segmentfault.com/a/1190000007830262 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值