Express源码级实现の路由全解析(下阕)

  • Pre-Notify
  • all方法
    • 注册路由
    • 分发路由
  • 中间件
    • intro
    • 测试用例1与功能分析
    • 功能实现
      • 注册中间件
      • 分发中间件
        • 匹配
        • 分发
    • 测试用例2与功能分析
      • 功能实现
  • 动态路由
    • 测试用例与功能分析
    • 功能实现
      • 注册动态路由
      • 分发动态路由
      • param方法
  • 其它
    • *路径
    • 支持注册路由时以'/'结尾

Pre-Notify

前情提要

Express深入理解与简明实现

Express源码级实现の路由全解析(上阕)

本篇是 Express深入理解与实现系列 的第三篇,将着重讲述 中间件错误中间件路由容器动态路由param的应用与实现。

emmm...前两章没点赞的话是看不懂这篇的哦!咳咳。。。

all方法

注册路由

首先我们来补全上回就应该实现的一个方法 app.all

.all方法注册的路由在分发时可以无视method类别,也就是说只要请求路径是对的,不论使用何种请求方式,都能匹配成功分发路由。

首先我们在app接口层添加一个接口

Application.prototype.all = function(){
    this.lazyrouter();
    this._router.all = ...等等!!
}
复制代码

我们静静思考两秒中,emm....all 方法 和其它的 33种请求方法的接口有什么不同吗?

Nothing else!!

都是往router.stack中注册一层路由,然后再在这层route中存放一层层cb

再想想http.METHODS,我们之前利用这货来批量产出我们的接口,嗯,是不是想到了什么?

So,我们能采用一种更简单的方式来完成这个接口和委托给router的那些个方法。

//route.js

http.METHODS.push('all'); //<--- 看这里!!! 这里是关键

http.METHODS.forEach(function(METHOD){
  let method = METHOD.toLowerCase();
  Route.prototype[method] = function(){
  	...
    return this;
  }
});
复制代码

只需要以上多添加那么一行代码就实现接口以及它完整的功能,为什么呢?

因为我们在这里给 http.METHODS push一个all,那么我们在router/index.jsapplication.js中加载http模块时,引入的http模块中的METHODS也会多一个all。

为什么其他文件引入http时,也会在METHODS下面多一个all呢?这设计到require加载问题,require加载同一个文件是有缓存的,且级联加载时最后加载的最先执行完毕。

分发路由

以上我们完成了注册路由方面的,接下来我们需要一个标识来特别标注这是一个all方法,这样我们在路由匹配快速匹配检查时以及分发路由,即调用route.dispatch方法进行请求方式的校验时才能被放行。

实际上我们在调用 router.middle 注册路由时,已经给每一层route添加了一个 methods['middle']=true , 给每一层route.stack 也添加了一个method属性,其值为middle。

So其实我们已经做好标识,现在只需要在对应的检查方法中进行放行。

对路由匹配时候的 handle_methods快速匹配方法进行修改

//route.js
Route.prototype.handle_method = function(method){ //快速匹配
    return (this.methods[method.toLowerCase()]||this.methods['all'])?true:false;
};

复制代码

对分发路由时的method判断做出修改

// route.prototype.dispatch 方法中 

if((layer.method === req.method.toLowerCase())||layer.method === 'all'){
    layer.handle_request(req,res,next);
}
...
复制代码

中间件

intro

顾名思义,中间件,中间的那个谁,嗯。。件。。

它是谁和谁的中间呢?是得到请求和执行真正响应之间的一层,主要是做一些预处理的工作。

中间件的使用和注册路由的.get等方法大致是相同的,都支持

  • 一个动作同时注册多个cb
  • 每个cb中可以决定是否调用next来继续匹配后面的layer(包括router.stack和route.stack里的)

中间件会和路由一样在路由匹配时参与匹配,且和注册路由一样,谁先注册谁先被匹配。

但路由毕竟是路由,中间件毕竟是中间件。

其中最明显的一点不同之处在于,一般来说当路由被匹配上就会结束响应不再向下匹配,而对于中间件来说,同一条请求可以匹配上多个中间件,(虽然说当路由被匹配上时我们可以不使用end结束响应并且使用next继续向下匹配,但这种做法是不推荐的)

且中间件路径匹配和路由的匹配在细节上是不同的,路由匹配时路径必须完全相同,而中间件只需要路径的开头是以中间件注册时的路径就可以了,比如

//请求路径为/user/a
app.use('/user',cb1,cb2...);
...
复制代码

以上,请求路径为 /user/b ,它是以中间件注册时的路径 /user 开头的,故它匹配上了。

其它不同之处:

  • 中间件可以省略路径,省略路径时它的路径为'/'。
  • 有一种特殊的中间件专门用来处理错误,称之为错误处理中间件。

测试用例1与功能分析

app
  .use('/user/',function(req,res,next){
    res.setHeader('Content-Type','text/html;Charset=utf-8');
    res.write('/user中间件;');
    next('跳转到错误处理 ---');
    // next();
  },function(err,req,res,next){
    res.write(err+'我是错误处理');
    next(err);
  },function(req,res,next){
    console.log('/user中间件3');
    res.write('/user中间件2');
    next();
  })
  .get('/user',function(req,res,next){
    res.end('/user结束')
  })
  .use(function(err,req,res,next){
    res.write(err+'我是错误处理2');
    next(err);
  },function(err,req,res,next){
    res.write(err+'我是错误处理3');
    next(err);
  })
  .use(function(err,req,res,next){
    res.write(err+'我是最后的错误处理');
    next();
  })
  .get('/user',function(req,res,next){
    res.end('/user结束2')
  })

.listen(8080);

>>> /user

<<< 输出到页面
/user中间件 跳转到错误处理 --- 
我是错误处理 跳转到错误处理 --- 
我是错误处理2 跳转到错误处理 --- 
我是错误处理3 跳转到错误处理 --- 
我是最后的错误处理
/user结束2
复制代码

以上是一个错误中间件的使用示例,我们可以注意到错误中间件相较于普通中间件有一个显著的不同,这货有四个参数,多了一个err!

嗯,这很关键,我们在源码里就是借由这一点来区分普通中间件和错误处理中间件的,

当我们在一个普通的中间中调用next并且传递了err时,这就表示 something wrong 了,接下来的匹配就不再会匹配路由和普通中间件,只会匹配上错误处理中间件,并将错误交给错误中间件来处理。

另外,当匹配上一个错误处理中间件,错误是可以继续向下传递的,且在经过我们最后一个错误处理中间件处理完成后,我们仍然可以选择让它继续向下匹配普通的中间件和路由!

最后还有一个细节需要注意,在同一个注册中间件的动作中所注册的callbcaks中可以同时存在普通中间件和错误处理中间件,这是什么意思呢?emmm...上代码

app
  .use('/user/',function(req,res,next){
    res.setHeader('Content-Type','text/html;Charset=utf-8');
    res.write('/user中间件');
    next('跳转到错误处理 ---');
    // next();
  },function(err,req,res,next){
    //res.write('我是错误处理');
    res.end(err+'我是同一个中间件中的错误处理');  //<--- 看这里!!!
    next(err);
  },function(req,res,next){
    res.write('/user中间件2');
    next();
  })
  ...
复制代码
功能实现

基本实现其实都和一般路由的实现都差不多

[warning] 注意上一篇讲过的这一篇不再赘述,如果有些源码看不懂,联系不起来,请回播(*  ̄3)(ε ̄ *)

注册中间件

先在Application接口层添加一个对外接口。

Application.prototype.use = function(){
    this.lazyrouter();
    this._router.use.apply(this._router,arguments);
    return this;
}
复制代码

再在router中实现这个接口

在这一层,就和普通的路由方法们的实现不一样了,我们需要对路径做一下兼容处理(这也是为什么我们不像实现.all方法一样直接在METHODS中 push 一下 use)。

//router/index.js

Router.prototype.use = function(path){
    let handlers;
    if(typeof(path)!=='string'){
    	handlers = slice.call(arguments);
    	path = '/';
    }else{
    	handler = slice.call(arguments,1);
    }
    let route = this.route(path,true);
    route.middle.apply(route,handlers);
}
复制代码

上面中我们还需要注意的一点是,我们调用this.route注册中间件时,多传了一个参数true,这是因为原本这个方法是用来注册路由的,它会往router.stack添加一层layer且会标注这个layer是个路由,但我们注册的是中间件,So这里传了一个参告诉route方法我们不需要标注它是route,而应该是middle!

Router.prototype.route = function(path,middle){
	...
    if(middle){
    	layer.middle = route;
    }else{
    	layer.route = route;
    }
    ...
}
复制代码

另外我们需要注意的一点是,我们在router.stack中存放的layer中存放的handler仍然是route.dispatch,用于分发中间件。

Router.prototype.route = function(path,middle){
	...
    let layer = new Layer(path,route.dispatch.bind(route));
    self.stack.push(layer);
    ...
}
复制代码

接下来我们来实现route.middle这个方法,这个方法我们主要是用来往route.stack里添加一层层cb,这个方法的实现和普通的route.get/post等方法实现的流程是完全相同的,

只需在往route这个stack中存放cb时标识一下stack里存放的有中间件,以便于路由匹配时进行快速匹配。

this.methods['middle'] = true;
复制代码

然后在每一层cb下标识一下这是个中间件的回调即可。以便于在路由分发时能够被放行。

layer.method = 'middle'
复制代码

这样我们就基本完成了注册中间件的功能

分发中间件

分发分两个步骤,一是匹配,二是真·分发。

匹配

首先因为中间件匹配时路径检测和路由匹配时的路径检测的不同

我们需要更改 router.prototype.handle 中的 layer.match 方法,向里面添加对中间件路径判断的支持。

Layer.prototype.match = function(path){
     //路由路径检查
    if(path === this.path)return true;
    
    //中间件路径检查
    if(this.path === '/' || this.path === path || path.startsWith(this.path+'/'))return true;
    
    return false;
}
复制代码

注意第三个||中路径规则中最后加上了一个/,是为了避免以下情况时被误匹配

注册路径:/user
请求路径:/user123
复制代码

当路径匹配成功后,我们大体的思路是这样的

...
if(!this.route){ //说明是中间件
    if(err){
    	layer.handle_error(err,req,res,next);
    }else{
    	layer.handle_request(req,res,next)
    }
}esle{ //说明是路由
    if(!err&&layer.route&&layer.route.handle_methods){
    	...
    }else{
    	next(err)
    }
}
...
复制代码

由于错误中间件有四个参数,调用它时需要传递err,和调用路由时只需传递3个参数是不同的,So我们需要将中间件和路由的处理分开。

并且之前我们说过,当发生错误时会跳过普通的中间件和路由,So我们在进入路由的分支中还对err的有无进行了判断,如果存在err,那么会跳过此次匹配。

Layer.prototype.handle_error = function(err,req,res,next){
    if(!this.middle&&this.handler.length!=4) return next(err);
    this.handler(req,res,next);
}
复制代码

在错误处理的方法中,我们对普通中间件进行了跳过,且我们使用了!this.middle进行筛选,之所以要这么做是为了防止中间件匹配成功后 中间件存储在router.stack中的 分发函数 被handle_error给跳过。

这是怎么样的一种场景呢?

当一个中间件被匹配上,且在next中传递了err,当下一条中间件被匹配上时它会执行handle_error,这时我们需要在进一步对route.stack里的callbacks们进行筛选(选出带有err参数的错误处理回调),this.handler.length!=4这个判断就是用来过滤这些普通callbacks的,但它会误伤在上一个层级中的中间件分发函数route.dispatch(这货也只有3个参数),故我们使用!this.middle对其进行放行(this.middle这个标识只存在于callbacks当中,而不会存在在中间件的dispatch分发函数中)。

我们再来看看 handle_request 方法,这个方法是针对普通中间件被匹配上的情景的。

Layer.prototype.handle_request = function(req,res,next){
    this.handler(req,res,next);
}
复制代码

嗯...相当简单,是吗?

但这样会产生一个bug,或则说和设计初衷不符。

有这样一种情景,

错误处理中间件被匹配上,但没有人传递err给它,它会走到handle_request,而我们其实是不希望它被执行的,故我们需要做些处理。

Layer.prototype.handle_request = function(req,res,next){
    if(this.handler.length === 4)return next();
    this.handler(req,res,next);
}
复制代码
分发

我们仍然是在route.dispatch中对中间件进行分发,

首先因为我们调用分发的时候,可能是通过handle_request调用的也可能是通过handle_error调用的,它们所传递的参数是不同的,故我们需要对 route.dispatch 接收到的参数进行兼容处理

Route.prototype.dispatch = function(req,res,next){
	...
    if(arguments.length==4){ //说明是通过handle_error调用的  需要做兼容
        args = slice.call(arguments);
        req = args[1];
        res = args[2];
        out = args[3];
        next(args[0]);
    }else{
    	next();
    }
    ...
}
复制代码

其次我们在对每一层layer进行方法认证的时,需要对中间件进行放行

...
function next(err){
    if((layer.method === req.method.toLowerCase())||layer.method === 'all'){
      ...
    }else if(layer.method==='middle'){ //对中间件进行放行
      if(err){
        layer.handle_error(err,req,res,next);
      }else{
        layer.handle_request(req,res,next);
      }
    }else{
      next(err);
    }
}
...

复制代码

最后我们需要注意一点的是,在dispatch方法中,若分发的是一个路由且在执行完一个cb后调用next且传递了err,那么分发应该终止,跳出,进行下一条路由或则中间件的匹配。

function next(err){
	...
    if(err&&self.route){ //或则用!self.methods['middle']来判断
    	return out(err);
    }
    ...
}
复制代码

测试用例2与功能分析

const express = require('../lib/express.js');
const app = express();

const r1 = express.Router();
  r1.use(function(req,res,next){
    res.setHeader('Content-Type','text/html;charset=uft-8');
    res.write('middle:/  ');
    next();
  });
  r1.use('/1',function(req,res,next){
    res.write('middle:/1  ');
    next();
  });

app
  // .get('/user',user) //这种get套子路由的需求是不存在的
  .use('/user',r1) //<--- 看这里,很关键!!!
  .get('/user',function(req,res,next){
    res.end('get:/user')
  })
  .get('/user/1',function(req,res,next){
    res.end('get:/user/1')
  })
.listen(8080);

>>> /user
<<< middle:/ get:/user

>>> /user/1
<<< /middle:/ middle:/1 get:/user/1
复制代码

这里演示的是express中 路由容器 的功能,我们可以通过express.Router()来创建一个路由容器,在这个路由容器中我们也能往里面注册路由啊注册中间件啊什么的。

最后我们需要把这个路由容器注入到一个注册的中间当中,这样就形成了路由的嵌套,当我们匹配到这个中间件时,会接着往下匹配它所注入的路由容器里所注册的路由。

但需要注意的一点是,在路由容器中进行匹配时,是要省略掉它父容器的路径的,像上面的栗子当中,当请求路径为/user,匹配上中间件.use('/user',r1),再往里匹配时就需要去掉/usr,请求路径就变为了/,而路由容器里注册的第一个layer就是.use(fn),是一个匿名中间件,它的路径默认即为/,故这个匿名中间件也会被匹配上。

功能实现

首先我们需要在框架接口层添加一个Router接口

//express.js
...
createApplication.prototype.Router = Router;
...
复制代码

接着我们再魔改一下router

//router/index.js

function Router(){
    function router(req,res,next){
        router.handle(req,res,next);
    }
    router.stack = [];
    Object.setPrototypeOf(router,proto);
    return router;
}

let proto = Object.create(null);
// 把原本挂在Router.prototype上的方法都挂载到proto上去
proto.route = ...
复制代码

这样我们就能使用express()express.Router()两种方式来得到一个router。

接下来我们需要对注册中间件的方法进行一些兼容,因为此时注册中间件时候存放的不再是一般的回调函数,而是一个路由容器,我们希望路由匹配成功时对router.handle方法递归,而不是调用dispatch

proto.use = function(path){
    let handlers,router,route;
    if(typeof(path)!='string'){
    	handlers = slice.call(arguments);
        path = '/';
        if(arguments[0].stack) router = arguments[0]; // 利用router相较于普通callbcak有一个stack属性来作为标识
    }else{
    	handlers = slice.call(arguments,1);
        if(arguments[1].stack) router = arguments[1];
    }
    
    if(!router){
        let layer = new Layer(path,router);
        this.stack.push(layer);
    }else{
        ... //普通中间件注册时走这里
    }
}
复制代码

这样我们就完成了路由嵌套的大体框架。

但有一点我们需要注意,我们说过子路由的路径都是相对于父路由的,So我们需要在递归router.handle方法之前,对req.url做出一些修改

proto.handle = function(req,res,next){
    let self = this
    	,index =0
        ,removed
        ...
    ...
    if(!layer.route){
    	removed = layer.path;
        req.url = req.url.slice(removed.length)
        if(err){
          layer.handle_error(err,req,res,next); //这样我们传入的req.url是经过裁剪过的
        }else{
          layer.handle_request(req,res,next);
        }
    }
    ...
}
复制代码

经过上面的修改,假若我们请求的路径为/user/abc,中间件注册时的路径为/user,那么裁剪过后的路径为/abc,最终会传入router.handle递归时的req.url即为/abc

但这里其实是有一个小bug的,若请求路径为/user,它被裁剪后路径就变成''了,而我们中间不填写path时的默认路径为/,于是乎这样就不能匹配上。除此之外若请求路径/user/abc,而注册路径为/user/,子注册路径为/abc这样的也会存在一些bug,匹配不上。

故我们需要统一对路径做一些处理,

proto.patch_path = function(req){
  if(req.url === '/')return; //默认req.url 为/
  if(req.url === '')return req.url = '/'; 
  if(req.url.endsWith('/'))return req.url = req.url.slice(0,req.url.length-1);
};
复制代码

在进入handle方法中的next之前调用这个方法

proto.handle = function(req,res,next){
    ...
    {pathname} = url.parse(req.url,true);
    self.patch_path(pathname);
    ...
}
复制代码

以上我们就实现了中间件以及嵌套路由容器的所有功能与细节。

动态路由

测试用例与功能分析

app
  .param('name',function(req,res,next,value,key){ //不支持在一个动作里同时注册多个cb,但支持分开注册cb到同一个动态参数下
    console.log(slice.call(arguments,3)); //[ 'ahhh', 'name' ]
    next();
  })
  .param('name',function(req,res,next,value,key){
    console.log('同个动态参数下绑定的第二个函数');
    next();
  })
  .get('/account/:name/:id/',function(req,res,next){
    res.setHeader('Content-Type','text/html;charset=utf-8');
    res.write('name:'+req.params.name+'<br/>');
    res.end('id:'+req.params.id);
  })
.listen(8080);

>>> /account/ahhh/1

<<< 输出到控制台
['ahhh','name']
同个动态参数下绑定的第二个函数

<<<  输出到页面
name:ahhh
id:1
复制代码

动态路由允许我们只写一条路由就能匹配上多条不同的请求,前提是这些请求满足我们注册动态路由时所规定的格式。

例如上栗中的/account/:name/:id,就只会匹配上/account/xx/xx而匹配不上/account/xx或则/account/xx/xx/xx

且动态路由,顾名思义,路由啊路由,只针对路由,中间件是木有动态中间一说的,嘛。。。中间件本身就不是固定路径匹配嘛。

除此之外,当动态路由匹配上时,那些被匹配上的 动态参数 还会被缓存起来,我们能够在req.params拿到这些数据。

这里提到一个名词,动态参数 ,就是指注册动态路由时以:开头的那些路径分块,:name:id它们都是一个动态参数。

嗯。。。上面的例子中还有一个面生的,.param方法,这个方法能在注册的动态路由上挂载一些钩子(准确来说是在这些动态路由的动态参数上挂载的钩子),这些钩子和动态路由参数是绑定的。

当一条动态路由被匹配上,它会先执行这些钩子函数,这些钩子函数执行时,能在内部能拿到他们对应绑定的那些动态参数的值,从而能针对这些参数进行一些预处理。而动态路由会等待它身上的钩子函数全部执行完毕后才在最后执行它注册的回调。就如上面的示例,会先打印控制台的输出,再给予页面响应。

至于动态路由与param方法的应用场景在这个系列的第一篇举过栗子,这里就不再赘述 点我了解更多哦

[warning] 这些钩子函数在一次请求当中只会执行一次

功能实现

我们先来理一理我们要做哪些事,其实如果小伙伴们是耐着性子看到这里的,不难发现我们实现普通路由、中间件这些功能时做的事情都是一样。(嗯。。套路都是共通的)

无非就是先注册路由,然后再分发路由,分发路由的时候我们先要对路由进行匹配然后再进行分发。

动态路由也是如此,我们先要注册路由,然后再去分发。但细节上是有些不同的,

注册动态路由

比如我们在注册动态路由的时候不可能像注册普通路由一样把地址存起来,动态路由的path长成/xxx/:a/:b这种带:的鬼样子,鬼大爷认得到,真正的请求路径是不可能长这样的,更何况这代表的是一类路径,而不是一条真正的路径

so,我们需要对动态路径进行一些转化,转换成一种计算机能识别的且还能代表一类路径的规则,这样我们在匹配路由时才能达到我们想要的效果。

emm...想一想,规则?一类?还计算机能认识? 有没有想起什么熟悉的东东!嘿,对就是正则!

想好了转化的方法,我们来看看我们该在什么时候动这手脚,emmm...当然是存路径的时候就要转化好啦,不然等到它检查路径时再转化吗?这不就耽误响应时间了嘛。

嗯。。。那我们是什么时候存放路径的呢?是在router[method]中调用router.route往router.stack里注册route时在layer下挂载的path属性来存放路径的。(有点绕哇,没理清的小伙伴可以回顾一下之前的内容)

//layer.js

function Layer(path){
    ...
    this.keys = [];
    this.path = path;
    this.regexp = self.pathToRegexp(path,keys);
    ...
}

Layer.prototype.pathToRegexp = function(path,keys){
    if(path.includes(':')){ // /:name/:id
        path = path.replace(/:([^\/]+)/g,function(){ //:name,name
          keys.push({
            name:arguments[1]
            ,optional:false
            ,offset:arguments[2]
          });
          return '([^\/]+)';
        });
        path += '[\\/]?$'; //注意需以$结尾
        return new RegExp(path); // --> /\/user\/([^/]+)\/([^/]+)[\/]?/
    }
}
复制代码

由于普通路由存放路径时也是这样存放的,为了避免误伤,我们特意在转换方法里包了一层判断来确认是否需要转换,即path.includes(':'),当路径种包含:时,我们就转换。

另外我们还往layer下添加了一个keys属性,用来存放每一个动态路径参数(的名字)。这是为了在进行路径匹配时,能拿到请求路径中对应每一个动态路径参数所处位置分块的值。

分发动态路由

说回动态路由,此时我们已经完成了动态路由的注册以及把路径转换好存储了起来。接下来我们还需要作出修改的是,匹配路由时对路径的检测。

我们是在layer.match方法中对路径进行匹配(检测)的

Layer.prototype.match = function(path){
    //验证是不是普通路由的路径并进行匹配
    ...
    //验证是不是中间件的路径并进行匹配
    ...
    //验证是不是动态路由的路径并进行匹配
    if(this.route&&this.regexp){
        let matches = this.regexp.exec(path); // /user/1
        if(matches){
          this.params = {};
          for(let i=1;i<matches.length;++i){
            let name = this.keys[i-1].name;
            let val = matches[i];
            this.params[name] = val;
          }
          return true;
        }
    }
    return false;
}
复制代码

注意,我们在上面不仅对是不是动态路由的路径进行了检查和匹配,我们还往这一层路由身上添加了一个params属性,这个属性里存放的是该动态路由的动态路径参数的键值对,其键为一个个动态路径参数,其值为动态路径参数所处位置对应的请求路径中那一部分的值。

在前面的测试示例中,我们演示了一个功能,就是在我们动态路由注册的回调中我们能通过req.params来获得请求路径种动态路径参数所对应的值,比如注册的动态路由为/user/:username,请求路径为/user/ahhh,我们就能通过req.params.username来拿到ahhh这个值。而这也是为什么我们上面在对路径进行匹配成功时还在这个路由信息对象下挂载一个params属性的原因。

我们只需要在调用真正的回调之前,先把这个路由信息对象下的params赋给req即可达到上述的功能。

...
if(!err&&layer.route.handle_method(req.method)){ //如果是查找错误处理中间件会跳过
      req.params = layer.params;
      layer.handle_request(req,res,next);
...
复制代码
param方法

param也分为注册和分发两个阶段

注册很简单,只需要找一个地方把钩子函数们存起来即可,存在哪里呢?router?还是route?

答案是router。

钩子函数绑定的是动态路径参数,而不是动态路由,这意味着不同的动态路由若是拥有相同的动态路径参数,则会触发相同的钩子函数,So我们要缓存的话,应该是缓存在router这个层级,而不是一层route当中。

同样的先在application接口层添加一个接口

Application.prototype.param = function(){
    this.lazyrouter();
    this._router.param.apply(this._router,arguments);
    return this;
}
复制代码

接着在router中具体实现

function Rouer(){
    ...
    router.paramCallbacks = [];
    ...
}
proto.param = function(param,handler){
    this.paramCallbacks[param] = this.paramCallbacks[param]?this.paramCallbacks[param]:[];
    this.paramCallbacks[param].push(hanlder);
}
复制代码

这样我们就完成了param的注册


我们再来看分发怎么实现

首先我们要知道,什么时候我们开始分发param注册的钩子函数?

嗯,当然是要在一个动态路由被匹配上,在动态路由注册的回调执行之前,我们就需要对param注册的钩子们进行分发。

在分发的时候我们仍然需要先对动态路由的动态路径参数进行匹配,若存储的钩子和这些动态路径参数匹配的上,则会执行,否则对下一个钩子进行匹配,直到所有存储的钩子被匹配完毕,我们最后才开始分发动态路由注册的回调们。

So,我们先要对动态路由的分发做一些修改

...
if(!err&&layer.route.handle_methods(req.method)){
    req.params = layer.params;
    self.process_params(layer,req,res,()=>{ //动态路由的分发将在param分发完毕后开始
        layer.handle_request(req,res,next);
    });
}
复制代码

接着我们来实现我们param的匹配和分发

这个设计思路和之前路由的存储和分发是类似的,并且由于这些钩子函数中也可能存在异步函数,我们也采取next递归的方式来遍历动态路径参数和钩子们。

//先处理param回调,处理完成后才会执行路由回调
proto.process_params = function (layer, req, res, out) {
  let keys = layer.keys;
  let self = this;
  //用来处理路径参数
  let paramIndex = 0 /**key索引**/, key/**key对象**/, name/**key的值**/, val, callbacks, callback ,callbackIndex;
  //调用一次param意味着处理一个路径参数
  function param() {
    if (paramIndex >= keys.length) {
      return out();
    }
    key = keys[paramIndex++];//先取出当前的key //之所以用keys而不用req.params是为了好实用i++遍历
    name = key.name;// uid
    val = req.params[name];
    callbacks = self.paramCallbacks[name];// 取出等待执行的回调函数数组
    if (!val || !callbacks) {//如果当前的key没有值,或者没有对应的回调就直接处理下一个key
      return param();
    }
    callbackIndex = 0;
    execCallback();
  }
  
  function execCallback() {
    callback = callbacks[callbackIndex++];
    if (!callback) {
      return param();//如果此key已经没有回调等待执行,则代表本key处理完毕,该执行一下key
    }
    callback(req, res, execCallback, val, name);
  }
  
  param();
  
};
复制代码

其它

*路径

express中能使用*代表任意路径

这个功能实现很简单,

咯,只需要在layer.match中修改一丢丢

Layer.prototype.match = function(path){
    if(path===this.path||this.path==='*') return true;
    ...
}
复制代码

支持注册路由时以'/'结尾

嗯。。。清楚了整个系统以后,要实现这个功能也很简单,我们只需要在缓存路径时对路径做一些修改即可

那么问题来了,我们什么时候缓存路径的?

..思考两秒..

1

2

嗯,答案是在我们 new Layer 的时候,至于什么时候new layer的。。。咳咳,还没反应过来的同学可以回看啦

function Layer(path,handler){
    ...
    if(path!=='/'&&path.endsWith('/'))path=path.slice(0,path.length-1);  //注册时统一将路径修改为不以'/'结尾的
    this.path = path;
    ...
}
复制代码

源码

仓库:点我!点我~


ToBeContinue...

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值