Express原始碼級實現の路由全解析(下闋)

Cris_冷崢子發表於2019-03-01
  • 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…

相關文章