- Pre-Notify
- 項目目錄
- express.js 和 application.js
- app對象之http服務器
- app對象之路由功能
- 注冊路由
- 接口實現
- 分發路由
- 接口實現
- router
- 測試用例1與功能分析
- 功能實現
- router和route
- layer
- 注冊路由
- 注冊流程圖
- 路由分發
- 分發流程圖
- 測試用例2與功能分析
- 功能實現
- Q
- 為什么選用next遞歸遍歷而不選用for?
- 我們從Express的路由系統設計中能學到什么?
- 源碼
## Pre-Notify
閱讀本文前可以先參考一下我之前那篇簡單版的express實現的文章。
> [Express深入理解與簡明實現](https://juejin.im/post/5a9b35de6fb9a028e46e1be5)
相較于之前那版,此次我們將實現Express所有核心功能。
預計分為:路由篇(上、下)、中間件篇(上、下)、炸雞篇~
(づ ̄ 3 ̄)づ Let's Go!
## 項目目錄
```
iExpress/
|
|
| - application.js #app對象
|
| - html.js #模板引擎
|
| - route/
| | - index.js #路由系統(router)入口
| | - route.js #路由對象
| | - layer.js #router/route層
|
| - middle/
| | - init.js #內置中間件
|
| - test-case/
| | - 測試用例文件1
| | - ...
|
·- express.js #框架入口
```
## express.js 和 application.js
在簡單版Express實現中我們已經知道,將express引入到項目后會返回一個函數,當這個函數運行后會返回一個`app`對象。(這個app對象是原生**http的超集**)
其中,*express.js*模塊導出的就是那個運行后會返回app對象的函數
```
// test-case0.js
let express = require('./express.js');
let app = express(); //app對象是原生http對象的超集
...
app.listen(8080); //調用的其實就是原生的server.listen
```
上個版本中因為實現的功能較簡單,只用了一個express.js文件就搞定了,而在這個版本中我們需要專門用一個模塊*application.js*來存放app相關的部分
```
//express.js
const Application = require('./application.js'); //app
function createApplication(){
return new Application(); //app對象
}
module.exports = createApplication;
```
## app對象之http服務器
`app對象` 最重要的一個作用是用來啟動一個`http服務器`,通過`app.listen`方法我們能間接調用到原生的.listen方法來啟動一個服務器。
```
//application.js
function Application(){}
Application.prototype.listen = function(){
function done(){}
let server = http.createServer(function(req,res,done){
...
})
server.listen.apply(server,arguments);
}
```
## app對象之路由功能
`app對象`的另外一個重要作用,也就是`Express`框架的主要作用是實現路由功能。
路由功能是個蝦?
路由功能能讓服務器針對客戶端不同的請求路徑和請求方法做出不同的回應。
而要實現這個功能我們需要做兩件事情:`注冊路由` 和 `路由分發`
>[warning] 為了保證app對象作為接口層的清晰明了,app對象只存放接口,而真正實現部分是委托給路由系統(router.js)來處理的。
### 注冊路由
當一個請求來臨時,我們可以依據它的請求方式和請求路徑來決定服務器**是否給予響應以及怎么響應。**
而我們怎么讓服務器知道哪些請求該給予響應以及怎樣響應呢?
這就是注冊路由所要做的事情了。
在服務器啟動時,我們需要對服務器想要給予回應的請求做上記錄,**先存起來**,這樣在請求來臨的時候服務器就能對照這些記錄分別作出響應。
>[warning]注意
每一條記錄都對應一條請求,記錄中一般都包含著這條請求的請求路徑和請求方式。但一條請求不一定只對應一條記錄(中間件、all方法什么的)。
#### 接口實現
我們通過在 app對象 上掛載`.get`、`.post`這一類的方法來實現路由的注冊。
其中`.get`方法能匹配請求方式為**get**的請求,`.post`方法能匹配請求方式為**post**的請求。
請求方式一共有33種,每一種都對應一個app下的方法,emmm...我們不可能寫33遍吧?So我們需要利用一個`methods包`來幫助我們減少代碼的冗余。
```
const methods = require('methods');
// 這個包是http.METHODS的封裝,區別在于原生的方法名全文大寫,后者全為小寫。
methods.forEach(method){
Application.prototype[method] = function(){
//記錄路由信息到路由系統(router)
this._router[method].apply(this._router,slice.call(arguments));
return this; //支持app.get().get().post().listen()連寫
}
}
//以上代碼是以下的簡寫
Application.prototype.get = fn
Application.prototype.post = fn
...
```
>[info] 可以發現,app.get等只是一個對外接口,實際要做的事情我們都是委托給router這個類來做的。
### 分發路由
當請求來臨時我們就需要依據記錄的路由信息來作出對應的響應了,這個過程我們稱之為`分發路由/dispatch`
上面是廣義的分發路由的含義,但其實分發路由其實包括兩個過程,`匹配路由` 和 `分發路由`。
- 匹配路由
當一個請求來臨時,我們需要知道我們所記錄的路由信息中是否囊括這條請求。(如果沒有囊括,一般來說服務器會對客戶端作出一個提示性的回應)
- 分發路由
當路由匹配上,則會執行被匹配上的路由信息中所存儲的回調。
#### 接口實現
```
Application.prototype.listen = function(){
let self = this;
let server = http.createServer(function(req,res){
function done(){ //沒有匹配上路由時的回調
res.end(`Cannot ${req.method} ${req.url}`);
}
//將路由匹配的具體處理交給路由系統的handle方法
//handle方法中會對匹配上的路由再進行路由分發
self._router.handle(req,res,done);
})
server.listen.apply(server,arguments);
}
```
## router
### 測試用例1與功能分析
```
const express = require('../lib/express');
const app = express();
app
.get('/hello',function(req,res,next){
res.write('hello,');
next();
},function(req,res,next){
res.write('world');
next();
})
.get('/other',function(req,res,next){
console.log('不會走這里');
next();
})
.get('/hello',function(req,res,next){
res.end('!');
})
.listen(8080,function(){
let tip = `server is running at 8080`;
console.log(tip);
});
<<< 輸出
hello,world!
```
相較于之前簡單版的express實現,完整的express還支持同一條路由同時添加多個`cb`,以及分開對同一條路由添加`cb`。
這是怎么辦到的呢?
最主要的是,我們存儲路由信息時,將`路由`和`方法`組織成了一種類似于二維數組的二維數據形式
即在`router`(路由容器)里存放一層層`route`,**而又在每一層route(路由)里再存放一層層**`callbcak`。
這樣我們通過遍歷router中的route,匹配上一個route后,就能在這個route下找到所這個route注冊的callbacks。
### 功能實現
#### router和route
> 在`router`(路由容器)里存放一層層`route`,而又在每一層route(路由)里再存放一層層`callbcak`。
首先我們需要在有兩個構造函數來生產我們需要的router和route對象。
```
//router/index.js
function Router(){
this.stack = [];
}
```
```
//router/route.js
function Route(path){
this.path = path;
this.stack = [];
this.methods = {};
}
```
接著,我們在Router和Route中生產出的對象下都開辟了一個`stack`,這個stack用來存放一層層的**層/layer**。這個`layer(層)`,在Router和Route中所存放的東東是不一樣的,在router中存放的是一個層層的`route`(即Router的實例),而route中存放的是一層層的`方法`。
它們各自的`stack`里存放的對象大概是長這樣的
```
//router.stack
[
{
path
handler
}
,{
...
}
]
//route.stack
[
{
handler
}
,{
...
}
]
```
可以發現,這兩種stack里存放的對象都包含handler,并且第一種還包含一個path。
第一種包含`path`,這是因為在`router.stack`遍歷時是**匹配路由**,這就需要比對`path`。
而兩種都需要有一個handler屬性是為什么呢?
我們很容易理解第二個stack,`route.stack`里存放的就是我們設計時準備要存放的`callbacks`,**那第一個stack里的handler存放的是什么呢?**
當我們路由匹配成功時,我們需要接著遍歷這個路由,這個`route`,這就意味著我們需要個鉤子在我們路由匹配成功時執行這個操作,**這個遍歷route.stack的鉤子就是第一個stack里對象所存放的handler**(即是下文中的`route.dispatch`方法)。
#### layer
實際項目中我們將`router.stack`和`route.stack`里存放的對象們封裝成了同一種對象形式——layer
一方面是為了語義化,一方面是為了把對layer對象(原本的routes對象和methods對象)進行操作的方法都歸納到layer對象下,以便維護。
```
// router/layer.js
function Layer(path,handler){
this.path = path; //如果這一層代表的存放的callbcak,這為任意路徑即可
this.handler =handler;
}
//路由匹配時,看路徑是否匹配得上
Layer.prototype.match = function(path){
return this.path === path?true:false;
}
```
#### 注冊路由
```
//在router中注冊route
http.METHODS.forEach(METHOD){
let method = METHOD.toLowercase();
Router.prototype[method] = function(path){
let route = this.route(path); //在router.stack里存儲一層層route
route[method].apply(route,slice.call(arguments,1)); //在route.stack里存儲一層層callbcak
}
}
Router.prototype.route = function(path){
let route = new Route(path);
let layer = new Layer(path,route.dispatch.bind(route)); //注冊路由分發函數,用以在路由匹配成功時遍歷route.stack
layer.route = route; //用以區分路由和中間件
this.stack.push(layer);
return route;
}
```
```
//在route中注冊callback
http.METHODS.forEach(METHOD){
let method = METHOD.toLowercase();
Route.prototype[method] = function(){
let handlers = slice.call(arguments);
this.methods[method] = true; //用以快速匹配
for(let i=0;i<handlers.length;++i){
let layer = new Layer('/',handler[i]);
layer.method = method; //在遍歷route中的callbacks依據請求方法進行篩選
this.stack.push(layer);
}
return this; //為了支持app.route(path).get().post()...
}
}
```
##### 注冊流程圖

#### 路由分發
整個路由分發就是遍歷我們之前用`router.stack`和`route.stack`所組成的二維數據結構的過程。
我們將遍歷`router.stack`的過程稱之為**匹配路由**,將遍歷`route.stack`的過程稱之為**路由分發**。
匹配路由:
```
// router/index.js
Router.prototype.handle = function(req,res,done){
let self = this,i = 0,{pathname} = url.parse(req.url,true);
function next(err){ //err主要用于錯誤中間件 下一章再講
if(i>=self.stack.length){
return done;
}
let layer = self.stack[i++];
if(layer.match(pathname)){ //說明路徑匹配成功
if(layer.route){ //說明是路由
if(layer.route.handle_method){ //快速匹配成功,說明route.stack里存放有對應請求類型的callbcak
layer.handle_request(req,res,next);
}else{
next(err);
}
}else{ //說明是中間件
//下一章講先跳過
next(err);
}
}else{
next(err);
}
}
next();
}
```
路由分發
上面在我們最終匹配路由成功時,會執行`layer.handle_request`方法
```
// layer.js中
Layer.prototype.handle_request = function(req,res,next){
this.handler(req,res,next);
}
```
此時的handler為`route.dispatch` (忘記的同學可以往上查看注冊路由部分)
```
//route.js中
Route.prototype.dispatch = function(req,res,out){ //注意這個out接收的是遍歷route.stack時的next()
let self = this,i =0;
function next(err){
if(err){ //說明回調執行錯誤,跳過當前route.stack的遍歷交給錯誤中間件來處理
return out(err);
}
if(i>=self.stack.length){
return out(err); //說明當前route.stack遍歷完成,繼續遍歷router.stack,進行下一條路由的匹配
}
let layer = self.stack[i++];
if(layer.method === req.method){
self.handle_request();
}else{
next(err);
}
}
next();
}
```
##### 分發流程圖

### 測試用例2與功能分析
```
const express = require('express');
const app = express();
app
.route('/user')
.get(function(req,res){
res.end('get');
})
.post(function(req,res){
res.end('post');
})
.put(function(req,res){
res.end('put');
})
.delete(function(req,res){
res.end('delete');
})
.listen(3000);
```
以上是一種`resful`風格的借口寫法,如果理清了我們上面的東東,其實這個實現起來相當簡單。
無非就是在調用`.route()`方法的時候**返回我們的route**(route.stack里的一層),這樣再調用`.get`等其實就是調用`Route.prototype.get`等了,就能夠順利往這一層的`route`里添加不同的`callbcak`了。
>[warning] **注意:** .listen此時不能與其它方法名連用,因為.get等此時返回的是route而不是app
### 功能實現
```
//application.js中
Application.prototype.route = function(path){
this.lazyrouter();
let route = this._router.route(path);
return route;
}
```
另外要注意的是,需要讓 `route.prototype[method]` 返回route以便連續調用。
So easy~
## Q
### 為什么選用next遞歸遍歷 而不 選用for?
emmm...我想說express源碼是這么設計的,嗯,這個答案好不好??(′???`?)
其實可以用for的哦,我有試過的啦,
修改router/index.js 下的 handle方法如下
```
let self = this
,{pathname} = url.parse(req.url,true);
for(let i=0;i<self.stack.length;++i){
if(i>=self.stack.length){
return done();
}
let layer = self.stack[i];
if(layer.match(pathname)){
if(!layer.route){
}else{
if(layer.route&&layer.route.handle_method(req.method)){
// let flag = layer.handle_request(req,res);
for(let j=0;j<layer.route.stack.length;++j){
let handleLayer = layer.route.stack[j];
if(handleLayer.method === req.method.toLowerCase()){
handleLayer.handle_request(req,res);
if(handleLayer.stop){
return;
}
}
}//遍歷handleLayer
}//快速匹配成功
}//說明是路由
}//匹配路徑
}
```
我們調用`.get`等方法時就不再需要傳遞next和傳入next參數
```
app
.get('/hello',function(req,res){
res.write('hello,');
// this.stop = true;
this.error = true; //交給錯誤處理中間件來處理。。 中間件還沒實現,但原則上來說是能行的
// next();
},function(req,res,next){
res.write('world');
this.stop = true; //看這里!!!!!!!!!!!!layer遍歷將在這里結束
// next();
})
.get('/other',function(req,res){
console.log('不會走這里');
// next();
})
.get('/hello',function(req,res){
res.end('!'); //不會執行,在上面已經結束了
})
.listen(8080,function(){
let tip = `server is running at 8080`;
console.log(tip);
});
```
在上面這段代碼中`this.stop=true`的作用就相當于不調用`next()`,而不在回調身上掛載`this.stop`時就相當于調用了next()。
原理很簡單,就是在遍歷每一層`route.stack`時(**注意是route的stack不是router的stack**),檢查`layer.handler`是否設置了stop,如果設置了就停止遍歷,不論是路由layer(router.stack)的遍歷還是callbacks layer(route.stack)的遍歷。
**那么問題來了,有什么理由非要用next來遍歷嗎?**
答案是:for無法支持異步,而next能!這里的支持異步是指,**當一個callbcak執行后需要拿到它的異步結果在下一個callbcak執行時用到**。嗯...for就干不成這事了,for無法感知它執行的函數中是否調用了異步函數,也不知道這些異步函數什么能執行完畢。
### 我們從Express的路由系統設計中能學到什么?
emmm...私認為`layer`這個抽象還是不錯的,把對每一層(不關心它具體是route還是callback)的層級相關操作都封裝掛載到這個對象下,嗯。。。回顧了一下類誕生的初衷~
當然next這種鉤子式遞歸遍歷也是可以的,我們知道了它的應用場景,支持異步~
emmm...學到什么...我們不僅要模仿寫一個框架,更重要的是,嗯..要思考!要思考!同學們,學到了個什么,要學以致用...嗯...嘿哈!
所以我半夜還在碼這篇文章到底學到了個蝦??emmm...
世界那么大——
## 源碼
```
//express.js
const Application= require('./application.js');
const Router = require('./router');
function createApplication(){
return new Application;
}
createApplication.Router = Router;
module.exports = createApplication;
```
```
//application.js
const http = require('http');
const url = require('url');
const Router = require('./router');
function Application(){
}
Application.prototype.lazyrouter = function(){
if(!this._router){
this._router= new Router();
}
};
http.METHODS.forEach(function(METHOD){
let method = METHOD.toLowerCase();
Application.prototype[method] = function(){
this.lazyrouter();
this._router[method].apply(this._router,arguments);
return this;
}
});
Application.prototype.listen = function(){
let self = this;
let server = http.createServer(function(req,res){
function done(){
let tip = `Cannot ${req.method} ${req.url}`;
res.end(tip);
}
self._router.handle(req,res,done);
});
server.listen.apply(server,arguments);
};
module.exports = Application;
```
```
//router/index.js
//這一部分兼容了一些后一章要將的內容
let http = require('http');
const Route = require('./route.js');
const Layer = require('./layer.js');
const slice = Array.prototype.slice;
const url = require('url');
function Router(){
function router(){
router.handle(req,res,next);
}
Object.setPrototypeOf(router,proto);
router.stack = [];
router.paramCallbacks = [];
return router;
}
let proto = Object.create(null);
proto.route = function(path){
let route = new Route(path)
,layer = new Layer(path,route.dispatch.bind(route));
layer.route = route;
this.stack.push(layer);
return route;
};
http.METHODS.forEach(function(METHOD){
let method = METHOD.toLowerCase();
proto[method] = function(path){
let route = this.route(path); //注冊路由層
route[method].apply(route,slice.call(arguments,1)); //注冊路由層的層
}
});
proto.handle = function(req,res,done){
let index = 0,self = this
,removed
,{pathname} = url.parse(req.url,true);
function next(err){
if(index>=self.stack.length){
return done();
}
if(removed){
req.url = removed+req.url;
removed = '';
}
let layer = self.stack[index++];
if(layer.match(pathname)){
if(!layer.route){
} else{
if(layer.route&&layer.route.handle_method(req.method)){
layer.handle_request(req,res,next);
}else{
next(err);
}
}
}else{
next(err);
}
}
next();
};
module.exports = Router;
```
```
// router/route.js
let http = require('http');
let Layer = require('./layer.js');
let slice = Array.prototype.slice;
function Route(path){
this.path = path;
this.methods = {};
this.stack = [];
}
http.METHODS.forEach(function(METHOD){
let method = METHOD.toLowerCase();
Route.prototype[method] = function(){
let handlers = slice.call(arguments);
this.methods[method] = true;
for(let i=0;i<handlers.length;++i){
let layer = new Layer('/',handlers[i]);
layer.method = method;
this.stack.push(layer);
}
return this;
}
});
Route.prototype.handle_method = function(method){
return this.methods[method.toLowerCase()]?true:false;
};
Route.prototype.dispatch = function(req,res,out){
let self = this
,index = 0;
// let q = 0
function next(err){
if(err){
return out(err); //出現錯誤,退出當前路由交給錯誤中間件處理
}
if(index>=self.stack.length){
return out(); //當前路由的layer已經遍歷完 跳出 繼續匹配下一條路由
}
let layer = self.stack[index++];
if(layer.method === req.method.toLowerCase()){
layer.handle_request(req,res,next);
}else{
next(err);
}
}
next();
};
module.exports = Route;
```
```
// router/layer.js
function Layer(path,handler){
this.path = path;
this.handler = handler;
}
Layer.prototype.match = function(path){
return path === this.path?true:false;
};
Layer.prototype.handle_request = function(req,res,next){
this.handler(req,res,next);
};
Layer.prototype.handle_error = function(err,req,res,next){
if(this.handler.length !=4){
return next(err);
}
this.handler(err,req,res,next);
};
module.exports = Layer;
```