[TOC]
## Pre-Notify
前情提要
> [Express深入理解與簡明實現](https://juejin.im/post/5a9b35de6fb9a028e46e1be5)
>[Express源碼級實現の路由全解析(上闋)](https://juejin.im/post/5aa0a309f265da23826d8afb)
本篇是 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.js`、`application.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`方法的應用場景在這個系列的第一篇舉過栗子,這里就不再贅述 [點我了解更多哦](https://juejin.im/post/5a9b35de6fb9a028e46e1be5)
>[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;
//調用一次param意味著處理一個路徑參數
function param() {
if (paramIndex >= keys.length) {
return out();
}
key = keys[paramIndex++];//先取出當前的key //之所以用keys而不用req.params是為了好實用i++遍歷
name = key.name;// uid
val = layer.params[name];
callbacks = self.paramCallbacks[name];// 取出等待執行的回調函數數組
if (!val || !callbacks) {//如果當前的key沒有值,或者沒有對應的回調就直接處理下一個key
return param();
}
execCallback();
}
let callbackIndex = 0;
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;
...
}
```