其實關於這個話題之前已經提到過了, 也寫過一篇關於express和koa對比的文章, 但是現在回過頭看, 其實還是挺多錯誤的地方, 比如關於express和koa中介軟體原理的部分陷入了一個陷阱, 當時也研究了挺久但是沒怎麼理解. 關於這部分其實就是對於設計模式的欠缺了. 關於中介軟體模式我們不說那麼多概念或者實現了, 針對程式碼說話.
柿子當然挑軟的捏, express的程式碼量不算大, 但是有個更加簡單的connect, 我們就從connect入手吧.
花了點時間畫了個示意圖, 但是之前沒怎麼畫過程式碼流程圖, 意思一下而已:
程式碼分析
首先我們看看connect是怎麼使用的:
const connect = require('connect')
const app = connect()
app.use('/', function (req, res, next) {
console.log('全域性中介軟體')
next()
console.log('執行完了')
})
app.use('/bar', function (req, res) {
console.log('第二個中介軟體')
res.end('end')
})
app.listen(8001)
複製程式碼
跟express類似, 新建例項, 匹配路由, 很簡潔也很有效. 上面程式碼執行訪問後我們發現其實next後還是會回來執行下面的程式碼的, 似乎跟koa的中介軟體有點類似, 號稱洋蔥型中介軟體嘛. 結論是否定的, 反正這裡不是與koa進行對比.
梳理一下程式碼結構吧:
var proto = {}
var createServer = function () {}
proto.use = function () {}
proto.handle = function () {}
proto.listen = function () {}
複製程式碼
主要就是上面這幾個函式, 其他輔助函式我們砍掉. 可以看到我們用connect主要就是在proto這塊, 讓我們根據程式碼來看我們啟動一個connect伺服器到底發生了哪些事情.
首先我們是新建一個connect例項:
var app = connect()
複製程式碼
毫無疑問呼叫的是createServer
, 因為這個模組最終匯出的就是它嘛, createServer
部分的程式碼也很簡單:
function createServer() {
function app(req, res, next){ app.handle(req, res, next); }
merge(app, proto); // 繼承了proto
merge(app, EventEmitter.prototype); // 繼承了EventEmitter
app.route = '/';
app.stack = []; // 暫存路由和方法的地方
return app;
}
複製程式碼
上面有用的部分我已經標出來了, 可以看出來其實我們那些常用的connect方法都來自proto, 那麼我們下面主要工作就圍繞著proto來.
app.use
當我們想設定某個路由的時候就是呼叫app.use
, 但是可能大家並不太清楚他具體做了什麼事情, 比如下面的程式碼:
app.use('/bar', function (req, res) {
res.end('end')
})
複製程式碼
上面已經講了, 有個stack陣列是專門用來存放路由和他的方法的, 很容易的就能想到: app.use
就是將我們想的路由和方法推進去等待執行, 實際上也是這樣的:
proto.use = function use(route, fn) {
var handle = fn;
var path = route;
// default route to '/'
if (typeof route !== 'string') {
handle = route;
path = '/';
}
// wrap sub-apps
if (typeof handle.handle === 'function') {
var server = handle;
server.route = path;
handle = function (req, res, next) {
server.handle(req, res, next);
};
}
// wrap vanilla http.Servers
if (handle instanceof http.Server) {
handle = handle.listeners('request')[0];
}
// strip trailing slash
if (path[path.length - 1] === '/') {
path = path.slice(0, -1);
}
// add the middleware
debug('use %s %s', path || '/', handle.name || 'anonymous');
this.stack.push({ route: path, handle: handle });
return this;
};
複製程式碼
看上去蠻複雜的, 我們簡化一下, 不考慮各種異常以及相容, 預設只能app.use(route, handle)
呼叫:
// 很好嘛 把if都給去掉了就是簡化2333
proto.use = function use(route, fn) {
var handle = fn;
var path = route;
this.stack.push({ route: path, handle: handle });
return this;
};
複製程式碼
簡化後是不是順眼多了, 其實就是維護陣列, 當然這樣肯定有問題的, 重複路由什麼的就不管了.
中介軟體的實現
那use實現後其實我們就有點數了, 中介軟體現在都在stack裡, 那我們執行中介軟體就是針對具體路由來遍歷這個stack嘛, 對的, 就是遍歷stack, 但是connect的中介軟體事順序執行的, 如果一個個排下來就是所有中介軟體都會執行一遍, 可能的情況就是比如一個異常處理的中介軟體, 我只要在出現異常的時候才需要呼叫這個中介軟體.這時候next
就上場了, 首先來看看proto.handle實現的幾十行程式碼吧:
proto.handle = function handle(req, res, out) {
var index = 0;
var protohost = getProtohost(req.url) || '';
var removed = '';
var slashAdded = false;
var stack = this.stack;
// final function handler
var done = out || finalhandler(req, res, {
env: env,
onerror: logerror
});
// store the original URL
req.originalUrl = req.originalUrl || req.url;
function next(err) {
if (slashAdded) {
req.url = req.url.substr(1);
slashAdded = false;
}
if (removed.length !== 0) {
req.url = protohost + removed + req.url.substr(protohost.length);
removed = '';
}
// next callback
var layer = stack[index++];
// all done
if (!layer) {
defer(done, err);
return;
}
// route data
var path = parseUrl(req).pathname || '/';
var route = layer.route;
// skip this layer if the route doesn't match
if (path.toLowerCase().substr(0, route.length) !== route.toLowerCase()) {
return next(err);
}
// skip if route match does not border "/", ".", or end
var c = path.length > route.length && path[route.length];
if (c && c !== '/' && c !== '.') {
return next(err);
}
// trim off the part of the url that matches the route
if (route.length !== 0 && route !== '/') {
removed = route;
req.url = protohost + req.url.substr(protohost.length + removed.length);
// ensure leading slash
if (!protohost && req.url[0] !== '/') {
req.url = '/' + req.url;
slashAdded = true;
}
}
// call the layer handle
call(layer.handle, route, err, req, res, next);
}
next();
};
複製程式碼
還是挺長的, 需要簡化, 同理我們把if都給去掉簡化程式碼:
proto.handle = function handle(req, res, out) {
var index = 0;
var protohost = getProtohost(req.url) || '';
var removed = '';
var slashAdded = false;
var stack = this.stack;
// final function handler
var done = out || finalhandler(req, res, {
env: env,
onerror: logerror
});
// store the original URL
req.originalUrl = req.originalUrl || req.url;
function next(err) {
// next callback
var layer = stack[index++];
// all done 這個不能去
if (!layer) {
defer(done, err);
return;
}
// route data
var path = parseUrl(req).pathname || '/';
var route = layer.route;
// skip this layer if the route doesn't match
if (path.toLowerCase().substr(0, route.length) !== route.toLowerCase()) {
return next(err);
}
// call the layer handle
call(layer.handle, route, err, req, res, next);
}
next();
};
複製程式碼
簡化後我們可以看到, 其實next是個遞迴, 只要符合條件它會不停地呼叫自身, 也就是說只要你在中介軟體裡呼叫了next它會遍歷stack尋找中介軟體如果找到了就執行, 如果沒找到就defer(done), 注意proto.handle定義了一個index, 這是尋找中介軟體的一個索引, next一直需要用到. 這裡無關緊要的函式就不提了, 比如getProtohost
, 比如call
.
app.listen
app.listen
其實也很簡單了, 無法是新建一個http.Server
而已, 程式碼如下:
proto.listen = function listen() {
var server = http.createServer(this);
return server.listen.apply(server, arguments);
};
複製程式碼
結束
說到這裡差不多快結束了, 我們其實可以知道, connect/express的中介軟體模型是這樣的:
http.createServer(function (req, res) {
m1 (req, res) {
m2 (req, res) {
m3 (req, res) {}
}
}
})
複製程式碼
當我們呼叫next的時候才會繼續尋找中介軟體並呼叫. 這樣寫出來我自己好像也清楚了很多(逃