其實關於這個話題以前已經提到過了, 也寫過一篇關於express和koa對比的文章, 可是如今回過頭看, 其實仍是挺多錯誤的地方, 好比關於express和koa中間件原理的部分陷入了一個陷阱, 當時也研究了挺久可是沒怎麼理解. 關於這部分其實就是對於設計模式的欠缺了. 關於中間件模式咱們不說那麼多概念或者實現了, 針對代碼說話.express
柿子固然挑軟的捏, 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進行對比.app
梳理一下代碼結構吧:koa
var proto = {}
var createServer = function () {}
proto.use = function () {}
proto.handle = function () {}
proto.listen = function () {}
複製代碼
主要就是上面這幾個函數, 其餘輔助函數咱們砍掉. 能夠看到咱們用connect主要就是在proto這塊, 讓咱們根據代碼來看咱們啓動一個connect服務器到底發生了哪些事情.函數
首先咱們是新建一個connect實例:ui
var app = connect()
複製代碼
毫無疑問調用的是createServer
, 由於這個模塊最終導出的就是它嘛, createServer
部分的代碼也很簡單:this
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來.url
當咱們想設置某個路由的時候就是調用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
其實也很簡單了, 沒法是新建一個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的時候纔會繼續尋找中間件並調用. 這樣寫出來我本身好像也清楚了不少(逃