资讯专栏INFORMATION COLUMN

express.js 路由实现解读

sugarmo / 481人阅读

摘要:关于的实现源码解读,版本为。主要为路由部分。返回到的遍历是通过尾递归的方式实现的,注意到被传入的方法中,中处理事情最后向传入,从而是否继续遍历取决于的实现是否调用的方法。

关于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的过程:

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

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

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

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

决定是否返回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()方法。

出现异常则:

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

若不能接受,需要中断,则调用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://www.ucloud.cn/yun/86505.html

相关文章

  • 解读express 4.x源码(1)

    摘要:在后续的总结中,我会继续分析,并准备将一些值得分析的逐一解读,也会涉及一些。从一个官方示例开始这是官方给出的一个简单程序,运行后访问显示。第一行载入了框架,我们来看源代码中的。代码的开始定义了一个函数,函数有形参,,为回调函数。 这两天仔细看了看express的源码,对其的整个实现有了较清晰的认识,所以想总结一下写出来,如果有什么不对的地方,望指出。 这是第一篇,首先介绍一个最简单的...

    summerpxy 评论0 收藏0
  • 笔记:解读express 4.x源码

    摘要:载入了框架,我们来看源代码中的。函数函数代码如下代码的开始定义了一个函数,函数有形参,,为回调函数。相应的,等同于继承,从而让有了事件处理的能力。 此为裁剪过的笔记版本。 原文在此:https://segmentfault.com/a/11...原文在此: https://cnodejs.org/topic/574... 感谢@YiQi ,@leijianning 带来的好文章。我稍作...

    jzman 评论0 收藏0
  • Express源码学习-路由

    摘要:框架核心特性路由定义了路由表用于执行不同的请求动作。中间件可以设置中间件来响应请求。注册一个请求路由结束响应开启监听端口执行上面代码是一种实用工具,将为您的源的任何变化并自动重启服务器监控。 Express 简介 Express 是一个简洁而灵活的 node.js Web应用框架, 提供了一系列强大特性帮助你创建各种 Web 应用,和丰富的 HTTP 工具。使用 Express 可以快...

    laznrbfe 评论0 收藏0
  • 学习express.js源代码的方法

    摘要:学习的源代码的好处自然不少。阅读源代码可以帮你实现你的好奇心。本文会推荐一些的源代码分析文章,可以帮助更快的,更加全方位的理解研读之。 尽管有Hapi,Koa等有力的竞争者,express.js依然是非常流行的nodejs web服务器框架,毕竟它早于2007年就已经在开发了。 学习expressjs的源代码的好处自然不少。 它可以帮你深刻理解HTTP协议,这个协议是做前端后端都必然需...

    huaixiaoz 评论0 收藏0
  • express 源码阅读(全)

    摘要:每个请求都会对应一个响应。一个响应主要包括状态行响应头消息体,将常用的数据封装为类,在上面的代码中就是该类的一个对象。执行测试用例,报错,提示不存在。目前在中,一个路由是由三个部分构成路径方法和处理函数。 1. 简介 这篇文章主要的目的是分析理解express的源码,网络上关于源码的分析已经数不胜数,这篇文章准备另辟蹊径,仿制一个express的轮子,通过测试驱动的开发方式不断迭代,正...

    Steven 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<