10

[笔记] Express4.x 源码-02

router

0x01 Layer 模块

Layer模块主要是用来解析路由的,整个模块导出一个Layer函数,这个函数是一个构造函数,一个构造函数的常见的模式写法如下:

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

这样做的好处是即使忘了使用new 操作符,仍然可以得到一个Layer实例对象。这个对象中有this.handle属性,用来储存处理

函数,this.name属性用来存储函数的名称,this.keys中则存储了以:开头的典型express路由,this.regexp则代

表了开发着定义的路由的正则表达式解析对象。

回头看了 path-to-regexp,觉得写得还是相当可以的,后续还需要好好研究以下正则的写法。

接下来就是Layer对象的原型方法了,有handle_error, handle_request, macth 方法,错误处理函数会检查参数的长度是否符合

要求,即是否为4,如果不符合,将会通过next函数将error传播到下一个处理函数,并不执行所传入的参数。handle_request

数也会校验参数,如果入参的长度超过3个,那么不会调用处理函数。当然,这些错误处理函数和请求处理函数都只是针对layer对象的。

match函数则会用来匹配路径,在上面的layer对象中,this.regexp对象有两个特殊的标志属性,fast_star,fast_slash,如果

fast_slash为true,那么将会匹配/,但是参数将不会被包含在layer对象的params对象中,如果fast_star为true,

那么将会匹配任意路径,并且将会在layer对象的params中保存所有的参数。当然了,在这个参数对象中会对已经编码的参数进行解码。

如果没有匹配到相应的路径,那么将会返回false,如果匹配到东西,那么将会把params参数填充为上文所提到的this.keys中的每个

key的name属性。

0x02 route 模块

route 模块是router模块下的一个子模块,这个模块本身利用了三个小模块,array-flaten模块用来将整个数据进行扁平化,核心

函数就是一个递归函数。然后是layer模块,最后是methods模块。

methods模块使用了核心的http模块来构建服务,主要是使用了http.METHODS将node支持的方法导出到外部模块。方法有:

get, post, put, head, delete, options等等常见的方法。

route模块类似,router模块也是导出了Route方法,Route构建了一个包含path,stack, methods属性的对象。其原型方法

_handles_method, _options, dispatch, all

_handle_method方法是用来决定是否去处理特定的方法。如果有this.methods._all标记,那么将支持所有的方法,否则,将方法

的名称小写化后,如果所处理的方法是head并且在this.methods中没有该方法,那么则会把它当作get方法来处理。最后将会返回

一个真值,来表明在this.methods中是否存在该key

其实head 方法可以看作是没有返回响应体的get方法。

_options方法是用来表示受支持的http methods,这个方法将会自动插入head方法,最后将this.methods中支持的方法小写化

后,返回。

dispatch方法

这个方法是需要着重讲一下的, dispatch方法是用来将相应的request, response对象分配给相应的路由的。在这里,我们可以看到

express框架中常见的next()方法的原型了。与上面情况的_handle_method方法类似,需要对head进行特殊处理。然后将当前处理

的route挂在request对象的route属性下。

next() 方法接收两类型的错误信息,route, router,不同的是当出现route时,不会传入错误,只调用done()函数,而使用

router的时候,则会将错误信息传递给done()方法来执行。在Route 对象的this.stack中,存放的是一个个 Layer对象,Layer在上面

的模块中已经讲过了。是路由的核心处理模块,负责对路径的匹配,错误和请求的处理, handle_error, hanlde_request。如果对应的layer

不存在,那么将会结束当前的执行。如果layer的方法与对应的请求的方法不一致,那么将传播错误。

all方法

all方法可以添加一个处理函数对于一个路由route的所有http方法,用起来就好像一个中间件一样并且可以相应或者调用next方法去处理。

好比可以这样使用:

  function validate_user() {}
  function check_something() {}


  route
    .all(valildate_user)
    .all(check_something)
    .get(function(req, res, next){
      //todo something
    });

首先会调用上文的array-flatten扁平函数来返回一个只有一个数组的函数对象数组。然后在循环中对每个all函数中的处理函数新建一个Layer

对象,并将这个处理函数作为handle参数传入。这样,Layer对象就有相应的处理函数了。然后将_all标志设置为true,表示在这一个route对象中

有相应的all方法来对下面每层的路由做处理。

在这里需要区分出Layer对象和Route对象的区别,Route对象是用来对一整个route对象进行处理的,而layer则是对route对象下的每一层

进行处理。或者这么说,Layer 对象其实是对每个handle函数进行的处理,因为每个处理函数都会有一个Layer对象,而这些处理函数都是在一个route下面的。

在route模块的最后,对每个http方法进行了赋函数的操作,也就是我们常用的router.get(func1, func2, func3, func4....)这样的方式,它还是

使用了flatten进行扁平化,最后与上面的all方法类似,为每一个方法创建一个layer对象,复制method属性为所处理的方法。然后在route中开启该方法,

然后在stack栈中保存该层。

最后在末尾返回 this 来进行链式调用。

get, post等等 http 方法

express 中,我们常常会有这样的处理函数,router.get, router.post 等等这样的处理函数,然而。这样的函数其实和上面提到的all 方法

还是有很多相似的地方的。不同的地方仅在于在为每一个Layer对象创建时,会给 method和特定的methods[method]标志位设置为true。当然,整个Layer

对象最后还是要存放到router的stack中的。

感觉route模块就是一个简化的router模块,在下文中可以看到,route.dispatch方法只是简单的执行next方法,而next方法则是

通过简单地在处理route的关键词后,在this.stack,也就是route.stack中顺序执行了layer.handle_request方法或者

layer.handle_error方法,next方法是通过不断地递归来结束所有的handle的。

从另一个角度来说,就是method方法,从router模块中可以看出,route模块就是它的一个简化版本。

0x03 router 模块

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

router.get('/hello/world', function(req, res, next){
  //todo this
});

router模块作为一个模块经常会单独使用。上面就是一个简单的例子。在router模块中,默认会导出一个route函数,这个route函数有params属性,还有

一些可选的配置。这个params属性是用来存类似与/get/:type, /post/:hello后面的:name参数的。

router.param

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


router.param('type', function(req, res, next, type){
//todo this
});


router.get('/hello/:type', function(req, res, next) {
  //todo this
});

这个用法是经常使用的param方法,在使用的时候,传入的参数是参数的name, 以及回调函数fn。正常的使用像上面那样的,会将每个

name以及它的回调函数保存在route.params中。

router.route

这是一个公共方法,主要是结合了route,layer两个模块的东西,而且主要

是新建一个route,一个layer,然后给这个layer添加route属性,最后将这个layer放到router的stack栈属性中。

router.#method

router.get('/get/method', function(){
  //todo
});

router.post('/post/method', function(){
  //todo
});

上面这个例子就是router的常用方法,当然了这一批方法都是通过以下方法构造的:

// create Router#VERB functions
methods.concat('all').forEach(function (method) {
  proto[method] = function (path) {
    var route = this.route(path)
    route[method].apply(route, slice.call(arguments, 1));
    return this;
  };
});

router.handle

下面则是handle,是一个私有的函数,与上面的route模块中的route.dispatch方法十分类似,这个函数的主要功能是分发一个request, response

对象到一个router当中去的。值得一提的是在这个函数中,也有一个next方法,上次遇到这个next方法是在./router/route.js

块中的dispatch方法中,相比之下,这个next方法更为复杂。

在这个next方法中,主要会做几件事情:

  • 清除被添加的/,保存更改后的url;

  • 判断是否发出了router信号来退出;

  • 是否匹配到的更多的layers;当然这个阶段是通过this.stack的长度来判断的;

  • 如果上面的判断就略过了,那么说明这是一个正常的path处理过程,通过一个while循环来判断是否能够匹配到当前说接收到的路由。如果能够匹配到,那么
    还要在判断是否每个匹配到的layer,在路由中会判断每个路由的layer.route属性是否存在,如果不存在的话,那么是不用处理的,但是一般情况下,通过
    route.#method#方法使用时,是会给每个匹配路径layer添加对应的route的。然后将所有的layer统一加到router中。

  • 下一步这是判断当前的route.method中是否与req.method是否相互匹配了,如果方法不匹配而且不是head方法,那么匹配将会结束。

  • 如果上面这些不都略过了,那么接下来,就进入了参数param合并的阶段,

  • 最后一步就是处理layer的param

handle 方法中用到的一些函数

  • matchLayer :这个函数其实就是上面提到的layer.match方法

  • appendMethods : 将当前不受支持的方法加到该route中

  • mergeParams : 合并router中的params和layer中的params 这个模块需要特别关注下

router.process_params

这个方法主要是用来处理有关params的东西,有req.params, router.params,,如果和先前已经调用过的param有相同的值,或者回调函数中出现了错误,那么

将会退出param处理函数,如果没有出现以上两个错误那么将会调用paramCallback来执行回调函数。

router.use

这个函数多是用在app.use方法中的,但是呢在route中也是可以用的,因为它默认是将这个方法使用到/路径下的。当然了,在平常使用的过程中,也是可以直接传入函数,也可以传入/route以及相应的处理函数,在函数中有相应的判断。

总结

最后插一张我做的router整体架构图作为总结,当然,这个图 不可能面面俱到,我只做了我认为核心的部分:

[译] Node.js 核心模块---process