[TOC]
// ## 導讀
// 我有一個問題和不太成熟的想法,不知道該不該提!
// 掘金既然支持目錄TOC,為什么不能把目錄放在一個顯眼的地方,比如左邊?一大片空白不用非要放在右下角和其它面板搶,emmm...
// - express概要
// - 創建一個服務器以及分發路由
// - 簡單實現1
// - 脈絡
// - 實現路由
// - .all方法和實現
// - 應用
// - 實現
// - 在app下添加.all方法(用來存儲路由信息對象)
// - 對遍歷路由信息對象時的規則判斷做出調整
// - 中間件
// - 概要
// - 中間件和路由的異同
// - 1.都會包裝成路由信息對象
// - 2.匹配條數的不同
// - 3.匹配路徑上的不同
// - next與錯誤中間件
// - 實現
// - 在app下添加.use方法
// - 改造request方法
// - params
// - params常用屬性
// - params與動態路由
// - 實現
// - 動態路由與動態路由屬性的實現
// - params其它屬性的實現
// - .param方法
// - api一覽
// - 注意事項
// - 應用場景
// - 和中間件的區別
// - 實現
// - 添加param方法
// - 修改request
## express概要
express是一個node模塊,它是對node中`http`模塊的二次封裝。
express相較于原生http模塊,為我們提供了作為一個服務器最重要的功能:`路由`。
路由功能能幫助我們**根據不同的路徑不同的請求方法來返回不同的內容**。
除此之外express還支持 `中間件` 以及其他類似于 `req.params` 這些小功能。
## 創建一個服務器以及分發路由
```
let express = require('express');
let app = express();
//針對不同的路由進行不同的返回
app.get('/eg1',function(req,res){
res.end('hello');
});
app.post('/eg1',function(req,res){
res.end('world');
});
app.listen(8080,function(){
console.log(`server started at 8080`);
});
```
可以發現,引入express后會返回一個**函數**,我們稱之為express。
express這個函數運行后又會返回一個對象,這個對象就是包裝后的http的**server對象**。
這個對象下有很多方法,這些方法就是express框架為我們提供的新東東了。
上面用到了`.get`方法和`.post`方法,get和post方法能幫助我們對路由進行分發。
什么是路由分發呢?其實就是在原生`request`回調中依據`請求方法`和`請求路徑`的不同來返回不同的響應內容。
就像在上面的示例中我們通過`.get`和`.post`方法對路徑為`/eg1`的請求**各**綁定了一個回調函數,
但這個兩個回調函數不會同時被調用,因為請求方法只能是一種(get或則post或則其它)。
如果請求方法是**get**請求路徑是`/eg1`則會返回`.get`中所放置的回調
```
<<< 輸出
hello
```
否則若請求路徑不變,請求方法是**post**則會返回`.post`方法中放置的回調
```
<<< 輸出
world
```
### 簡單實現1
#### 脈絡
我們首先要有一個函數,這個函數運行時會返回一個app對象
```
function createApplication(){
let app = function(req,res){};
return app;
}
```
這個app對象下還有一些方法`.get`,`.post`,`.listen`。
```
app.get = function(){}
app.post = function(){}
app.listen = function(){}
```
其中`app.listen`其實就是原生http中的`server.listen`。
`app`就是原生中的`request`回調。
```
app.listen = function(){
let server = http.createServer(app);
server.listen.apply(server,arguments); //事件回調中,不管怎樣this始終指向綁定對象,這里既是server,原生httpServer中也是如此
}
```
#### 實現路由
我們再來想想`app.get`這些方法到底做了什么。
其實無非定義了一些路由規則,對匹配上這些規則的路由進行一些針對性的處理(執行回調)。
上面一句話做了兩件事,匹配規則 和 執行回調。
這兩件事執行的時機是什么時候呢?是服務器啟動的時候嗎?不是。
**是當接收到客戶端請求的時候**。
這意味著什么?
當服務器啟動的時候,其實這些代碼已經執行了,它們根本不會管請求是個什么鬼,只要服務器啟動,代碼就執行。
所以我們需要將**規則**和**回調**先**存起來**。(類似于發布訂閱模式)
```
app.routes = [];
app.get = function(path,handler){
app.routes.push({
method:'get'
,path
,handler
})
}
```
上面我們定義了一個`routes`數組,用來存放每一條規則和規則所對應的回調以及請求方式,即`路由信息對象`。
但有一個地方需要我們優化。不同的請求方法所要做的事情都是相同的(只有method這個參數不同),我們不可能每增加一個就重復的寫一次,請求的方法是有非常多的,這樣的話代碼會很冗余。
```
//http.METHODS能列出所有的請求方法
>>>
console.log(http.METHODS.length);
<<<
33
```
So,為了簡化我們的代碼我們可以遍歷`http.METHODS`來創建函數
```
http.METHODS.forEach(method){
let method = method.toLowerCase();
app[method] = function(path,handler){
app.routes.push({
method
,path
,handler
})
}
}
```
然后我們會在請求的響應回調中用到這些路由信息對象。而**響應回調在哪呢?**
上面我們已經說過其實`app`這個`函數對象`就是原生的`request`回調。
接下來我們只需要等待請求來臨然后執行這個app回調,**遍歷每一個路由信息對象進行匹配,匹配上了則執行對應的回調函數。**
```
let app = function(req,res){
for(let i=0;i<app.routes.length;++i){
let route = app.routes[i];
let {pathname} = url.parse(req.url);
if(route.method==req.method&&route.path==pathname){
route.handler(req,res);
}
}
}
```
## .all方法和實現
### 應用
`.all`也是一個路由方法,
```
app.all('/eg1',function(req,res){})
```
和普通的`.get`,`.post`這些和請求方法直接綁定的路由分發不同,.all方法只要路徑匹配得上**各種請求方法**去請求這個路由都會得到響應。
還有一種更暴力的使用方式
```
app.all('*',function(req,res){})
```
這樣能匹配所有方法所有路勁,**all!**
**通常它的使用場景是對那些沒有匹配上的請求做出兼容處理。**
### 實現
#### 在app下添加.all方法(用來存儲路由信息對象)
和一般的請求方法是一樣的,只是需要一個標識用以和普通方法區分開。
這里是在method取了一個`all`關鍵之作為method的值。
```
app.all = function(method,handler){
app.routs.push({
method:'all'
,path
,handler
})
}
```
#### 對遍歷路由信息對象時的規則判斷做出調整
另外還需要在`request`回調中對規則的匹配判斷做出一些調整
```
if((route.method==req.method||route.method=='all')&&(route.path==pathname||route.path=='*')){
route.handler(req,res);
}
```
## 中間件
### 概要
中間件是什么鬼呢?中間件嘛,顧名思義中間的件。。。emmm,我們直接說說它的作用吧!
中間件主要是**在請求和真正響應之間**再加上一層處理,
處理什么呢?比如說權限驗證、數據加工神馬的。
這里所謂的**真正響應**,你可以把它當做`.get`這些路由方法所要執行的那些個回調。
```
app.use('/eg2',function(req,res,next){
//do something
next();
})
```
### 中間件和路由的異同
#### 1.都會包裝成路由信息對象
服務器啟動時,中間件也會像路由那樣被存儲為一個一個`路由信息對象`。
#### 2.匹配條數的不同
`路由`只要匹配上了一條就會立馬返回數據并結束響應,不會再匹配第二條。
而`中間件`只是一個臨時中轉站,對數據進行過濾或則加工后會繼續往下匹配。
**So,中間件一般放在文件的上方,路由放在下方。**
#### 3.匹配路徑上的不同
中間件進行路徑匹配時,只要**開頭**匹配的上就能執行對應的回調。
這里所謂的開頭意思是:
假若中間件要匹配的路徑是`/eg2`,
那么只要url.path是以`/eg2`開頭,像`/eg2`,`/eg2/a`,`/eg2/a/b`即可。(/eg2a這種不行,且必須以/eg2開頭,a/eg2則不行)
而路由匹配路徑時必須是完全匹配,也就是說規則若是`/eg2`則只有`/eg2`匹配的上。這里的完全匹配其實是針對路徑的 **/的數量** 來說的,因為`動態路由`中匹配的值不是定死的。
除此之外,中間件可以不寫路徑,當不寫路徑時express系統會為其默認填上`/`,即全部匹配。
### next與錯誤中間件
中間件的回調相較于路由多了一個參數`next`,next是一個函數。
這個函數能讓中間件的回調執行完后繼續向下匹配,如果**沒有寫next也沒有在中間件中結束響應**,那么請求會一直處于`pending`狀態。
next還可以進行傳參,如果傳了慘,表示程序運行出錯,將匹配`錯誤中間件`進行處理**且只會交由錯誤中間件處理**。
錯誤中間件相較于普通中間件在回調函數中又多了一個參數`err`,用以接收中間件`next()`傳遞過來的錯誤信息。
```
app.use('/eg2',function(req,res,next){
//something wrong
next('something wrong!');
})
app.use('/eg2',function(err,req,res,next){
console.log('i catch u'+err);
next(err); //pass to another ErrorMiddle
});
// 錯誤中間接收了錯誤信息后仍然允許接著向下傳遞
app.use('/eg2',function(err,req,res,next){
res.end(err);
});
```
其實錯誤中間件處理完成后也能匹配路由
```
app.use('/eg2',function(req,res,next){
//something wrong
next('something wrong!');
})
app.use('/eg2',function(err,req,res,next){
console.log('i catch u'+err);
next(err); //pass to another ErrorMiddle
});
app.get('/eg2',function(req,res){
//do someting
})
```
### 實現
#### 在app下添加.use方法
像路由方法一樣,其實就是用來存儲路由信息對象
```
app.use = function(path,handler){
if(typeof handler != 'function'){ //說明只有一個參數,沒有path只有handler
handler = path;
path = "/"
}
app.routes.push({
method:'middle' //需要一個標識來區分中間件
,path
,handler
});
};
```
#### 改造request方法
```
let app = function(req,res){
const {pathname} = url.parse(req.url, true);
let i = 0;
function next(err){
if(index>=app.routes.length){ //說明路由信息對象遍歷完了仍沒匹配上,給出提示
return res.end(`Cannot ${req.method} ${pathname}`);
}
let route = app.routes[i++];
if(err){ //是匹配錯誤處理中間件
//先判斷是不是中間件
if(route.method == 'middle'){
//如果是中間件再看路徑是否匹配
if(route.path=='/'||pathname.startsWith(route.path+'/')||route.path==pathname){
//再看是否是錯誤處理中間件
if(route.handler.length==4){
route.handler(err,req,res,next);
}else{
next(err);
}
}else{
next(err);
}
}else{
next(err); //將err向后傳遞直到找到錯誤處理中間件
}
}else{ //匹配路由和普通中間件
if(route.method == 'middle'){ //說明是中間件
if(route.path=='/'||pathname.startsWith(route.path+'/')||route.path==pathname){
route.handler(req,res,next);
}else{ //此條路由沒有匹配上,繼續向下匹配
next();
}
}else{ //說明是路由
if((route.method==req.method||route.method=='all')&&(route.path==pathname||route.path=='*')){
//說明匹配上了
route.handler(req,res);
}else{
next();
}
}
}
}
next();
}
```
我們可以把對錯誤中間件的判斷封裝成一個函數
```
function checkErrorMiddleware(route){
if(route.method == 'middle'&&(route.path=='/'||pathname.startsWith(route.path+'/')||route.path==pathname)&&route.handler.length==4){
return true;
}else{
next(err);
}
}
```
## params
### params常用屬性
express為我們在`request`回調中的req對象參數下封裝了一些常用的屬性
```
app.get('/eg3',function(req,res){
console.log(req.hostname);
console.log(req.query);
console.log(req.path);
})
```
### params與動態路由
```
app.get('/article/:artid',function(req,res){
console.log(req.artid);
})
>>>
/article/8
<<<
8
```
### 實現
#### 動態路由與動態路由屬性的實現
首先因為路由規則所對應的路徑我們看得懂,但機器看不懂。
So我們需要在存儲路由信息對象時,對路由的規則進行**正則提煉**,將其轉換成正則的規則。
```
...
app[method] = function(path,handler){
let paramsNames = [];
path = path.replace(/:([^\/]+)/g,function(/*/:aaa ,aaa*/){
paramsNames.push(arguments[1]); //aaa
return '([^\/]+)'; // /user/:aaa/:bbb 被提煉成 /user/([^\/]+)/([^\/]+)
});
layer.reg_path = new RegExp(path);
layer.paramsNames = paramsNames;
}
app.routes.push(layer);
}
```
我們拿到了一個`paramsNames`包含所有路徑的分塊,并將每個分塊的值作為了一個新的param的名稱,
我們還拿到了一個`reg_path`,它能幫助我們對請求的路徑進行分塊匹配,匹配上的每一個子項就是我們新param的值。
對`request`路由匹配部分做出修改
```
if(route.paramsNames){
let matchers = pathname.match(req.reg_path);
if(matchers){
let params = {};
for(let i=0;i<route.paramsNames.length;++i){
params[route.paramsNames[i]] = matchers[i+1]; //marchers從第二項開始才是匹配上的子項
}
req.params = params;
}
route.handler(req,res);
}
```
#### params其它屬性的實現
這里是內置中間件,即在框架內部,它會在第一時間被注冊為路由信息對象。
實現很簡單,就是利用`url`模塊對`req.url`進行解析
```
app.use(function(req,res,next){
const urlObj = url.parse(req.url,true);
req.query = urlObj.query;
req.path = urlObj.pathname;
req.hostname = req.headers['host'].split(':')[0];
next();
});
```
## .param方法
### api一覽
```
app.param('userid',function(req,res,next,id){
req.user = getUser(id);
next();
});
```
next和中間件那個不是一個意思,這個next執行的話會執行被匹配上的那條動態路由所對應的回調
id為請求時userid這個路徑位置的實際值,比如
```
訪問路徑為:http://localhost/ahhh/9
動態路由規則為:/username/userid
userid即為9
```
### 注意事項
**必須配合動態路由!!**
param和其它方法最大的一點不同在于,**它能對路徑進行截取匹配**。
什么意思呢,
上面我們講過,路由方法路徑匹配時必須**完全匹配**,而中間件路徑匹配時需要**開頭一樣**。
而`param`方法**無需開頭一樣,也無需完全匹配**,它只需要路徑中某一個**分塊**(即用`/`分隔開的每個路徑分塊)和方法的規則對上即可。
### 應用場景
當不同的路由中包含相同路徑分塊且使用了相同的操作時,我們就可以對這部分代碼進行提取優化。
比如每個路由中都需要根據id獲取用戶信息
```
app.get('/username/:userid/:name',function(req,res){}
app.get('/userage/:userid/:age',function(req,res){}
app.param('userid',function(req,res,next,id){
req.user = getUser(id);
next();
});
```
### 和中間件的區別
相較于中間件它更像是一個**真正的鉤子**,它不存在放置的先后問題。
如果是中間件,一般來說它必須放在文件的上方,而param方法不是。
導致這樣結果的本質原因在于,**中間件類似于一個路由**,它會在請求來臨時加入的路由匹配隊列中參與匹配。而**param并不會包裝成一個路由信息對象**也就不會參與到隊列中進行匹配,
它的觸發時機是在**它所對應的那些動態路由被匹配上時**才會觸發。
### 實現
#### 添加param方法
在app下添加了一個param方法,并且創建了一個`paramHandlers`對象來存儲這個方法所對應的回調。
```
app.paramHandlers = {};
app.param = function(name,handler){
app.paramHandlers[name] = handler; //userid
};
```
#### 修改request
修改`request`回調中 動態路由被匹配上時的部分
當動態路由被匹配上時,通過它的動態路由參數來遍歷`paramHandlers`,看是否設置了對應的`param回調`,
```
if(route.paramsNames){
let matchers = pathname.match(route.reg_path);
if(matchers){
let params = {};
for(let i=0;i<route.paramsNames.length;++i){
params[route.paramsNames[i]] = matchers[i+1];
}
req.params = params;
for(let j=0;j<route.paramsNames.length;++j){
let name = route.paramsNames[j];
let handler = app.paramHandlers[name];
if(handler){
//回調觸發更改在了這里
//第三個參數為next,這里把route.handler放在了這里,是讓param先執行再執行該條路由
return handler(req,res,()=>route.handler(req,res),req.params[name]);
}else{
return route.handler(req,res);
}
}
}else{
next();
}
}
```
---
## 源碼
```
let http = require('http');
let url = require('url');
function createApplication() {
//app其實就是真正的請求監聽函數
let app = function (req, res) {
const {pathname} = url.parse(req.url, true);
let index = 0;
function next(err){
if(index>=app.routes.length){
return res.end(`Cannot ${req.method} ${pathname}`);
}
let route = app.routes[index++];
if(err){
//先判斷是不是中間件
if(route.method == 'middle'){
//如果是中間件再看路徑是否匹配
if(route.path=='/'||pathname.startsWith(route.path+'/')||route.path==pathname){
//再看是否是錯誤處理中間件
if(route.handler.length==4){
route.handler(err,req,res,next);
}else{
next(err);
}
}else{
next(err);
}
}else{
next(err); //將err向后傳遞直到找到錯誤處理中間件
}
}else{
if(route.method == 'middle'){ //中間件
//只要請求路徑是以此中間件的路徑開頭即可
if(route.path=='/'||pathname.startsWith(route.path+'/')||route.path==pathname){
route.handler(req,res,next);
}else{
next();
}
}else{ //路由
if(route.paramsNames){
let matchers = pathname.match(route.reg_path);
if(matchers){
let params = {};
for(let i=0;i<route.paramsNames.length;++i){
params[route.paramsNames[i]] = matchers[i+1];
}
req.params = params;
for(let j=0;j<route.paramsNames.length;++j){
let name = route.paramsNames[j];
let handler = app.paramHandlers[name];
if(handler){ //如果存在paramHandlers 先執行paramHandler再執行路由的回調
return handler(req,res,()=>route.handler(req,res),req.params[name]);
}else{
return route.handler(req,res);
}
}
}else{
next();
}
}else{
if ((route.method == req.method.toLowerCase() || route.method == 'all') && (route.path == pathname || route.path == '*')) {
return route.handler(req, res);
}else{
next();
}
}
}
}
}
next();
};
app.listen = function () { //這個參數不一定
let server = http.createServer(app);
//server.listen作為代理,將可變參數透傳給它
server.listen.apply(server, arguments);
};
app.paramHandlers = {};
app.param = function(name,handler){
app.paramHandlers[name] = handler; //userid
};
//此數組用來保存路由規則
app.routes = [];
// console.log(http.METHODS);
http.METHODS.forEach(function (method) {
method = method.toLowerCase();
app[method] = function (path, handler) {
//向數組里放置路由對象
const layer = {method, path, handler};
if(path.includes(':')){
let paramsNames = [];
//1.把原來的路徑轉成正則表達式
//2.提取出變量名
path = path.replace(/:([^\/]+)/g,function(){ //:name,name
paramsNames.push(arguments[1]);
return '([^\/]+)';
});
// /user/ahhh/12
// /user/([^\/]+)/([^\/]+)
layer.reg_path = new RegExp(path);
layer.paramsNames = paramsNames;
}
app.routes.push(layer);
};
});
//all方法可以匹配所有HTTP請求方法
app.all = function (path, handler) {
app.routes.push({
method: 'all'
, path
, handler
});
};
//添加一個中間件
app.use = function(path,handler){
if(typeof handler != 'function'){ //說明只有一個參數,沒有path只有handler
handler = path;
path = "/"
}
app.routes.push({
method:'middle' //需要一個標識來區分中間件
,path
,handler
});
};
//系統內置中間件,用來為請求和響應對象添加一些方法和屬性
app.use(function(req,res,next){
const urlObj = url.parse(req.url,true);
req.query = urlObj.query;
req.path = urlObj.pathname;
req.hostname = req.headers['host'].split(':')[0];
next();
});
return app;
}
module.exports = createApplication;
```