Express源码系列之路由匹配

我心飞翔 分类:javascript

本系列目录

1. 全局 点我去看
2. 路由匹配 (本文)
3. 错误处理
4. 视图模板
5. 设计思想

回顾

上篇我们说到,Express中的四个对象之间的关系,他们各司其职

  • app对象负责对外暴露接口,并管理一些程序层面的全局变量,比如全局路由器,全局设置,模板引擎等等

  • router对象负责底层的路由匹配,就是负责管理中间件和请求路径的关系,解析路径并调用相应的中间件

  • route对象一般负责管理一些最后对req,red对象执行操作的函数,理解为二级或者多级中间件也行,比如说一些负责返回数据的函数,也就是平时写的最多的那种函数

    router.get('/user',function(req,res,next){
    	res.end('hello world');
    })
     
  • layer对象根据其handle函数类型的不同分成三种类型,一种handle为中间件函数,这种layer表示中间件,住在全局router.stack里,一种handle为route.dispatch,负责唤醒route,进入route.stack的匹配流程,也住在全局router.stack里,最后一种layer的handle就是上面写的函数,这种layer一般具有特定的方法,只有路径和方法都匹配时才会工作

在本文,我们将会站在路由匹配的视角看看Express时怎么处理控制中间件和路由的匹配,以及动态path的解析等等相关的问题


使用

​ 看源码之前,熟悉文档和使用应该是前提,因为当你遇到所谓的黑魔法的时候就会升起好奇心去寻找这是为什么,所以在真正开始分析路由是怎么匹配之前,先回顾下,使用的时候,在接口层面,是怎么设置路由和对应layer之间的关系的,Express文档速速过来!

1. 正常路由

//This route path will match requests to the root route, /.
app.get('/', function (req, res) {
  res.send('root')
})
 

长得很清秀,很好!

2. 正常动态路由

//To define routes with route parameters, simply specify the route parameters in the path of the route as shown below.
app.get('/users/:userId/books/:bookId', function (req, res) {
  res.send(req.params)
})
 

还行,能忍,还能看得懂,勉强也能写

3. 欧几里得变态路由(名字瞎起的)

app.get('/ab?cd', function (req, res) {
  res.send('ab?cd')
})
//This route path will match abcd, abbcd, abbbcd, and so on.
app.get('/ab+cd', function (req, res) {
  res.send('ab+cd')
})
//This route path will match abcd, abxcd, abRANDOMcd, ab123cd, and so on.
app.get('/ab*cd', function (req, res) {
  res.send('ab*cd')
})
//This route path will match /abe and /abcde.
app.get('/ab(cd)?e', function (req, res) {
  res.send('ab(cd)?e')
})
 

我:???

其实,无论匹配路径写成什么样,内部匹配操作都是一样的,所以本文就分析前面作为代表的两种,上述几种路由都是同一个api设置的,无论哪种设置,app.use,app.get等等,内部实现差不多,我们就以app[method]为例来分析,不清楚app[method]内部实现可以看上一篇 ,点我速去

贴下api文档,方便接下来的流程说明

app[method]

上篇说过,app[method]其实是调用router.route,并传入path和fns,接着router.route将path传递给layer和route进行初始化,所以layer初始化的path就是app[method]传进去的path

万事具备,只欠东风,东风来!

深入Layer

​ layer对象和路由匹配密切相关,只有了解清楚layer对象的属性,才能明白到底是怎么匹配的。

1. 属性与初始化

layer对象属性

!注意:上图中的path可不是传进来初始化的path

layer初始化

可以看到,初始化一个layer时,传进来一个path,这个path会被传进 pathRegexp这个方法并且,返回值被赋予this.regexp对象,这个pathRegexp方法就是实现上述各种动态路由功能的重中之重

2. path-to-regexp 点我速去

​ 对的,没错,这是个第三包,显然看这个第三方包的源码是不理智的,因为我最晕正则表达式了,所以在npm包网上找下用法,简单看看得了

​ 首先这个包一共只有四个方法,new Layer用的就是第一个,显然参数除了path还有keys,和options,了解下

const { pathToRegexp, match, parse, compile } = require("path-to-regexp");
// pathToRegexp(path, keys?, options?)
// match(path)
// parse(path)
// compile(path)
 

参数说明

知道你们英文不好,谷歌翻译了,将就看吧

  • path:字符串,字符串数组或正则表达式

  • keys:用在路径中找到的key填充的数组.

  • options

    • sensitive 如果为true,则正则表达式将区分大小写.(默认值:false)-
    • strict 如果为true,则regexp不允许匹配可选的尾部定界符(分隔符,form 必应词典).(默认值:false)
    • end 如果为true,则regexp将匹配字符串的末尾.(默认值:true)

你说看不懂?我也看不懂,放心,人家有给例子

//栗子1
const keys = [];
const regexp = pathToRegexp("/foo/:bar", keys);
// regexp = /^\/foo(?:\/([^\/#\?]+?))[\/#\?]?$/i
// keys = [{ name: 'bar', prefix: '/', suffix: '', pattern: '[^\/#\?]+?', modifier: '' }]

//栗子2
const regexp = pathToRegexp("/:foo/:bar");
// keys = [{ name: 'foo', prefix: '/', ... }, { name: 'bar', prefix: '/', ... }]
 
regexp.exec("/test/route");
//=> [ '/test/route', 'test', 'route', index: 0, input: '/test/route', groups: undefined ]
 

不要纠缠与此,直接总结了

  1. 该函数传进path参数作为动态路由路径,生成正则表达式,该正则表达式匹配该动态路由路径
比如 path = '/foo/:id' 生成的正则表达式则匹配 '/foo/1','foo/2'
 
  1. keys数组每一项都是一个动态参数key和相关的一些属性组成的对象,用于等会获取真实传进来的参数

    比如 path = '/foo/:id' 生成的keys数组为 [{name:'id'.....}],数组的长度就是动态参数的个数
     
  2. options选项就是控制1生成的正则表达式是怎么匹配的,有是否忽略大小写,是否匹配到末尾等等

另外还有两个布尔值,regexp.fast_star,regexp.fast_slash,

  • regexp.fast_star:当当前layer传进来的path是’*‘时,为true
  • regexp.fast_slash:当当前layer传进来的path是’/‘并且是全局中间件的时候为true

3. match方法

​ match方法就是layer匹配路由的方法,它的逻辑决定了哪些中间件对应哪些路由

总结如下:

  1. 初始化时的path为’*‘号,直接返回true,params={},path=’‘

  2. 初始化是的path为’/‘号,并且是全局中间件,params={’0‘:decodeparam(path)},path = 匹配路径

  3. 上述都不满足,开始正则匹配

  4. 匹配不上:返回false,params,path复位为undefined

  5. 匹配上了:返回true,path = 第一个匹配全体,params = 解析路径得到的params

4. 正式匹配

​ 准备开始正式匹配流程,上篇说到,当有请求到达的时候,是app作为回调执行,内部最后会执行router.handle方法,路由的匹配一切细节就发生在该方法中。

再次说明,router.handle函数是作为整个流程管理者的存在,是一个闭包,里面生命的变量在整个匹配流程中一直有效

​ 该函数首先声明一些请求层面上的全局闭包变量

全局变量

整个过程中会操作这些变量来进行对req.url路径的变换和切割进行嵌套路由的匹配,这里只看大体逻辑,因为next方法才是中间件流调度方法,所以具体的切割变换操作也发生在其中,我们只关注next函数中关于匹配的操作

  1. 首先匹配的第一步就是拿到请求路由(这里用到第三方包,不用管,只是缓存下req.url,最终都是返回path)

  2. 第二步就是找到匹配的layer,就是利用上面说过的match方法 (源码有删减)

  3. 第三步处理动态参数,这步的目的就是把解析到的动态参数挂载到req.params上

    具体解析参数源码

    proto.process_params = function process_params(layer, called, req, res, done) {
    var params = this.params;
    // captured parameters from the layer, keys and values
    var keys = layer.keys;
    // fast track
    if (!keys || keys.length === 0) {
    return done();
    }
    var i = 0;
    var name;
    var paramIndex = 0;
    var key;
    var paramVal;
    var paramCallbacks;
    var paramCalled;
    // process params in order
    // param callbacks can be async
    function param(err) {
    if (err) {
    return done(err);
    }
    if (i >= keys.length) {
    return done();
    }
    paramIndex = 0;
    key = keys[i++];
    name = key.name;
    paramVal = req.params[name];
    paramCallbacks = params[name];
    paramCalled = called[name];
    if (paramVal === undefined || !paramCallbacks) {
    return param();
    }
    // param previously called with same value or error occurred
    if (paramCalled && (paramCalled.match === paramVal ||
    (paramCalled.error && paramCalled.error !== 'route'))) {
    // restore value
    req.params[name] = paramCalled.value;
    // next param
    return param(paramCalled.error);
    }
    called[name] = paramCalled = {
    error: null,
    match: paramVal,
    value: paramVal
    };
    paramCallback();
    }
    // single param callbacks
    function paramCallback(err) {
    var fn = paramCallbacks[paramIndex++];
    // store updated value
    paramCalled.value = req.params[key.name];
    if (err) {
    // store error
    paramCalled.error = err;
    param(err);
    return;
    }
    if (!fn) return param();
    try {
    fn(req, res, paramCallback, paramVal, key.name
    );
    } catch (e) {
    paramCallback(e);
    }
    }
    param();
    };
    
  4. 第四步就是执行上一步的callback,该callback会对全局中间件layer进行去除匹配前缀的操作,并更新req.url的值使之可以匹配嵌套路由,最后执行layer的handle

    function trim_prefix(layer, layerError, layerPath, path) {
    // layerPath = undefined  path = /user
    if (layerPath.length !== 0) {
    // Validate path breaks on a path separator
    var c = path[layerPath.length]
    if (c && c !== '/' && c !== '.') return next(layerError)
    // Trim off the part of the url that matches the route
    // middleware (.use stuff) needs to have the path stripped
    debug('trim prefix (%s) from url %s', layerPath, req.url);
    removed = layerPath;
    req.url = protohost + req.url.substr(protohost.length + removed.length);
    // Ensure leading slash
    if (!protohost && req.url[0] !== '/') {
    req.url = '/' + req.url;
    slashAdded = true;
    }
    // Setup base URL (no trailing slash)
    req.baseUrl = parentUrl + (removed[removed.length - 1] === '/' ?
    removed.substring(0, removed.length - 1) :
    removed);
    }
    debug('%s %s : %s', layer.name, layerPath, req.originalUrl);
    if (layerError) {
    layer.handle_error(layerError, req, res, next);
    } else {
    layer.handle_request(req, res, next);
    }
    }
    

5. 总结

​ 路由匹配就是将请求路径和相对应的layer进行匹配

  1. layer在初始化的时候就会依据传进来的path生成特定的正则表达式,同时生成keys数组,管理自己那部分path中的动态参数
  2. layer具有match方法,其逻辑就是验证实际path与layer是否匹配,若匹配,设置layer的params和path
  3. router.handle是个闭包,整个中间件流都在其中完成,故常保存一些请求层面的闭包变量,如req.baseurl,req.url等,也保存和路由切割,路由拼接相关的变量,如removed,Addslash等
  4. 正式的匹配就是不断寻找与path匹配的layer,执行layer的handle方法,执行完后利用next方法的闭包性回到router.handle作用域,以此往复,期间进行params的挂载,路由路径前缀切割等操作。

回复

我来回复
  • 暂无回复内容