[TOC]
## Pre-Notify
閱讀本文前可以先參考一下我之前那篇簡單版的express實現的文章。
> []()
相較于之前那版,此次我們將實現Express所有核心功能。
(づ ̄ 3 ̄)づ Let's Go!
## 框架目錄
```
express/
|
|
| - application.js #app對象
|
| - html.js #模板引擎
|
| - route/
| | - index.js #路由系統(router)入口
| | - route.js #路由對象
| | - layer.js #router/route層
|
| - middle/
| | - init.js #內置中間件
|
·- express.js #框架入口
```
## express.js 和 application.js
之前我們說過,將express引入到項目后會返回一個函數,當這個函數運行后會返回一個`app`對象,這個app對象是原生http的超集。
我們先來實現這個生產app對象的函數
```
// express.js
cont Application = require('./application.js');
function createApplication(){
return new Application;
}
module.exports = createApplication;
```
我們在`express.js`中引入了一個`application.js` 文件,這個application文件導出的即是我們的`app對象`。
## app對象之http服務器
`app對象` 的主要作用是用來啟動一個`http服務器`,通過`.listen`方法。
```
const http = require('http');
function Application(){
}
Application.prototype.listen = function(){
let server = http.createServer(function(req,res){
//作出響應
})
server.listen.apply(server,arguments);
}
```
## app對象之路由功能
`app對象`的另外一個重要作用,也就是`Express`框架的主要作用是實現路由功能。
路由功能,即讓服務器針對客戶端不同的請求路徑和請求方法做出不同的回應。
而要實現這個功能我們需要做兩件事情:`注冊路由` 和 `路由分發`
>[warning] 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;
}
}
//以上代碼是以下的簡寫
Application.prototype.get = fn
Application.prototype.post = fn
...
```
因為`app`這個對象只是作為一個`對外接口層`,為了保證此接口層一目明了,我們把實際累死累活的工作外包給其它專門的模塊來處理,比如說注冊路由,我們交給了`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。
```
Application.prototype.lazyrouter = function(){
if(!this._router){
this._router = new Router();
}
};
```
```
...
self.lazyrouter();
self._router.handle(req,res,done);
...
```
## router路由系統
```
function Router(){
function router(req,res,next){ //為支持子路由容器功能做準備
router.handle(req,res,next);
}
Object.setPrototypeOf(router,proto);
router.stack = [];
router.paramCallbacks = {};
router.use(init); // 加載內置中間件
return router;
}
let proto = Object.create(null);
proto.route = function(path){} //添加一層路由
methods.forEach(function(method){ //33個注冊路由的普通方法:get、post
})
proto.use = function(path,handler){} //注冊中間件
proto.param = function(name,handler){} //注冊param回調
proto.handle = function(req,res,out){} //路由匹配函數 包括路由分發
proto.process_params = function(){} //處理param回調的handle
```
### router、route和layer
當我們調用app.get('/user',cb1,cb2...)方法的時候,其實我們調用的是router的get方法。
在這個方法里我們將`路由信息`抽象成了一個對象——route,并往router里的`stack`存放了一層,同時我們也在route里開辟了一個`stack`,我們也往route里的stack里存放了一層。
```
methods.forEach(function(method){
proto[method] = function(path){
let route = this.route(path); //注冊路由,往Router里添加一層
route[method].apply(route,slice.call(arguments,1)); //向Route里添加一層
return this;
}
});
```
route是routerの`stack/棧`中所存放的一層路由,實際中我們是將route又包裝成了一個`layer`對象存儲在routerのstack中。
```
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;
};
```
上面我們說到`route`其實也擁有自己的`stack`,這個stack里存放的是也是一層層`layer`對象。
```
methods.forEach(function(method){
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;
}
});
```
綜上,我們可以發現layer是一個相對的感念,我們可以將router中每一層稱作一個`layer`,也可以將route里的每一層稱作一個`layer`。
這兩個layer的不同之處在于前者在layer的handler屬性下掛載是`route.dispatch`這個方法,而后者掛載的是我們使用注冊路由方法時所注冊的那些個**回調**。
也就是說**route里的每一層存放的才是我們真正的響應函數**,而router里每一層存放的`handle`是幫助我們路由匹配成功后對路由進行分發的。簡而言之,后者存儲的`dispatch方法`會調用前者所存儲的每個**響應回調**。
### handle和dispatch
#### 遍歷二維數據
`router.proto.handle方法` 主要對請求進行路由匹配
```
proto.handle = function(req,res,out){
let index = 0,self = this;
let {pathname} = url.parse(req.url,true);
function next(err){
if(index>=self.stack.length){
return out; //路由信息清單(stack)遍歷完仍沒匹配上
}
let layer = self.stack[index++];
if(layer.match(pathname)){
if(layer.route){ //說明匹配的是路由
if(layer.route.handler_method(req.method)){
layer.handle_request(req,res,next);
}else{
next(err); //路徑匹配但方法名不匹配
}
}else{
//說明匹配的是中間件
}
}else{
next(err); //路徑不匹配
}
}
next();
}
```
`route.prototype.dispatch` 方法,當路由匹配上時,會執行當初該條路由注冊時所注冊的回調函數(請求方法想匹配的回調函數)。
```
Route.prototype.dispatch = function(req,res,out){
let idx = 0,self = this;
function next(err){
if(err){ //如果一旦在路由函數中出錯了,則會跳過當前路由的所有方法匹配
return out(err);
}
if(idx >= self.stack.length){
return out(); //此out接收的是Router里的next,即跳轉到下一條路由進行匹配
}
let layer = self.stack[idx++];
if(layer.method == req.method.toLowerCase()){
layer.handle_request(req,res,next);
}else{
next();
}
}
next();
};
```
>[warning] 上面兩個方法中的layer的層級是不一樣的,通過它倆的配合使用達到了了對二維數據的遍歷。
#### handle_method快速匹配
我們從上面的源碼中可以發現,其實我們對一個路由的請求方法進行了兩次匹配,為什么要這樣做呢?
第一次我們是在對route進行匹配,route里有一個屬性`methods`,這個屬性告訴我們這個route下存放的響應回調都是針對哪些請求方法的。這樣,我們就只用通過查看這個`methods`屬性就能知道route中存放的響應回調支持哪些類型的請求方式,當請求方法不匹配時我們就能直接跳過當前路由而不需要再次進入route對routeのstack的每一層進行匹配,So達到了一種**快速匹配**的效果,這就是`handle_method`方法的作用。
```
Route.prototype.handle_method = function(method){
method =method.toLowerCase();
return this.methods[method];
};
```
### app.route方法
用法示例:
```
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');
})
```
要實現上面這個方法,很簡單,只需將route這一路由層作為`.route`方法的返回值即可
```
Application.prototype.route = function(path){
this.lazyrouter();
return this._router.route(path);
};
```
這樣我們通過`.route`后再使用`.get`、`.post`等,就是直接調用的`route`里的`.get`和`.post`方法,**是往route里添加層**。
## 路由容器
```
const user = express.Router(); //router
user.use(function(req,res,next){
console.log('我是一個中間件')
next();
});
//在子路徑里的路徑是相對于父路徑的 //下面是指/user/2
user.get('/2',function(req,res,next){
res.end('2');
});
//use表示使用中間件,只需要匹配前綴即可
app.use('/user',user);
```
Router就是一個路由容器,而上面這個栗子其實是在Router.stack里的一層route里添加了一個新的`router`。
這是什么意思呢?
當原本的Router.stack里的一層route被匹配時,若這個route存儲的是一個新的路由容器,那么會對這個新的路由容器里的路由再次進行匹配。并且這個新的路由容器里的路由的路徑都是相當于它們的父級的。
像上面的栗子,假若請求路徑為`/user/1`,那么子路由系統的中間件會被匹配上但.get路由不會被匹配上,如果請求路徑為`/user/2`,那么都會匹配上。
### express.Router
修改express.js文件
```
//添加
const Router = require('./router);
createApplication.Router = Router;
```
So這個路由容器就是我們原本的`router`,不過是一個全新的實例。
### Router和router
修改 `Router function`
```
function Router(){
function router(req,res,next){ //為支持子路由容器功能做準備
router.handle(req,res,next);
}
Object.setPrototypeOf(router,proto);
router.stack = [];
router.paramCallbacks = {};
router.use(init); // 加載內置中間件
return router;
}
```
我們將原本的Router構造函數改造成了一般的函數,不論我們使用`new`方式(調用.get(path,fn)來注冊路由)
還是`Router()`方式(調用.get(path,router))我們都會得到`router`這個函數。
**注意**,`router`函數將會在路由匹配方法(router.prototype.handle)路由匹配時被執行,它執行時會再次調用`router.handle`,即遞歸調用遍歷這個新的router容器內的路由。
```
...
let layer = self.stack[idx++];
if(layer.method == req.method.toLowerCase()){
layer.handle_request(req,res,next);
...
```
這時有一個問題,子路由的路徑是省略了父路由的路徑的,為了能容路徑匹配正確,我們需要將傳遞給遞歸的那個`router.handle`的`req`的`url`稍作修改
```
...
if(layer.match(pathname)){
if(!layer.route){ //這一層是中間件層 /usr/2
removed = layer.path; // /user
req.url = req.url.slice(removed.length); // /2 //從這里以后開始獲取url會是不準確的,若在回調中用到了req.url需要注意
if(err){
layer.handle_error(err,req,res,next)
...
```