引用:https://github.com/WangZhechao/expross
1.簡介
這篇文章是分析express框架的源碼辩稽,網(wǎng)上關(guān)于源碼的分析已經(jīng)很多了似芝,這篇文章通過仿制一個express框架龙誊,以測試驅(qū)動的方式不斷迭代,正向理解express源碼。
這篇文章中express源碼的版本為(v4.15.4),文章的整體思路參考早期的另一篇文章(https://segmentfault.com/a/1190000005833119)捺宗。
準備學(xué)習(xí)express源碼的基本原理,前提條件式有一定的express框架使用經(jīng)驗川蒙,來掌握其背后的原理蚜厉。
2.框架初始化
在仿制express框架前,首先完成兩件事:
- 確認需求
- 確認結(jié)構(gòu)
首先確認需求畜眨,從express官網(wǎng)入手昼牛,網(wǎng)站有一個Hello world的示例程序,想要仿制express康聂,該程序肯定要通過測試贰健,將該代碼負值保存到測試目錄test/index.js。
const express = require('express');
const app = express();
app.get('/', function(req, res) {
res.rend('Hello World!');
});
app.listen(3000, function() {
console.log('Example app listening on port 3000!');
});
接下來恬汁,確認框架名稱及目錄伶椿,框架名稱為express,目錄結(jié)構(gòu)如下:
express
|
|-- lib
| |
| |-- express.js
|
|-- test
| |
| |-- index.js
|
|-- index.js
然express/index.js文件加載lib目錄下的express文件。
module.exports = require('./lib/express');
通過測試前兩行可以判斷l(xiāng)ib/express.js導(dǎo)出的結(jié)果應(yīng)該是一個函數(shù)脊另,所以在express.js文件中添加如下代碼:
function createApplication() {
return {};
}
exports = module.exports = createApplication;
測試程序中包含兩個函數(shù)导狡,所以暫時將createApplication函數(shù)體現(xiàn)如下:
function createApplication() {
return {
get: function() {
console.log(express().get function);
},
listen: function() {
console.log('express().listen function');
}
}
}
雖然無法得到想要的結(jié)果,但至少可以將測試程序跑通偎痛,函數(shù)的核心內(nèi)容可以在接下來的步驟中不斷完善旱捧。至此,初始框架搭建完畢踩麦,測試test/index.js文件的前兩行:
const express = require('../lib/express');
const app = express();
運行node test/index.js查看結(jié)果
2.第一次迭代
本節(jié)是express的第一次迭代枚赡,主要的目標是將當前的測試用例功能完整的實現(xiàn),一共分兩部分:
- 實現(xiàn)http服務(wù)器靖榕。
- 實現(xiàn)get路由請求标锄、
實現(xiàn)http服務(wù)器比較簡單,參考node.js官網(wǎng)實現(xiàn):
const http = require('http');
const hostname = '127.0.0.1';
const post = 3000;
const server = http.createServer((req, res) => {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end('Hello World\n');
});
server.listen(port, hostname, () => {
console.log(`Server running at http://${hostname}:${port}`);
});
參考案例茁计,實現(xiàn)express的lsiten函數(shù):
listen: function(port, cb) {
var server = http.createServeer(function(req, res) {
console.log('http.createServer...');
});
return server.listen(port, cb);
}
當前l(fā)isten函數(shù)包含兩個參數(shù)料皇,但是http.listen有很多重載函數(shù),為了和http.listen一致星压,可以將函數(shù)設(shè)置為http.listen'代理'践剂,這樣可以保持express().listen和http.listen的參數(shù)統(tǒng)一。
listen: function(port. cb) {
var server = http.createServer(function(req, res) {
console.log('http.createServer...');
});
// 代理
return server.listen.apply(server, arguments);
}
在listen函數(shù)中娜膘,我們攔截了所有http請求逊脯,每次http請求都會打印http.createServer...,之所以攔截http請求竣贪,是因為express需要分析每次http請求军洼,根據(jù)http請求的不同來處理不同的業(yè)務(wù)邏輯。
在底層:
一個http請求主要包括請求行演怎、請求頭和消息體匕争,node.js將常用的數(shù)據(jù)封裝為http.IncomingMessage類,在上面的代碼中req就是該類的一個對象爷耀。
每個http請求都會對應(yīng)一個http響應(yīng)甘桑,一個http響應(yīng)主要包括:狀態(tài)行、響應(yīng)頭歹叮、消息跑杭,node.js將常用的數(shù)據(jù)封裝為http.ServerResponse類,在上面的代碼中res就是該類的一個對象咆耿。
不僅僅是node.js德谅,基本上所有的http服務(wù)框架都包含request和response兩個對象,分別代表著http的請求和響應(yīng)萨螺,負責(zé)服務(wù)端和瀏覽器的交互窄做。
在上層:
服務(wù)器后臺代碼根據(jù)http請求的不同宅荤,綁定不同的邏輯,在真正的http請求到來時浸策,匹配這些http請求,執(zhí)行與之對應(yīng)的邏輯惹盼,這個過程就是web服務(wù)器的執(zhí)行流程庸汗。
對于這些http請求的管理,有一個專有名詞:路由管理手报,每個http請求就默認為一個路由蚯舱,常見的路由策略包括:URL、HTTP請求名詞等等掩蛤,單不僅僅限定這些枉昏,所有http請求頭上的參數(shù)其實都可以進行判斷區(qū)分,例如使用user-agent字段來判斷移動端揍鸟。
不同的框架對于路由的管理策略不同兄裂,但不管怎樣,都需要一組管理http請求和業(yè)務(wù)邏輯映射的函數(shù)阳藻,測試用例中的get函數(shù)就是路由管理中的一個函數(shù)晰奖,主要負責(zé)添加get請求。
既然知道路由管理的重要腥泥,這里就創(chuàng)建一個router數(shù)組匾南,負責(zé)管理所有路由映射,參考express框架蛔外,抽象出每個路由的基本屬性:
- path請求路徑:例如:/books蛆楞、/books/1。
- method請求方法:例如:GET夹厌、POST豹爹、PUT、DELETE尊流。
- handle處理函數(shù):
var router = [{
path: '*',
method: '*',
handle: function(req, res) {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('404');
}
}];
修改listen函數(shù)帅戒,將http請求攔截改為匹配router路由表,如果匹配成功崖技,執(zhí)行對應(yīng)的handle函數(shù)逻住,否則執(zhí)行router[0].handle函數(shù)。
listen: function(port, cb) {
var server = http.craeteServer(function() {
for(var i=1, len=router.length; i<len; i++) {
if((req.url === router[i].path || router[i],path === '*') &&
(req.method === router[i].method || router[i].method === '*') {
return router[i].handle && router[i].handle(req, res);
}
}
return router[0].handle && router[0].handle(req, res);
});
return server/listen.apply(server, arguments);
}
實現(xiàn)get路由請求非常簡單迎献,該函數(shù)主要是添加get請求路由瞎访,只需要將其信息加入到router數(shù)組即可。
get: function(path, fn) {
router.push({
path: path,
method: 'GET'
handle: fn
});
}
執(zhí)行測試用例時吁恍,報錯扒秸,提示res.send不存在播演。該函數(shù)并不是node.js原生函數(shù),這里在res上臨時添加函數(shù)send伴奥,負責(zé)發(fā)送響應(yīng)到瀏覽器写烤。
listen: function(port. cb) {
var server = http.createServer(function(req, res) {
if(!res.send) {
res.send = function(body) {
res.send = function(body) {
res.writeHead(200, {
'Content-Type': 'text/plain'
});
res.end(body);
};
}
}
......
});
return server.listen.apply(server, arguments);
}
在技術(shù)這次迭代之前,拆分整理一下程序目錄:
創(chuàng)建application.js文件拾徙,將createApplication函數(shù)中的代碼轉(zhuǎn)移到該文件洲炊,express.js文件只保留引用。
var app = require('./application');
function createApplication() {
return app;
}
exports = module.exports = createApplication;
整個目錄結(jié)構(gòu)如下:
express
|
|-- lib
| |
| |-- express.js
| |-- application.js
|
|-- test
| |
| |-- index.js
|
|-- index.js
最后尼啡,運行 node test/index.js暂衡,打開瀏覽器訪問http://127.0.0.1:3000/。
3.第二次迭代
本節(jié)是express的第二次迭代崖瞭,主要目標是構(gòu)建一個初步的路由系統(tǒng)狂巢。根據(jù)上一節(jié)的改動,目前的路由是用一個router數(shù)組進行描述管理书聚,對于router的操作有兩個唧领,分別是:application.get函數(shù)和application.listen函數(shù),前者用于添加雌续,后者用于處理疹吃。
按照面向?qū)ο蟮姆庋b法則,接下來將路由系統(tǒng)的數(shù)據(jù)和路由系統(tǒng)的操作封裝到一起定義一個Router類負責(zé)整個路由系統(tǒng)的主要工作:
var Router = function() {
this.stack = [{
path: '*',
method: '*',
handle: function(req, res) {
res.writeHead(200, {
'Content-Type': 'text/plain'
});
res.end('404');
}
}];
};
Router.prototype.get = function(path, fn) {
this.stack.push({
path: path,
method: 'GET',
handle: fn
});
};
Router.prototype.handle = function(req, res) {
for(var i=1, len=this.stack.length; i<len; i++) {
if((req.url === this.stack[i].path || this.stack[i].path === '*') &&
(req.method === this.stack[i].method || this.stack[i].method === '*')) {
return this.stack[i].handle && this.stack[i].handle(req, res);
}
}
return this.stack[0].handle && this.stack[0].handle(req, res);
};
修改原有的application.js文件的內(nèi)容:
var http = require('http');
var Router = require('./router');
exports = module.exports = {
_router: new Router(),
get: function(path, fn) {
return this._router.get(path, fn);
},
listen: function(port, cb) {
var self = this;
var server = http.createServer(function(req, res) {
if(!res.send) {
res.send = function(body) {
res.writeHead(200, {
'Content-Type': 'text/plain'
});
res.end(body);
};
}
return self._router.handle(req, res);
});
return server.listen.apply(server, arguments);
}
};
這樣組織之后西雀,路由方面的操作只和Router本身有關(guān)萨驶,而與application分離,使代碼更加清晰艇肴。
這個版本路由系統(tǒng)正常運行沒有問題腔呜,但是隨著路由不斷增多,this.stack數(shù)組會不斷地增大再悼,匹配效率會不斷降低核畴,為了解決效率的問題,需要分析路由的組成成分冲九。
目前在express中谤草,一個路由是由三部分組成:路徑、方法和處理函數(shù)莺奸。前兩者的關(guān)系并不是一對一關(guān)系丑孩,而是一對多關(guān)系,如:
GET books/1
PUT books/1
DELETE books/1
如果將路徑一樣的路由整合成一組灭贷,顯然效率會提高很多温学,于是引入Layer的概念。
這里將Router系統(tǒng)中this.stack數(shù)組中的每一項甚疟,代表一個Layer仗岖。每個Layer內(nèi)部包含3個變量:
- path, 表示路由的路徑
- handle逃延,代表路由的處理函數(shù)
- router,代表真正的路由
整體結(jié)構(gòu)如下圖所示:
------------------------------------------------
| 0 | 1 | 2 | 3 |
------------------------------------------------
| Layer | Layer | Layer | Layer |
| |- path | |- path | |- path | |- path |
| |- handle| |- handle| |- handle| |- handle|
| |- route | |- route | |- route | |- route |
------------------------------------------------
router 內(nèi)部
這里先創(chuàng)建Layer類:
function Layer(path, fn) {
this.handle = fn;
this.name = fn.name || '<anonymous>';
this.path = path;
}
// 簡單處理
Layer.prototype.request = function(req, res) {
var fn = this.handle;
if(fn) {
fn(req, res);
}
};
// 簡單匹配
layer.prototype.match = function(path) {
if(path === this.path || path === '*') {
return true;
}
return false;
};
再次修改Router類:
var Router = function() {
this.stack = [new Layer('*', function(req, res) {
res.write(200, {
'Content-Type': 'text/plain'
});
res.end('404');
})];
};
Router.prototype.handle = function(req, res) {
var self = this;
for(var i=1, len=self.stack.length; i<len; i++) {
if(self.stack[i].match(req.url)) {
return self.stack[i].handle_request(req, res);
}
}
return self.stack[0].handle_request(req, res);
};
Router.prototype.get = function(path, fn) {
this.stack.push(new Layer(path, fn));
};
運行node test/index.js轧拄,訪問http://127.0.0.1:3000/一切看起來很正常揽祥,但是上面的代碼忽略了路由的屬性method,這樣的結(jié)果導(dǎo)致如果測試用例存在問題:
app.put('/', function(req, res) {
res.send('put Hello World!');
});
app.get('/', function(req, res) {
res.send('get Hello World');
});
程序無法分清PUT和GET的區(qū)別檩电。
所以需要繼續(xù)完善路由系統(tǒng)中的Layer類的route屬性盔然,這屬性才是真正包含method屬性的路由。route結(jié)構(gòu)如下:
------------------------------------------------
| 0 | 1 | 2 | 3 |
------------------------------------------------
| item | item | item | item |
| |- method| |- method| |- method| |- method|
| |- handle| |- handle| |- handle| |- handle|
------------------------------------------------
route 內(nèi)部
創(chuàng)建Route類:
var Route = function(path) {
this.path = path;
this.stack = [];
this.method = {};
};
Route.prototype._handle_method = function(method) {
var name = method.toLowerCase();
return Boolean(this.methods[name]);
};
Route.prototype.get = function(fn) {
var layer = new Layer('/', fn);
this.method['get'] = true;
this.stack.push(layer);
};
Route.prototype.dispatch = function(req, res) {
var self = this,
method = req.method.toLowerCase();
for(var i=0, len=self.stack.length; i<len; i++) {
if(method === self.stack[i].method) {
return slef.stack[i].handle_request(req, res);
}
}
};
在上面的代碼中是嗜,斌沒有定義前面結(jié)構(gòu)圖中的item對象,而是使用了Layer對象進行替代挺尾,主要是為了方便快捷鹅搪,從另一個角度來看,其實而這存在很多共同點遭铺。另外丽柿,為了利于理解,代碼只實現(xiàn)了GET方法魂挂,其他方法的代碼實現(xiàn)是類似的甫题、既然有了Route類,接下來就該修改原有的Router類涂召,將Route集成于其中坠非。
Router.prototype.handle = function(req, res) {
var self = this,
method = req.method;
for(var i=0, len=self.stack.length; i<len; i++) {
if(self.stack[i].match(req.url) &&
self.stack[i].route && self.stack[i].route._handles_method(method)) {
return self.stack[i].handle_request(req, res);
}
}
return self.stack[0].handle_request(req, res);
};
Router.prototype.route = function route(path) {
var route = new Router(path);
var layer = new Layer(path, function(req, res) {
route.dispatch(req, res);
});
layer.route = route;
this.stack.push(layer);
return route;
};
Router.prototype.get = function(path, fn) {
var route = this.route(path);
route.get(fn);
return this;
};
運行node test/index.js,一切看起來和原來一樣果正。
這節(jié)內(nèi)容是創(chuàng)建一個完整的路由系統(tǒng)炎码,并在原始代碼的基礎(chǔ)上引入了Layer和Route兩個概念,并修改了大量的代碼秋泳,在技術(shù)本節(jié)之前總結(jié)一下目前的信息潦闲。
express
|
|-- lib
| |
| |-- express.js
| |-- application.js
| |-- router
| |
| |-- index.js
| |-- layer.js
| |-- route.js
|
|-- test
| |
| |-- index.js
|
|-- index.js
接著,總結(jié)了一下當前express各個部分的工作:
application代表一個應(yīng)用程序迫皱,express是一個工廠類來負責(zé)創(chuàng)建application對象歉闰。Router代表路由組件,負責(zé)應(yīng)用程序的整個路由系統(tǒng)卓起。組件內(nèi)部由一個layer數(shù)組構(gòu)成和敬,每個Layer代表一組路徑相同的路由信息国葬,具體信息存儲在Route內(nèi)部勘伺,每個Route內(nèi)部也是一個Layer對象,但是Route內(nèi)部的Layer和Router內(nèi)部的Layer是存在一定差異性的芍碧。
- Router內(nèi)部的Layer饲握,主要包含path私杜、route屬性蚕键。
- Route內(nèi)部的Layer,主要包含method衰粹、handle屬性锣光。
如果一個請求來臨,會先從頭到尾掃描router內(nèi)部的每一層铝耻,而處理每層的時候會對比URI誊爹,相同則掃描route的每一項,匹配成功則返回具體的信息瓢捉,沒有任何匹配則返回未找到频丘。
最后整個路由系統(tǒng)結(jié)構(gòu)如下:
--------------
| Application | ---------------------------------------------------------
| | | ----- ----------- | 0 | 1 | 2 | 3 | ... |
| |-router | ----> | | Layer | ---------------------------------------------------------
-------------- | 0 | |-path | | Layer | Layer | Layer | Layer | |
application | | |-route | ----> | |- method| |- method| |- method| |- method| ... |
|-----|-----------| | |- handle| |- handle| |- handle| |- handle| |
| | Layer | ---------------------------------------------------------
| 1 | |-path | route
| | |-route |
|-----|-----------|
| | Layer |
| 2 | |-path |
| | |-route |
|-----|-----------|
| ... | ... |
----- -----------
router
3.第三次迭代
本節(jié)是express的第三次迭代,主要目標是繼續(xù)完善路由系統(tǒng)泡态,主要工作有以下部分:
- 豐富接口搂漠,目前只支持get接口。
- 增加路由系統(tǒng)的流程控制某弦。
當前express框架只支持get接口桐汤,具體的接口時express提供的,內(nèi)部調(diào)用Router.get接口靶壮,而其內(nèi)部是對Route.get的封裝怔毛。
HTTP顯然不僅僅只有GET這一個方法,還包括很多類型腾降,例如:PUT拣度、POST、DELETE等等螃壤,每個方法單獨寫一個處理函數(shù)顯然是冗余的蜡娶,因為函數(shù)的內(nèi)容除了與函數(shù)名相關(guān)外,其他是一成不變的映穗。根據(jù)JavaScript腳本語言的特性窖张,這里可以通過HTTP的方法列表動態(tài)生成函數(shù)內(nèi)容。
想要動態(tài)生成函數(shù)蚁滋。
想要動態(tài)生成函數(shù)宿接,首先需要確定函數(shù)名稱。函數(shù)名就是node.js中HTTP服務(wù)器支持的方法名稱辕录,可以在官方文檔中獲取睦霎,具體參數(shù)是:http.METHODS。這個屬性是v0.11.8新增的走诞,如果node.js地獄該版本副女,需要手動建立一個方法列表,具體可以參考node.js代碼蚣旱。
express框架HTTP方法名的獲取封裝到另一個包碑幅,叫做methods戴陡,內(nèi)部給出了低版本的兼容動詞列表。
function getBasicNodeMethods() {
return [
'get',
'post',
'put',
'head',
'delete',
'options',
'trace',
'copy',
'lock',
'mkcol',
'move',
'purge',
'propfind',
'proppatch',
'unlock',
'report',
'mkactivity',
'checkout',
'merge',
'm-search',
'notify',
'subscribe',
'unsubscribe',
'patch',
'search',
'connect'
];
}
直到所支持的方法名列表數(shù)組后沟涨,剩下的只需要一個for循環(huán)生成所有的函數(shù)即可恤批。所有的動詞處理函數(shù)的核心內(nèi)容都在Router中。
http.METHODS.forEach(function(method) {
method = method.toLowerCase();
Route.prototype[method] = function() {
var layer = new Layer('/', fn);
layer.method = method;
this.methods[method] = true;
this.stack.push(layer);
return this;
};
});
接著修改Router:
http.METHODS.forEach(function(method) {
method = method.toLowerCase();
Router.prototype[method] = function(path, fn) {
var route = this.route(path);
route[method].call(route, fn);
return this;
};
});
最后修改application.js的內(nèi)容裹赴,這里改動較大喜庞,重新定義了一個Application類,而不是使用字面量直接創(chuàng)建棋返。
function Application() {
this._router = new Router();
}
Application.prototype.listen = function(port, cb) {
var self = this;
var server = http.createServer(function(req, res) {
self.handle(req, res);
});
return server.listen.apply(server, arguments);
};
Application.prototype.handle = function(req, res) {
if(!res.send) {
res.send = function(body) {
res.writeHead(200, {
'Content-Type': 'text/plain'
});
res.end(body);
}
}
var router = this._router;
router.handle(req, res);
};
接著增加HTTP方法函數(shù):
http.METHODS.forEach(function(method) {
method = method.toLowerCase();
Application.prototype[method] = function(path, fn) {
this._router[method].apply(this._router, arguments);
return this;
};
});
因為導(dǎo)出的是Application類延都,所以修改express.js文件
var Application = require('./application');
function createApplication() {
var app = new Application();
return app;
}
運行node test/index.js。
如果認真研究路由系統(tǒng)的源碼睛竣,會發(fā)現(xiàn)route本身的定義并不和我們的文字描述那樣晰房。例如:增加兩個同樣的路由:
app.put('/', function(req, res) {
res.send('put Hello World!');
});
app.get('/', function(req, res) {
res.send('get Hello World!');
});
并不像我們所設(shè)想的類似于以下結(jié)構(gòu):
---------------------------------------------------------
----- ----------- | 0 | 1 | 2 | 3 | ... |
| | Layer | ---------------------------------------------------------
| 0 | |-path | | Layer | Layer | Layer | Layer | |
| | |-route | ----> | |- method| |- method| |- method| |- method| ... |
|-----|-----------| | |- handle| |- handle| |- handle| |- handle| |
| | Layer | ---------------------------------------------------------
| 1 | |-path | route
| | |-route |
|-----|-----------|
| | Layer |
| 2 | |-path |
| | |-route |
|-----|-----------|
| ... | ... |
----- -----------
router
實際上是如下結(jié)構(gòu):
----- ----------- -------------
| | Layer | ----> | Layer |
| 0 | |-path | | |- method| route
| | |-route | | |- handle|
|-----|-----------| -------------
| | Layer | -------------
| 1 | |-path | ----> | Layer |
| | |-route | | |- method| route
|-----|-----------| | |- handle|
| | Layer | -------------
| 2 | |-path | -------------
| | |-route | ----> | Layer |
|-----|-----------| | |- method| route
| ... | ... | | |- handle|
----- ----------- -------------
router
之所以會這樣時因為路由系統(tǒng)存在先后順序的關(guān)系,前面的描述結(jié)構(gòu)很有可能丟失順序這個屬性酵颁。既然這樣,我們要思考Route的用處所在月帝。
因為express框架中躏惋,Route存儲的是真正的路由信息,可以當作單獨的成員使用嚷辅,如果想要真正得到前面所描述的結(jié)果簿姨,需要如下創(chuàng)建自己的代碼:
var router = express.Router();
router.route('/user/:user_id')
.get(function(req, res, next) {
res.json(req.user);
})
.put(function(req, res, next) {
// just an example of maybe updating the user
req.user.name = req.params.name;
// save user ... etc
res.json(req.user);
})
.post(function(req, res, next) {
next(new Error(not implemented));
})
.delete(function(req, res, next) {
next(new Error('not implemented'));
});
而不是這樣:
var app = express();
app.get('/user/:user_id', function(req, res) {
})
.put('/user/:user_id', function(req, res) {
})
.post('/users/:user_id', function(req, res) {
})
.delete('/users/:user_id', function(req, res) {
});
理解了Route的使用方法,接下來就要討論剛剛提到的順序問題簸搞。在路由系統(tǒng)中扁位,路由的處理順序非常重要,因為1路由是按照數(shù)組的方式存儲的趁俊,如果遇到兩個同樣的路由域仇,同樣的方法名,不同的處理函數(shù)寺擂,這時候前后聲明的順序?qū)⒅苯佑绊懡Y(jié)果(這也是express中間件存在順序相關(guān)的原因)暇务,例如下面的例子:
app.get('/', function(req, res) {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('first');
});
app.get('/', function(req, res) {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('second');
});
上面的代碼如果執(zhí)行會發(fā)現(xiàn)永遠都返回first,但是有時候會根據(jù)前臺傳來的參數(shù)動態(tài)判斷是否執(zhí)行接下來的路由怔软,怎樣才能跳過first進入second垦细?這就涉及到路由系統(tǒng)的流程控制問題。
流程控制分為主動和被動兩種模式:
對于express框架來說挡逼,路由綁定的處理邏輯括改、用戶設(shè)置的路徑參數(shù)這些都是不可靠的,在運行過程中很有可能會發(fā)生異常家坎,被動流程控制就是當這些異常發(fā)生的時候嘱能,express框架要負擔起捕獲這些異常的工作吝梅,因為如果不明確異常發(fā)生的位置,會導(dǎo)致js代碼無法繼續(xù)運行焰檩,并且無法精確地報出故障憔涉。
主動流程控制則是處理函數(shù)內(nèi)部的操作邏輯,以主動調(diào)用的方式來跳轉(zhuǎn)路由內(nèi)部的執(zhí)行邏輯析苫。
目前express通過引入next參數(shù)的方式來解決流程控制問題兜叨。next是處理函數(shù)的一個參數(shù),其本身也是一個函數(shù)衩侥,該函數(shù)有幾種使用方式:
- 執(zhí)行下一個處理函數(shù)国旷。執(zhí)行next()。
- 報告異常茫死。執(zhí)行next(err)跪但。
- 跳過當前的Route,執(zhí)行Route的下一項峦萎。執(zhí)行next('route')屡久。
- 跳過整個Router。執(zhí)行next('router')爱榔。
接下來被环,我們嘗試實現(xiàn)以下這些需求:
首先,修改最底層的Layer對象详幽,該對象的handle_request函數(shù)是負責(zé)調(diào)用路由綁定的處理邏輯筛欢,這里添加next函數(shù),并添加異常捕獲功能唇聘。
Layer.prototype.handle_request = function(req, res, next) {
var fn = this.handle;
try {
fn(req, res, next);
} catch (err) {
next(err);
}
};
接下來修改Route.dispatch函數(shù)版姑。因為涉及內(nèi)部的邏輯跳轉(zhuǎn),使用for循環(huán)按部就班不太合適迟郎,這里使用了類似于遞歸的方式:
Route.prototype.dispatch = function()req, res, done) {
var self = this,
method = req.method.toLowerCase(),
idx = 0, stack = self.stack;
function next(err) {
// 跳過route
if(err && err === 'route') {
return done(null);
}
// 跳過整個路由系統(tǒng)
if(err && err === 'router') {
return done(err);
}
// 越界
if(idx >= stack.length) {
return done(err);
}
// 不等枚舉下一個
var layer = stack[idx++];
if(method !== layer.method) {
return next(err);
}
if(err) {
// 主動報錯
return done(err);
} else {
layer.handle_request(req, res, next);
}
}
next();
};
整個處理過程本質(zhì)上是一個for循環(huán)剥险,唯一的差別就是在處理函數(shù)中主動調(diào)用next函數(shù)的處理邏輯。如果用戶通過next函數(shù)返回錯誤宪肖、route和router這三種情況炒嘲,目前統(tǒng)一拋給Router處理。因為修改了dispatch函數(shù)匈庭,所以調(diào)用該函數(shù)的Router,router函數(shù)也要修改夫凸,這次直接徹底修改,以后無需根據(jù)參數(shù)的個數(shù)進行調(diào)整阱持。
Router.prototype.route = function route(path) {
...
// 使用bind方式
var layer = new Layer(path, route.dispatch.bind(route));
...
};
接著修改Router.handle的代碼夭拌,邏輯和Route.dispatch類似:
Router.prototype.handle = function(req, res, done) {
var self = this,
method = req.method,
idx = 0, stack = self.stack;
function next(err) {
var layerError = (err === 'route' ? null : err);
// 跳過路由系統(tǒng)
if(layerError === 'router') {
return done(null);
}
if(idx >= stack.length || layerError) {
return done(layerError);
}
var layer = stack[idx++];
// 匹配,執(zhí)行
if(layer.match(req.url) && layer.route &&
layer.route._handles_method(method)) {
return layer.handle_request(req, res, next);
} else {
next(layerError);
}
}
next();
};
修改后的函數(shù)處理過程和原來的類似,不過有一點需要注意鸽扁,當發(fā)生異常的時候蒜绽,會將結(jié)果返回給上一層,而不是執(zhí)行原有的this.stack第0層的代碼邏輯桶现。
最后增加express框架異常處理的邏輯躲雅。
在之前,可以移除原有this.stack的初始化代碼骡和,將:
var Router = function() {
this.stack = [new Layer('*', function(req, res) {
res.writeHead(200, {
'Content-Type': 'text/plain'
});
res.end('404');
})];
};
改為:
var Router = function() {
this.stack = [];
};
然后相赁,修改Application.handle函數(shù):
Application.prototype.handle = function(req, res) {
...
var done = function finalhandler(err) {
res.writeHead(404, {
'Content-Type': 'text/plain'
});
if(err) {
res.end('404:' + err);
} else {
var msg = 'Cannot ' + req.method + ' ' + req.url;
res.end(msg);
}
};
var router = this._router;
router.handle(req, res, done);
};
這里簡單的將done函數(shù)處理為404頁面,其實在express框架中慰于,使用的是一個單獨的npm包钮科,叫做finalhandler。
簡單的修改一下測試用例證明一下成果:
var express = require('../lib/express');
var app = express();
app.get('/', function(req, res, next) {
next();
})
.get('/', function(req, res, next) {
next(new Error('error'));
})
.get('/', function(req, res) {
res.send('third');
});
app.listen(3000, function() {
console.log('Example app listening on port 3000!');
});
運行node test/index.js婆赠,訪問http://127.0.0.1:3000/绵脯,結(jié)果顯示:
404: Error: error
貌似目前一切都很順利,不過還有一個需求目前被忽略了休里。當前處理函數(shù)的異常全部是由框架捕獲蛆挫,返回的信息只能是固定的404頁面,對框架使用者顯然很不方便妙黍,大多時候悴侵,我們希望可以捕獲錯誤,并按照一定的信息封裝返回瀏覽器废境,所以express需要一個返回錯誤給上層使用者的接口畜挨。
目前和上層對接的處理函數(shù)的聲明如下:
function process_fun(req, res, next) {
}
如果增加一個錯誤處理函數(shù)筒繁,按照node,js的規(guī)則噩凹,第一參數(shù)是錯誤信息,定義應(yīng)該如下所示:
function process_err(err, req, res, next) {
}
因為兩個聲明的第一個參數(shù)信息時不同的毡咏,如果區(qū)分傳入的處理函數(shù)是處理錯誤的函數(shù)還是處理正常的函數(shù)驮宴,這個是express框架要搞定的關(guān)鍵問題。
javascript中呕缭,F(xiàn)unction.length屬性可以獲取傳入函數(shù)指定的參數(shù)個數(shù)堵泽,這個可以當做區(qū)分二者的關(guān)鍵信息。既然確定了原理恢总,接下來直接在Layer類上增加一個專門處理錯誤的函數(shù)迎罗,和處理正常新的函數(shù)區(qū)分開。
// 錯誤處理
Layer.prototype.handle.error = function(error, req, res, next) {
var fn = this.handle;
// 如果函數(shù)參數(shù)不是標準的4個參數(shù)片仿,返回錯誤信息
if(fn.length !== 4) {
return next(error);
}
try {
fn(error, req, res, next)
} catch(err) {
next(err);
}
};
接著修改Route.dispatch函數(shù):
Route.prototype.dispatch = function(req, res, done) {
var self = this,
method = req.method.toLowerCase(),
idx = 0, stack = self.stack;
function next(err) {
...
if(err) {
// 主動報錯
layer.handle_error(err, req, res, next);
} else {
layer.handle_request(req, res, next);
}
}
next();
};
當發(fā)生錯誤的時候纹安,Route會一直向后尋找錯誤處理函數(shù),如果找到則返回,否則執(zhí)行done(err)厢岂,將錯誤拋給Router光督。對于Router.handle的修改,因為涉及一些中間件的概念塔粒,完整的錯誤處理將移到下一節(jié)完成结借。本節(jié)的內(nèi)容基本上完成,包括HTTP相關(guān)的動作接口的添加卒茬、路由系統(tǒng)的跳轉(zhuǎn)以及Route級別的錯誤響應(yīng)等等船老,涉及到路由系統(tǒng)剩下的一點內(nèi)容,暫時放到以后講解扬虚。
4.第四次迭代
本節(jié)是express的第四次迭代努隙,主要目標是建立中間件機制并繼續(xù)完善路由系統(tǒng)的功能。
在express中辜昵,中間件其實是一個介于web請求來臨后到調(diào)用處理函數(shù)前整個流程體系中間調(diào)用的組件荸镊。起本質(zhì)是一個函數(shù),內(nèi)部可以訪問修改請求和響應(yīng)對象堪置,并調(diào)整接下來的處理流程躬存。
express官方給出的解釋如下:
Express是一個資深功能極簡,完全是路由和中間件構(gòu)成的一個web開發(fā)框架:從本質(zhì)上來說舀锨,一個Express應(yīng)用就是在調(diào)用各種中間件岭洲。
中間件(Middleware)是一個函數(shù),他可以訪問請求對象(request object(req))坎匿,響應(yīng)對象(response object(res))盾剩,和web應(yīng)用中處于請求-響應(yīng)循環(huán)流程中的中間件,一般被命名為next的變量替蔬。
中間件的功能包括:
- 執(zhí)行任何代碼告私。
- 修改請求和響應(yīng)對象。
- 終結(jié)請求-響應(yīng)循環(huán)承桥。
- 調(diào)用堆棧中的下一個中間件驻粟。
如果當前中間件沒有終結(jié)請求-響應(yīng)循環(huán),則必須調(diào)用next()方法將控制權(quán)交給下一個中間件凶异,否則請求就會掛起蜀撑。Express應(yīng)用可使用如下幾種中間件: - 應(yīng)用級中間件
- 路由級中間件
- 錯誤處理中間件
- 內(nèi)置中間件
- 第三方中間件
使用可選擇掛載路徑,可在應(yīng)用級別或路由級別裝載中間件剩彬。另外酷麦,你還可以同時裝載一系列中間件函數(shù),從而在一個掛載點上創(chuàng)建一個子中間件棧喉恋。
官方給出的定義其實已經(jīng)足夠清晰沃饶,一個中間件的樣式其實就是上一節(jié)所提到的處理函數(shù)粪摘,只不過并沒有正式命名。所以對于代碼來說Router類中的this.stack屬性內(nèi)部的每個handle都是一個中間件绍坝,根據(jù)使用接口不同區(qū)別了應(yīng)用級中間件和路由級中間件徘意,而四個參數(shù)的處理函數(shù)就是錯誤處理中間件。
接下來就給express框架添加中間件功能轩褐。
首先是應(yīng)用級中間件椎咧,其使用方法是Application類上的兩種方式:Application.use和Application.METHOD(HTTP的各種請求方法),后者其實在前面的小節(jié)已經(jīng)實現(xiàn)了把介,前者則是需要新增的勤讽。
在前面的小節(jié)里代碼已經(jīng)說明了Application.METHOD內(nèi)部其實是Router.METHOD的代理,Application.use同樣如此:
Application.prototype.use = function(fn) {
var path = '/',
router = this._router;
router.use(path, fn);
return this;
};
因為Application.use支持可選路徑拗踢,所以需要增加處理路徑的重載代碼脚牍。
Application.prototype.use = function(fn) {
var path = '/',
router = this._router;
// 路徑掛載
if(typeof fn !== 'function') {
path = fn;
fn = arguments[1]''
}
router.use(path, fn);
return this;
};
其實express框架支持的參數(shù)不僅僅這兩種,但是為了便于理解剔除一些旁枝末節(jié)巢墅,便于框架的理解诸狭,接下來實現(xiàn)Router.use函數(shù)。
Router.prototype.use = function(fn) {
var path = '/';
// 路徑掛載
if(typeof fn !== 'function') {
path = fn;
fn = arguments[1];
}
var layer = new Layer(path, fn);
layer.route = undefined;
this,stack.push(layer);
return this;
};
內(nèi)部代碼和Application.use差不多君纫,只不過最后不再是調(diào)用Router.use驯遇,而是直接創(chuàng)建一個Layer對象,將其放到this.stack數(shù)組中蓄髓。
在這段代碼中可以看到普通路由與中間路由的區(qū)別叉庐。普通路由放到Route中,且Router.route屬性指向Route對象会喝,Router.handle屬性指向Route.dispatch函數(shù)陡叠;中間件的Route.route屬性為undefined,Router.handle指向中間件處理函數(shù)肢执,被放到Router.stack數(shù)組中枉阵。
對于路由級中間件,首先按照要求導(dǎo)出Router類蔚万,便于使用岭妖。
exports.Router = Router;
上面的代碼添加到express.js文件中临庇,這樣就可以按照下面的使用方式創(chuàng)建一個單獨路由系統(tǒng)反璃。
var app = express();
var router = express.Router();
router.use(function(req, res, next) {
console.log('Time:', Date.now());
});
現(xiàn)在問題來了,如果像上面的代碼一樣創(chuàng)建一個新的路由系統(tǒng)無法讓路由系統(tǒng)內(nèi)部的邏輯生效假夺,因為這個路由系統(tǒng)沒法添加到現(xiàn)有的系統(tǒng)中淮蜈。
一種方法是添加一個專門添加新路由系統(tǒng)的接口,這是完全可行的已卷,但是更好的方式是按照express框架的方式梧田,這可能是Router叫做路由級中間件的原因。express將Router定義成一個特殊的中間件,而不是一個單獨的類裁眯。
這樣將單獨創(chuàng)建的路由系統(tǒng)添加到現(xiàn)有的應(yīng)用中的代碼將會非常簡單通用:
var router = express.Router();
// 將路由掛載至應(yīng)用
app.use('/', router);
這確實是一個好方法鹉梨,現(xiàn)在就將express改造成類似的樣子、
首先創(chuàng)建一個原型對象穿稳,將現(xiàn)有的Router方法轉(zhuǎn)移到該對象上存皂。
var proto = {};
proto.handle = function(req, res, done) {...};
proto.route = function route(path) {...};
proto.use = function(fn) {...};
http.METHODS.forEach(function(method) {
method = method.toLowerCase();
proto[method] = function(path, fn) {
var route = this.route(path, fn);
route[method].call(route, fn);
return this;
};
});
然后創(chuàng)建一個中間件函數(shù),使用Object.setPrototypeOf函數(shù)設(shè)置其原型逢艘,最后導(dǎo)出一個生成這些過程的函數(shù)旦袋。
module.exports = function() {
function router(req, res, next) {
router.handle(req, res, next);
}
Object.setPrototypeOf(router, proto);
router.stack = [];
return router;
};
修改測試用例,測試一下效果:
app.use(function(req, res, next) {
console.log('Time:', Date.noew());
next();
});
app.get('/', function(req, res, next) {
res.send('first');
});
router.use(function(req, res, next) {
console.log('Time:', Date.now());
next();
});
app.get('/', function(req, res, next) {
res.send('second');
});
app.use('/user', router);
app.listen(3000, function() {
console.log('Example app listening on port 3000!');
});
結(jié)果并不理想它改,原有的應(yīng)用程序還有兩個地方需要修改疤孕。
首先是邏輯處理問題。原有的Router.handle函數(shù)并沒有處理中間件的情況央拖,需要進一步修改祭阀。
proto.handle = function(req, res, done) {
...
function next(err) {
...
// 注意這里,layer.route屬性
if(layer.match(req.url) && layer.route &&
layer.route._handles_method(method)) {
layer.handle_request(req, res, next);
} else {
layer.handle_error(layerError, req, res, next);
}
}
next();
};
改進為:
proto.handle = function(req, res, done) {
...
function next(err) {
...
// 匹配鲜戒,執(zhí)行
if(layer.match(path)) {
if(!layer.route) {
// 處理中間件
layer.handle_request(req, res, next);
} else if(layer.route._handles_method(method)) {
// 處理路由
layer.handle_request(req, res, next);
}
} else {
layer.handle_error(layerError, req, res, nexr);
}
}
next();
};
其次是路徑匹配的問題柬讨。原有單一路徑被拆分成為不同中間件的路徑組合,這里需要判斷需要分多步進行袍啡,因為每個中間件只是匹配自己的路徑是否通過踩官,不過相對而言目前設(shè)計的匹配都是全等匹配,還沒有涉及到類似于express框架中的正則匹配境输,算是經(jīng)過一定簡化了蔗牡。
想要實現(xiàn)匹配邏輯就要清楚的知道哪段路徑和哪個處理函數(shù)匹配,這里定義三個變量:
- req.originalUrl 原始請求路徑嗅剖。
- req,url 當前路徑辩越。
- req,baseUrl 父路徑。
主要修改Router.handle函數(shù)信粮,該函數(shù)主要負責(zé)提取當前路徑段黔攒,便于和事先傳入的路徑進行匹配。
proto.handle = function(req, res, done) {
var self = this,
method = req.method,
idx = 0, stack = self.stack,
removed = '', slashAdded = false;
// 獲取當前父路徑
var parentUrl = req.baseUrl || '';
// 保存父路徑
req.baseUrl = parentUrl;
// 保存原始路徑
req.originalUrl = req.originalUrl || req.url;
function next(err) {
var layerError = (err === 'route' ? null : err);
// 如果有移除强缘,復(fù)原原有路徑
if(slashAdded) {
req.url = '';
slashAdded = false;
}
// 如果有移除督惰,復(fù)原原有路徑信息
if(remoe.length !== 0) {
req.baseUrl = parentUrl;
req.url = removed + req.url;
remove = '';
}
// 跳過路由系統(tǒng)
if(layerError === 'router') {
return done(null);
}
if(idx >= stack.length || layerError) {
return done(layerError);
}
// 獲取當前路徑
var path = require('url').parse(req.url).pathname;
var layer = stack[idx++];
// 匹配,執(zhí)行
if(layer.match(path)) {
// 處理中間件
if(!layer.route) {
// 要移除的部分路徑
removed = layer.path;
// 設(shè)置當前路徑
req.url = req.url.substr(removed.length);
if(req.url === '') {
req.url = '/' + req.url;
slashAdded = true;
}
// 設(shè)置當前路徑的父路徑
req.baseUrl = parentUrl + removed;
// 調(diào)用處理函數(shù)
layer.handle_request(req, res, next);
} else if(layer.route._handles_method(method)) {
// 處理路由
layer.handle_request(req, res, next);
} else {
layer.handle_error(layerError, req, res, next);
}
}
}
next();
};
這段代碼主要處理兩種情況:
第一種旅掂,存在路由中間件的情況赏胚。如:
router.use('/1', function(req, res, next) {
res.send('first user');
});
router.use('/2', function(req, res, next) {
res.send('second user');
});
app.use('/user', router);
這種情況下,Router.handle順序匹配到中間的時候商虐,會遞歸調(diào)用Router.handle觉阅,所以需要保存當前的路徑快照崖疤,具體路徑相關(guān)信息放到req.url,req.originalUrl和req.baseUrl這三個參數(shù)中典勇。
第二種劫哼,非路由中間件的情況,如:
app.get('/', function(req, res, next) {
res.send('home');
});
app.get('/books', function(req, res, next) {
res.send('books');
});
這種情況下割笙,Router.handle內(nèi)部主要是按照棧中的次序匹配路徑即可沦偎。
改好了處理函數(shù),還需要修改一下Layer.match這個匹配函數(shù)咳蔚。目前創(chuàng)建的Layer可能會有三種情況:
- 不含有路徑的中間件豪嚎。path屬性默認為/。
- 含有路徑的中間件谈火。
- 普通路由侈询。如果path屬性為,表示任意路徑糯耍。
修改原有Layer是構(gòu)造函數(shù)扔字,增加一個fast_star標記用來判斷path是否為。
function Layer(path, fn) {
this.handle = fn;
this.name = fn.name || '<anonymous>';
this.path = undefined;
// 是否為*
this.fast_star = (path === '*' ? true : false);
if(!this.fast_star) {
this.path = path;
}
}
接著修改Layer.match匹配函數(shù):
layer.prototype.match = function(path) {
// 如果為*温技,匹配
if(this.fast_star) {
this.path = '';
return true;
}
// 如果是普通路由革为,從后匹配
if(this.route && this.path === path.slice(-this.path.length)) {
return true;
}
if(!this.route) {
// 不帶路徑的中間件
if(this.path === '/') {
this.path = '/';
return true;
}
// 帶路徑的中間件
if(this.path === path.slice(0, this.path.length)) {
return true;
}
}
return false;
};
代碼中一共判斷四種情況,根據(jù)this.route區(qū)分中間件和普通路由舵鳞,然后分開判斷震檩。
express除了普通的中間件外還要一種錯誤中間件,專門來處理錯誤信息蜓堕。該中間件的聲明和上一小節(jié)最后介紹的錯誤處理函數(shù)是一樣的抛虏,同樣是四個參數(shù)分別為:err, req, res, next。
目前Router.handle中套才,當遇到錯誤信息的時候迂猴,會直接通過回調(diào)函數(shù)返回錯誤信息,顯示錯誤頁面背伴。
if(idx >= stack.length || layerError) {
return done(layerError);
}
這里需要修改策略沸毁,將其改為繼續(xù)調(diào)用下一個中間件,直到碰到錯誤中間件為止傻寂。
// 沒有找到
if(idx >= stack.length) {
return done(layerError);
}
原有這一部分的代碼只保留判斷枚舉是否完成息尺,將錯誤判斷轉(zhuǎn)移到最后執(zhí)行處理函數(shù)的為止。之所以這樣做是因為不管是執(zhí)行處理函數(shù)崎逃,或是執(zhí)行錯誤處理都需要進行路徑匹配操作和路徑分析操作掷倔,所以放到后面正好合適眉孩。
// 處理中間件
if(!layer.route) {
...
// 調(diào)用處理函數(shù)
if(layerError)
layer.hand_error(layerError, req, res, next);
else
layer.handle_request(req, res, next);
} else if(layer.route._handle_method(method)) {
// 處理路由
layer.handle_request(req, res, next);
}
到此為止个绍,express的錯誤處理部分算是基本完成了勒葱。
路由系統(tǒng)和中間件兩個大概念基本講解完畢,當然還有很多細節(jié)沒有完善巴柿。在剩下的文字里會繼續(xù)完善凛虽。
下一節(jié)的主要內(nèi)容是介紹前后端交互的兩個重要成員:request和response。express在node.js的基礎(chǔ)上進行了豐富的擴展广恢。
5.第五次迭代
本節(jié)是express的第五次迭代凯旋,主要的目標是封裝request和response兩個對象,方便使用钉迷。
其實node.js已經(jīng)給我們提供了兩個默認對象至非,之所以要封裝是因為豐富一下二者的接口,方便框架使用者糠聪,目前框架在response對象上已經(jīng)有一個接口:
if(!res.send) {
res.send = function(body) {
res.writeHead(200, {
'Content-Type': 'text/plain'
});
res.end(body);
};
}
如果需要繼續(xù)封裝荒椭,也要類似的結(jié)構(gòu)在代碼上添加顯然給人一種很亂的感覺,因為request和response的原始版本是node.js提供給框架的舰蟆,框架獲取到的是兩個對象趣惠,并不是類,要想在而浙商提供另一組接口的辦法有很多身害,規(guī)格芥蒂就是將新的接口加到該對象上或者該對象的原型鏈上味悄,目前的代碼選擇了前者,express的代碼選擇了后者塌鸯。
首先建立兩個文件:request.js和response.js侍瑟,二者分別導(dǎo)出req和res對象。
// request.js
var http = require('http');
var req = Object.create(http.IncomingMessage.prototype);
module.exports = req;
// response.js
var http = require('http');
var res = Object.create(http.ServerResponse.prototype);
module.exports = res;
二者文件的代碼都是創(chuàng)建一個對象丙猬,分別指向node.js提供的request和response兩個對象的原型丢习,以后express自定的接口可以統(tǒng)一掛載到兩個對象上。
接著修改Application.handle函數(shù)淮悼,因為這個函數(shù)里面有新鮮出爐的request和response咐低。思路很簡單,就是將二者的原型指向我們自建的req和res袜腥。因為req和res對象的原型和request和response的原型相同见擦,所以并不影響原有node.js的接口。
var request = require('./request');
var response = require('./response');
...
Application.prototype.handle = function(req, res) {
object.setPrototypeOf(req, request);
object.setPrototypeOf(res, response);
...
};
這里將原有的res.send轉(zhuǎn)移到了response.js文件中:
res.send = function(body) {
this.writeHead(200, {
'Content-Type': 'text/plain'
});
this.end(body);
};
注意函數(shù)中不再是res.writeHead和res.end羹令,而是this.wirteHead和this.end鲤屡。
在整個路由系統(tǒng)中,Router.stack每一項其實都是一個中間件福侈,每個中間件都有可能用到req和res兩個對象酒来,所以代碼中修改node.js原生提供的request和response對象的代碼放到了Application.handle中,這樣做并沒有問題肪凛,但是有一種更好的方式堰汉,express框架將這部分代碼封裝成了一個內(nèi)部中間件辽社。
為了確保框架中每個中間件接收這兩個參數(shù)的正確性翘鸭,需要將內(nèi)部中間件放到Router.stack的首項滴铅。這里將原有Application的構(gòu)造函數(shù)中的代碼去掉,不再是直接創(chuàng)建Router()路由系統(tǒng)就乓,而是用一種惰性懶加載的方式汉匙,動態(tài)創(chuàng)建。
去除原有Application構(gòu)造函數(shù)的代碼:
function Application() {}
添加惰性初始化函數(shù):
Application.prototype.lazyrouter = function() {
if(!this._router) {
this._router = new Router();
this._router.use(middleware.init());
}
};
因為是惰性初始化生蚁,所以在使用this._router對象前噩翠,一定要先調(diào)用該函數(shù)動態(tài)創(chuàng)建this._router對象。類似于如下代碼:
// 獲取router
this.lazyrouter();
router = this._router;
接下來創(chuàng)建一個叫middleware的文件夾邦投,專門放置內(nèi)部中間件的文件绎秒,在創(chuàng)建一個init.js文件,放置Application.handle中用來初始化res和req的代碼尼摹。
var request = require('../request');
var response = require('../response');
exports.init = function expressInit(req, res, next) {
// request文件可能用到res對象
req.res = res;
// response文件可能用到req對象
res.req = req;
// 修改原始req和res原型
Object.setPrototype(req, request);
Object.setPrototype(res, response);
// 繼續(xù)
next();
};
修改原有的Application.handle函數(shù):
Application.prototype.handle = function(req, res) {
...
// 這里無需調(diào)用lazyrouter见芹,因為listen前一定調(diào)用了.use或者.METHODS方法轧铁。
// 如果二者沒有調(diào)用罕邀,沒有必要創(chuàng)建路由it。this._router為undefined缨睡。
var router = this._router;
if(router) {
router.handle(req, res, done);
} else {
done();
}
};
運行node test/index.js......
express框架中和二,request和response兩個對象有很多非常好用的函數(shù)徘铝,不過大部分和框架結(jié)構(gòu)無關(guān),并且對這些函數(shù)內(nèi)部過于專研細節(jié)惯吕,對框架本身的理解并沒有多少幫助惕它。不過接下來有一各方面需要仔細研究一下,那就是前后端參數(shù)的傳遞废登,express如何獲取并分類這些參數(shù)淹魄,這一點還是需要稍微了解的。
默認情況下堡距,一共有三種參數(shù)獲取方式:
- req.query代表查詢字符串甲锡。
- req.params代表路徑變量。
- req.body代表表單提交變量羽戒。
req.query是最常用的方式缤沦,例如:
// GET /search?q=tobi+ferret
req.query.q
// => "tobi ferret"
// GET /shoes?order=des&shoe[color]=blue&shoe[type]=converse
req.query.order
// => "desc"
req.query.shoe.color
// => "blue"
req.query.shoe.type
// => "converse"
后臺獲取這些參數(shù)最簡單的方式就是通過node.js自帶的querystring模塊分析URL。express使用的另一個npm包易稠,qs缸废。并且將其分裝為另一個內(nèi)部中間件,專門負責(zé)解析查詢字符串,默認加載企量。
req.params是另一種從URL獲取參數(shù)的方式测萎,例如:
// 路由骨子額 /user/:name
// GET /user/tj
req.params.name
// => "tj"
這是一種express框架規(guī)定的參數(shù)獲取方式,對于批量處理邏輯非常實用梁钾。在我們的express中并沒有實現(xiàn)绳泉,因為路徑匹配問題過于細節(jié)化逊抡,如果對此感興趣可以研究path-to-regexp模塊姆泻,express也是在其上的封裝。
req.body是獲取表單數(shù)據(jù)的方式冒嫡,但是默認情況下框架是不會去解析這種數(shù)據(jù)拇勃,直接使用只會返回undefined。如果想要支持需要添加其他中間件孝凌,例如:body-parser或multer方咆。
本小節(jié)主要介紹了request和response兩個對象,而且講解了一下現(xiàn)有express框架中獲取參數(shù)的方式蟀架,整體上并沒有太深入的防治瓣赂,主要是這方面設(shè)計細節(jié)過多。
6.第六次迭代
本小節(jié)是第六次迭代片拍,主要目的是介紹一下express是如何集成現(xiàn)有的渲染引擎煌集。與渲染引擎有關(guān)的事情涉及到下面幾個方面:
- 如何開發(fā)或綁定一個渲染引擎。
- 如何注冊一個渲染引擎捌省。
- 如何指定模板路徑苫纤。
- 如何渲染模板引擎。
express通過app.engine(ext, callback)方法創(chuàng)建一個你自己的模板引擎纲缓。其中卷拘,ext指的是文件擴展名,callback是末班引擎的主函數(shù)祝高,接受文件路徑栗弟、參數(shù)對象和回調(diào)函數(shù)作為其參數(shù)。
// 下面的代碼演示的是一個非常簡單的能夠渲染".ntl"文件的模板引擎工闺。
var fs = require('fs'); // 此模板引擎依賴fs模塊
app.engine('ntl', function(filePath, options, callback) { // 定義模板引擎
fs.readFile(filePath, function(err, content) {
if(err) return callback(new Error(err));
// 這是一個功能極其簡單的模板引擎
var rendered = content.toString().replace('#title', '<title>' + options.title + '</title?')
.replace('#message#'. '<h1>' + options.message + '</h1>');
return callback(null, rendered);
})
});
為了讓應(yīng)用程序可以渲染模板文件横腿,還可以做如下設(shè)置:
// views,放模板文件的目錄
app.set('views', './views');
// view engine, 模板引擎
app.set('view engine', 'jade');
一旦view engine設(shè)置成功,就不需要顯示制定引擎斤寂,或者在應(yīng)用中加載引擎模塊耿焊,Express已經(jīng)在內(nèi)部加載。下面是如何渲染頁面的方法:
app.get('/', function(req, res) {
res.render('index', {title: 'Hey', message: 'Hello there!'});
});
想要實現(xiàn)上述功能遍搞,首先在Application類中定義兩個變量罗侯,一個存儲app.set和app.get這兩個方法存儲的值,另一個存儲模板引擎中的擴展名和渲染函數(shù)的對應(yīng)關(guān)系溪猿。
function Application() {
this.settings = {};
this.engines = {};
}
然后是實現(xiàn)app.set函數(shù):
Application.prototype.set = function(setting, val) {
if(arguments.length === 1) {
// app.get(setting)
return this.settings[setting];
}
this.settings[setting] = val;
return this;
};
代碼不僅僅實現(xiàn)了設(shè)置钩杰,而且當傳入的參數(shù)只要一個時等價于get函數(shù)纫塌。
接著實現(xiàn)app.get函數(shù)。因為現(xiàn)在已經(jīng)有一個app.get方法來設(shè)置路由讲弄,所以需要在該方法上進行重載措左。
http.METHODS.forEach(function(method) {
method = method.toLowerCase();
Application.prototype[method] = function(path, fn) {
if(method === 'get' && arguments.length === 1) {
// app.get(seting)
return this.set(path);
}
...
};
});
最后實現(xiàn)app.engine進行擴展名和引擎函數(shù)的映射。
Application.prototype.engine = function(ext, fn) {
// get file extension
var extension = ext[0] !== '.'
? '.' + ext
: ext;
// store engine
this.engines[extension] = fn;
return this;
};
擴展名當做key避除,統(tǒng)一添加"."怎披。
到此設(shè)置模板相關(guān)信息的函數(shù)部分算是完成,接下來就是最重要的渲染引擎函數(shù)的實現(xiàn)瓶摆。
res.render = function(views, options, callback) {
var app = this.req.app;
var done = callback;
var opts = options || {};
var self = this;
// 如果定義回調(diào)凉逛,則返回,否則渲染
done = done || function(err, str) {
if(err) {
return req.next(err);
}
self.send(str);
};
// 渲染
app.render(view, opts, done);
};
渲染函數(shù)一共有三個參數(shù):view表示模板的名稱群井,options是模板渲染的變量状飞,callback是渲染成功后的回調(diào)函數(shù)。
函數(shù)內(nèi)部直接調(diào)用render函數(shù)進行渲染书斜,渲染完成后調(diào)用done回調(diào)诬辈。
接下來創(chuàng)建一個view.js文件,主要是負責(zé)各種模板引擎和框架間的隔離荐吉,保持對內(nèi)接口的統(tǒng)一性焙糟。
function View(name, options) {
var opts = options || {};
this.defaultEngine = opts.defaultEngine;
this.root = opts.root;
this.ext = path.extname(name);
this.name = name;
vat fileName = name;
if(!this.ext) {
// get extension from default engine name
this.ext = this.defaultEngine[0] !== '.'
? '.' + this.defaultEngine
: this.defaultEngine;
fileName += this.ext;
}
// store loaded engine
this.engine = opts.engines[this.ext];
// lookup path
this.path = this.lookup(fileName);
}
View類內(nèi)部定義了很多屬性,主要包括:引擎稍坯、根目錄酬荞、擴展名、文件名等等瞧哟,為了以后的渲染做準備混巧。
View.prototype.render = function render(options, callback) {
this.engine(this.path, options, callback);
};
View的渲染函數(shù)內(nèi)部就是調(diào)用一開始注冊的引擎渲染函數(shù)。
了解了View的定義勤揩,接下來實現(xiàn)app.render模板渲染函數(shù)咧党。
Application.prototype.render = function(name, options, callback) {
var done = callback;
var engines = this.engines;
var opts = options;
view = new View(name, {
defaultEngine: this.get('view enine'),
root: this.get('views'),
engines: engines
});
if(!view.path) {
var err = neww Error('Failed to lookup view"' + name + '"');
return done(err);
}
try {
view.render(options, callback);
} catch(e) {
callback(e);
}
};
修改測試用例,嘗試一下:
var fs = require('fs'); // 此模板引擎依賴fs模塊
app.engine('ntl', function(filePath, options, callback) { // 定義模板引擎
fs.readFile(filePath, function(err, content) {
if(err) return callback(new Error(err));
// 這是一個功能極其簡單的模板引擎
var rendered = content.toString().replace('#title#', '<title>' + options.title + '</title>')
.replace('#message#', '<h1>' + options.message + '</h1>');
return callback(null, rendered);
});
});
app.set('views', './test/views'); // 制定視圖所在的位置
app.set('view engine', 'ntl'); // 注冊模板引擎
app.get('/', function(req, res, next) {
res.render('index', {title: 'Hey', message: 'Hello there!'});
});
運行node test/index.js陨亡,查看效果傍衡。
上面的代碼是自己注冊的引擎,如果想要和現(xiàn)有的模板引擎結(jié)合還需要在回調(diào)函數(shù)中引用模板自身的渲染方法负蠕,當然為了方便蛙埂,express框架內(nèi)部提供了一個默認方法,如果模板引擎導(dǎo)出了該方法遮糖,則表示該模板引擎支持express框架绣的,無需使用app.engine再次封裝。
該方法聲明如下:
__express(filePath, options, callback)
可以參考ejs模板引擎的代碼:
// 改代碼在lib/ejs.js文件中355行左右
exports.__express = exports.renderFile;
express為了實現(xiàn)這個默認加載功能,在View構(gòu)造函數(shù)中多加入一個判斷:
if(!opts.engines[this.ext]) {
// load engine
var mod = this.ext.substr(1);
opts.engines[this.ext] = require(mod).__express;
}
代碼很簡單屡江,如果沒有找到引擎對應(yīng)的渲染函數(shù)芭概,那就嘗試加載__express函數(shù)。
后記
擴展部分:
1.關(guān)于Application:
官方express源碼并沒有像本文一樣寫為一個類惩嘉,而是一個中間件罢洲,一個對象,該對象使用了mixin方法的多繼承手段文黎,express.js文件中的createApplication函數(shù)算是整個框架的切入點惹苗。
2.關(guān)于Router.handle:
這個函數(shù)可以說是整個express框架的核心,在仿制的時候舍棄了很多細節(jié)臊诊。函數(shù)內(nèi)部有2個關(guān)鍵點:一鸽粉,處理URL形式的參數(shù)斜脂,這里涉及對params參數(shù)的提取過程抓艳。其中有一個restore函數(shù)使用高階函數(shù)的方法做了緩存。二帚戳,setImmediate異步返回玷或,之所以使用異步處理,是因為下面的代碼需要運行片任,包括路徑相關(guān)的參數(shù)偏友,這些參數(shù)在下一個處理函數(shù)中可能會用到。
3.關(guān)于其他函數(shù):
涉及很多細節(jié):正則表達式对供,http協(xié)議層位他,node.js本身函數(shù)的使用。大部分函數(shù)都自成體系产场。