本文出自[Century's World]
不知從什么時(shí)候開始,node就開始風(fēng)靡起來(lái),我們甚至都沒有謹(jǐn)慎的研究過他和其他服務(wù)器的區(qū)別,便開始躍躍欲試。相信很多人會(huì)和筆者一樣,在接觸node服務(wù)器的第一時(shí)刻,便接觸到了express,我有個(gè)朋友也說過,學(xué)一個(gè)東西還是要結(jié)合框架來(lái)學(xué)比較好,當(dāng)時(shí)我便聽了話,開始了express+node的學(xué)習(xí),當(dāng)然,express非常順手,給了我很好的體驗(yàn)。而今天,我們撇開浮躁,靜下心來(lái),仔細(xì)研究Express框架。
假如沒有Express
說到底Express只是一個(gè)框架而已,那么,他是一個(gè)什么框架呢,我們撇開Express不談,我們想要完成一個(gè)Node的服務(wù),只要如下的代碼就可以完成:
var http = require("http");
var server = http.createServer(function(request, response) {
response.writeHead(200, {"Content-Type": "text/html"});
response.write("Hello World!");
response.end();
});
server.listen(80);
很簡(jiǎn)單的幾行代碼就實(shí)現(xiàn)了一個(gè)服務(wù)器,假如我們的需求只是簡(jiǎn)單的渲染一個(gè)頁(yè)面的話,我們大可以用這幾行代碼完成我們想做的事情,其實(shí)http這系列的庫(kù)做了很多事情,非常建議大家回頭去看看這一系列Express底層的Api。當(dāng)然了最后我們還是會(huì)用框架的,自己整理的也好,用現(xiàn)在流行的Expres或者Koa也好,目的是為了應(yīng)對(duì)復(fù)雜的使用場(chǎng)景,減少重復(fù)繁瑣的代碼。
小小的實(shí)現(xiàn)一下
說起Express的特點(diǎn),大概就是中間件吧,所有的東西都是通過中間件來(lái)完成的,那么中間件實(shí)現(xiàn)是怎么樣的呢,假如我們自己實(shí)現(xiàn)一個(gè)Express,我們就應(yīng)該先解決中間件的問題,那讓我們來(lái)嘗試一下實(shí)現(xiàn)一個(gè)簡(jiǎn)單的Express。
首先我們抽離createServer的參數(shù)
const app = function(req, res) {
//TODO someting
}
var server = http.createServer(app);
server.listen(port)
這樣我們邏輯處理就放到了app里面,有的時(shí)候一次運(yùn)行可能產(chǎn)生多個(gè)app的實(shí)例,為了分隔環(huán)境,我們可以用一個(gè)工廠方法或者構(gòu)造行數(shù)生成app
const express = function() {
return function(req, res) {
//TODO someting
}
}
const app = express();
var server = http.createServer(app)
server.listen(port)
接下來(lái)我們先實(shí)現(xiàn)app.use
function Middleware = function(path, fn) {
if (!(this instanceof Middleware)) {
return new Middleware(...arguments);
}
this.path = path;
this.fn = fn;
}
app.use = function(path, ...fns) {
if (arguments.length == 1) {
fns = path;
path = '/';
}
for(var index in fns) {
var fn = fns[index]
const middleware = Middleware(path, fn);
this.middlewares.push(middleware);
}
}
我們定義了一個(gè)Middleware的對(duì)象,其中有個(gè)很多框架中常用的hack,也就是在一個(gè)class里面判斷是否是使用new來(lái)創(chuàng)建對(duì)象的,因?yàn)槭褂胣ew來(lái)創(chuàng)建對(duì)象的時(shí)候this一定是當(dāng)前類的實(shí)例,所以我們可以根據(jù)this的類型來(lái)重定向一個(gè)new Function。Middleware用于封裝一個(gè)中間件層,用于綁定中間件和對(duì)應(yīng)的path。在app.use里面我們將中間件都保存在app的middlewares屬性中,那么接下來(lái),我們就要實(shí)現(xiàn)這個(gè)中間件的處理過程。
首先我們要增加一個(gè)handler的function
function mathMiddleware(url, middleware) {
// TODO match middleware
}
app.handler = function(req, res) {
var url = req.url;
var middlewares = app.middlewares;
var idx = 0;
var match = false;
var middleware;
function done() {
//完成這次請(qǐng)求, 比如有error的情況
}
function next (err) {
if (err) {
done(err)
}
while(match === false && idx < middlewares.length) {
middleware = middlewares[idx];
match = mathMiddleware(url, middleware)
}
middleware.handle(req, res, next);
}
}
在Middleware中加上一個(gè)handle
Middleware.prototype.handle = function(req, res, next) {
try {
this.fn(req, res, next);
} catch(e) {
next(e)
}
}
這只是簡(jiǎn)單的實(shí)現(xiàn)了一下Express的中間件的邏輯,這也大致是Express的實(shí)現(xiàn)邏輯,我們知道了這種中間件的實(shí)現(xiàn)方式,那么今后在我們的應(yīng)用中,對(duì)某一塊邏輯要使用策略模式、裝飾著模式或者工廠模式的時(shí)候,我們也可以用一個(gè)這樣的中間件的策略去切割代碼,讓邏輯的處理變的非常簡(jiǎn)單而清晰。
別YY了,看一下官方實(shí)現(xiàn)
官方的代碼其實(shí)寫的非常易懂,總的來(lái)說,給我的感覺,express就是一個(gè)微型的框架。
結(jié)構(gòu)
├── application.js
├── express.js
├── middleware
│ ├── init.js
│ └── query.js
├── request.js
├── response.js
├── router
│ ├── index.js
│ ├── layer.js
│ └── route.js
├── utils.js
└── view.js
express.js
這是整個(gè)應(yīng)用的入口,主要的工作的整合了application.js中的關(guān)于app的屬性,構(gòu)造了一個(gè)函數(shù)來(lái)輸出app,將一些輔助函數(shù)輸出,比如Router、Request等等
router
這個(gè)類我們應(yīng)該不陌生,我們?cè)谑褂胑xpress的時(shí)候經(jīng)常使用
var express = require('express');
var router = express.Router();
這個(gè)router其實(shí)是整個(gè)app的核心,包括app.use的中間件實(shí)現(xiàn)也是通過Router實(shí)現(xiàn)的,以下是源碼加上注釋
proto.use = function use(fn) {
var offset = 0; //用于計(jì)算參數(shù)的個(gè)數(shù)來(lái)獲取所有的中間件
var path = '/'; //設(shè)置默認(rèn)的path為 /
if (typeof fn !== 'function') { // use('/', fn)的情況
var arg = fn;
while (Array.isArray(arg) && arg.length !== 0) {
arg = arg[0];
}
if (typeof arg !== 'function') {
offset = 1;
path = fn;
}
}
//通過offset獲取所有的中間件
var callbacks = flatten(slice.call(arguments, offset));
//當(dāng)中間件的個(gè)數(shù)為零的時(shí)候,拋出異常
if (callbacks.length === 0) {
throw new TypeError('Router.use() requires a middleware function')
}
for (var i = 0; i < callbacks.length; i++) {
var fn = callbacks[i];
if (typeof fn !== 'function') {
throw new TypeError('Router.use() requires a middleware function but got a ' + gettype(fn))
}
debug('use %o %s', path, fn.name || '<anonymous>')
// 創(chuàng)建一個(gè)Layer,類似于之前自己實(shí)現(xiàn)的Middleware
var layer = new Layer(path, {
sensitive: this.caseSensitive,
strict: false,
end: false
}, fn);
layer.route = undefined;
this.stack.push(layer); //將這個(gè)layer加入到棧中
}
return this; //返回自己用于鏈?zhǔn)秸{(diào)用
};
之后會(huì)講到這個(gè)Layer類,這是一個(gè)中間件的承載物,所有的中間件的處理方法和路徑的綁定信息都被封裝在這個(gè)類的實(shí)例中,而Router的use方法即使將這些中間件打包成為一個(gè)Layer,然后存儲(chǔ)到自己的stack中用于之后使用。
然后是一個(gè)Router中的request處理類,用于使用中間件處理請(qǐng)求。
proto.handle = function handle(req, res, out) {
var self = this; //用self指向老的this
var idx = 0; // 迭代stack的迭代器
var protohost = getProtohost(req.url) || '' //獲取協(xié)議名
var removed = ''; // 定義刪除字段
var slashAdded = false;
var paramcalled = {};
var options = [];
var stack = self.stack; // layer 的集合
var parentParams = req.params;
var parentUrl = req.baseUrl || '';
var done = restore(out, req, 'baseUrl', 'next', 'params');
req.next = next;
if (req.method === 'OPTIONS') { // 處理Options的請(qǐng)求,跨域問題
done = wrap(done, function(old, err) {
if (err || options.length === 0) return old(err);
sendOptionsResponse(res, options, old);
});
}
req.baseUrl = parentUrl;
req.originalUrl = req.originalUrl || req.url;
next();
// 中間件的迭代方法
function next(err) {
var layerError = err === 'route' ? null : err;
//計(jì)算req.url, req.baseUrl
if (slashAdded) {
req.url = req.url.substr(1);
slashAdded = false;
}
if (removed.length !== 0) {
req.baseUrl = parentUrl;
req.url = protohost + removed + req.url.substr(protohost.length);
removed = '';
}
//處理特殊情況,結(jié)束這次訪問
if (layerError === 'router') {
setImmediate(done, null)
return
}
//處理特殊情況,結(jié)束這次訪問, 沒有匹配的路由了
if (idx >= stack.length) {
setImmediate(done, layerError);
return;
}
var path = getPathname(req); //獲取當(dāng)前path
if (path == null) {
return done(layerError);
}
var layer;
var match;
var route;
while (match !== true && idx < stack.length) {
// TODO: 循環(huán)獲取匹配的Layer
}
// 沒有匹配的
if (match !== true) {
return done(layerError);
}
//TODO: 根據(jù)layer注入req.params
//TODO: 根據(jù)需要調(diào)用layer.handle_request或者layer.handle_error
}
};
當(dāng)然了我省略了一部分代碼,給大家大致的介紹了一下router是如何處理中間件的,處理中間件的關(guān)鍵就這么兩個(gè)函數(shù),一個(gè)是use,一個(gè)是handle,handle用于以中間件的形式迭代處理請(qǐng)求,use用于注冊(cè)中間件
application.js
這是一個(gè)app的類,定義了app的屬性和函數(shù),那么application又和router有什么聯(lián)系呢,我們從api中可以發(fā)現(xiàn),幾乎router有的函數(shù),在app中都可以使用,比如router.use和app.use,router.get和app.get,那么官方的實(shí)現(xiàn)中,是怎么實(shí)現(xiàn)的呢,是否是簡(jiǎn)單的繼承呢。我們帶著疑問去觀察這個(gè)類,我們會(huì)發(fā)現(xiàn)application中有個(gè)router的屬性,在中間件的表現(xiàn)上,application只是一個(gè)傀儡,大部分的實(shí)現(xiàn)都還是依靠router的,application的中間件的操作都是交由其router來(lái)處理的,也就是說app.use()是約等于app.router.use()的。比如:
app.param = function param(name, fn) {
this.lazyrouter();
if (Array.isArray(name)) {
for (var i = 0; i < name.length; i++) {
this.param(name[i], fn);
}
return this;
}
this._router.param(name, fn);
return this;
};
話雖這么說,但是app的router是懶加載的,當(dāng)調(diào)用use之類的函數(shù)的時(shí)候會(huì)判斷當(dāng)前是否已經(jīng)創(chuàng)建了router,否則會(huì)創(chuàng)建一個(gè)router。除此之外,還會(huì)對(duì)參數(shù)做一些校驗(yàn)和轉(zhuǎn)換,因此還是推薦不直接使用app.router的方式的。
middleware
(有點(diǎn)無(wú)聊)這個(gè)middleware目錄下只是express內(nèi)置的兩個(gè)中間件,一個(gè)query是用于在req.query注入url中的query參數(shù)的,init是一個(gè)初始化的中間件,它把req、res相互引用了一些,并mixin了一些req, res的屬性,還有x-powered-by額?默認(rèn)給express打個(gè)廣告?
Layer
(有那么點(diǎn)意思)這是一個(gè)藏在Router下的對(duì)象,用于包裝中間件和對(duì)應(yīng)的path。
request&response
(比較無(wú)聊)這兩個(gè)類里面主要是一些api這個(gè)和express的核心部分的關(guān)系沒有那么大,他的主要工作主要集中在封裝了一些工具方法,一個(gè)方便開發(fā)者使用的req, res的屬性集合的對(duì)象。
看源碼有什么用?
總的來(lái)說,這次的Express源碼之旅是很有幫助的,這是我開始這個(gè)源碼計(jì)劃的第一個(gè)項(xiàng)目,選擇Express的原因是這個(gè)框架的代碼確實(shí)看起來(lái)比較簡(jiǎn)單,不需要編譯,其次Express還是我現(xiàn)在用的最多的node服務(wù)框架,當(dāng)然之后會(huì)考慮使用koa,所以之后很有可能會(huì)帶來(lái)koa的源碼解讀。作為一個(gè)node服務(wù)的框架,這次源碼閱讀讓我更加了解Node的http這個(gè)模塊的東西,有很多的基礎(chǔ)的模塊在jshttp中,閱讀它們會(huì)讓我更加理解一些關(guān)于http的問題,查看了整個(gè)中間件的實(shí)現(xiàn),讓我對(duì)這種模式豁然開朗,之后希望能夠在項(xiàng)目中靈活的運(yùn)用。對(duì)request和response的閱讀讓我知道了很多之前看文檔沒有仔細(xì)觀察到的api。