中間件 在 Node.js 中被普遍使用,它泛指一種特定的設計模式、一系列的處理單元、過濾器和處理程序,以函數的形式存在,鏈接在一塊兒,造成一個異步隊列,來完成對任何數據的預處理和後處理。javascript
它的優勢在於 靈活性:使用中間件咱們用極少的操做就能獲得一個插件,用最簡單的方法就能將新的過濾器和處理程序擴展到現有的系統上。java
中間件模式中,最基礎的組成部分就是 中間件管理器,咱們能夠用它來組織和執行中間件的函數,如圖所示: git
要實現中間件模式,最重要的實現細節是:github
use()
函數來註冊新的中間件,一般,新的中間件只能被添加到高壓包帶的末端,但不是嚴格要求這麼作;至於怎麼處理傳遞數據,目前沒有嚴格的規則,通常有幾種方式設計模式
而具體的處理方式取決於 中間件管理器 的實現方式以及中間件自己要完成的任務類型。api
舉一個來自於 《Node.js 設計模式 第二版》 的一個爲消息傳遞庫實現 中間件管理器 的例子:app
class ZmqMiddlewareManager {
constructor(socket) {
this.socket = socket;
// 兩個列表分別保存兩類中間件函數:接受到的信息和發送的信息。
this.inboundMiddleware = [];
this.outboundMiddleware = [];
socket.on('message', message => {
this.executeMiddleware(this.inboundMiddleware, {
data: message
});
});
}
send(data) {
const message = { data };
this.excuteMiddleware(this.outboundMiddleware, message, () => {
this.socket.send(message.data);
});
}
use(middleware) {
if(middleware.inbound) {
this.inboundMiddleware.push(middleware.inbound);
}
if(middleware.outbound) {
this.outboundMiddleware.push(middleware.outbound);
}
}
exucuteMiddleware(middleware, arg, finish) {
function iterator(index) {
if(index === middleware.length) {
return finish && finish();
}
middleware[index].call(this, arg, err => {
if(err) {
return console.log('There was an error: ' + err.message);
}
iterator.call(this, ++index);
});
}
iterator.call(this, 0);
}
}
複製代碼
接下來只須要建立中間件,分別在inbound
和outbound
中寫入中間件函數,而後執行完畢調用next()
就行了。好比:框架
const zmqm = new ZmqMiddlewareManager();
zmqm.use({
inbound: function(message, next) {
console.log('input message: ', message.data);
next();
},
outbound: function(message, next) {
console.log('output message: ', message.data);
next();
}
});
複製代碼
Express 所推廣的 中間件 概念就與之相似,一個 Express 中間件通常是這樣的:dom
function(req, res, next) { ... }
複製代碼
前面展現的中間件模型使用回調函數實現的,可是如今有一個比較時髦的 Node.js 框架Koa2
的中間件實現方式與以前描述的有一些不太相同。Koa2
中的中間件模式移除了一開始使用ES2015
中的生成器實現的方法,兼容了回調函數、convert
後的生成器以及async
和await
。koa
在Koa2
官方文檔中給出了一個關於中間件的 洋蔥模型,以下圖所示:
從圖中咱們能夠看到,先進入inbound
的中間件函數在outbound
中被放到了後面執行,那麼到底是爲何呢?帶着這個問題咱們去讀一下Koa2
的源碼。
在koa/lib/applications.js
中,先看構造函數,其它的均可以無論,關鍵就是this.middleware
,它是一個inbound
隊列:
constructor() {
super();
this.proxy = false;
this.middleware = [];
this.subdomainOffset = 2;
this.env = process.env.NODE_ENV || 'development';
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
}
複製代碼
和上面同樣,在Koa2
中也是用use()
來把中間件放入隊列中:
use(fn) {
if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
if (isGeneratorFunction(fn)) {
deprecate('Support for generators will be removed in v3. ' +
'See the documentation for examples of how to convert old middleware ' +
'https://github.com/koajs/koa/blob/master/docs/migration.md');
fn = convert(fn);
}
debug('use %s', fn._name || fn.name || '-');
this.middleware.push(fn);
return this;
}
複製代碼
接着咱們看框架對端口監聽進行了一個簡單的封裝:
// 封裝以前 http.createServer(app.callback()).listen(...)
listen(...args) {
debug('listen');
const server = http.createServer(this.callback());
return server.listen(...args);
}
複製代碼
中間件的管理關鍵就在於this.callback()
,看一下這個方法:
callback() {
const fn = compose(this.middleware);
if (!this.listenerCount('error')) this.on('error', this.onerror);
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
};
return handleRequest;
}
複製代碼
這裏的compose
方法其實是Koa2
的一個核心模塊koa-compose
(https://github.com/koajs/compose),在這個模塊中封裝了中間件執行的方法:
function compose (middleware) {
if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
for (const fn of middleware) {
if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
}
/** * @param {Object} context * @return {Promise} * @api public */
return function (context, next) {
// last called middleware #
let index = -1
return dispatch(0)
function dispatch (i) {
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
let fn = middleware[i]
if (i === middleware.length) fn = next
if (!fn) return Promise.resolve()
try {
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err)
}
}
}
}
複製代碼
能夠看到,compose
經過遞歸對中間件隊列進行了 反序遍歷,生成了一個Promise
鏈,接下來,只須要調用Promise
就能夠執行中間件函數了:
handleRequest(ctx, fnMiddleware) {
const res = ctx.res;
res.statusCode = 404;
const onerror = err => ctx.onerror(err);
const handleResponse = () => respond(ctx);
onFinished(res, onerror);
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
複製代碼
從源碼中能夠發現,next()
中返回的是一個Promise
,因此通用的中間件寫法是:
app.use((ctx, next) => {
const start = new Date();
return next().then(() => {
const ms = new Date() - start;
console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
});
});
複製代碼
固然若是要用async
和await
也行:
app.use((ctx, next) => {
const start = new Date();
await next();
const ms = new Date() - start;
console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
});
複製代碼
因爲還有不少Koa1
的項目中間件是基於生成器的,須要使用koa-convert
來進行平滑升級:
const convert = require('koa-convert');
app.use(convert(function *(next) {
const start = new Date();
yield next;
const ms = new Date() - start;
console.log(`${this.method} ${this.url} - ${ms}ms`);
}));
複製代碼
最後,若是以爲文章有點用處的話,求求大佬點個贊!若是發現什麼錯漏也歡迎提出!