淺談Node.js 中間件模式
中間件在 Node.js 中被廣泛使用,它泛指一種特定的設(shè)計(jì)模式、一系列的處理單元、過(guò)濾器和處理程序,以函數(shù)的形式存在,連接在一起,形成一個(gè)異步隊(duì)列,來(lái)完成對(duì)任何數(shù)據(jù)的預(yù)處理和后處理。
它的優(yōu)點(diǎn)在于 靈活性 :使用中間件我們用極少的操作就能得到一個(gè)插件,用最簡(jiǎn)單的方法就能將新的過(guò)濾器和處理程序擴(kuò)展到現(xiàn)有的系統(tǒng)上。
常規(guī)中間件模式
中間件模式中,最基礎(chǔ)的組成部分就是 中間件管理器 ,我們可以用它來(lái)組織和執(zhí)行中間件的函數(shù),如圖所示:

要實(shí)現(xiàn)中間件模式,最重要的實(shí)現(xiàn)細(xì)節(jié)是:
- 可以通過(guò)調(diào)用use()函數(shù)來(lái)注冊(cè)新的中間件,通常,新的中間件只能被添加到高壓包帶的末端,但不是嚴(yán)格要求這么做;
- 當(dāng)接收到需要處理的新數(shù)據(jù)時(shí),注冊(cè)的中間件在意不執(zhí)行流程中被依次調(diào)用。每個(gè)中間件都接受上一個(gè)中間件的執(zhí)行結(jié)果作為輸入值;
- 每個(gè)中間件都可以停止數(shù)據(jù)的進(jìn)一步處理,只需要簡(jiǎn)單地不調(diào)用它的毀掉函數(shù)或者將錯(cuò)誤傳遞給回調(diào)函數(shù)。當(dāng)發(fā)生錯(cuò)誤時(shí),通常會(huì)觸發(fā)執(zhí)行另一個(gè)專(zhuān)門(mén)處理錯(cuò)誤的中間件。
至于怎么處理傳遞數(shù)據(jù),目前沒(méi)有嚴(yán)格的規(guī)則,一般有幾種方式
- 通過(guò)添加屬性和方法來(lái)增強(qiáng);
- 使用某種處理的結(jié)果來(lái)替換 data;
- 保證原始要處理的數(shù)據(jù)不變,永遠(yuǎn)返回新的副本作為處理的結(jié)果。
而具體的處理方式取決于 中間件管理器 的實(shí)現(xiàn)方式以及中間件本身要完成的任務(wù)類(lèi)型。
舉一個(gè)來(lái)自于 《Node.js 設(shè)計(jì)模式 第二版》 的一個(gè)為消息傳遞庫(kù)實(shí)現(xiàn) 中間件管理器 的例子:
class ZmqMiddlewareManager {
constructor(socket) {
this.socket = socket;
// 兩個(gè)列表分別保存兩類(lèi)中間件函數(shù):接受到的信息和發(fā)送的信息。
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);
}
}
接下來(lái)只需要?jiǎng)?chuàng)建中間件,分別在 inbound 和 outbound 中寫(xiě)入中間件函數(shù),然后執(zhí)行完畢調(diào)用 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 所推廣的 中間件 概念就與之類(lèi)似,一個(gè) Express 中間件一般是這樣的:
function(req, res, next) { ... }
Koa2 中使用的中間件
前面展示的中間件模型使用回調(diào)函數(shù)實(shí)現(xiàn)的,但是現(xiàn)在有一個(gè)比較時(shí)髦的 Node.js 框架 Koa2 的中間件實(shí)現(xiàn)方式與之前描述的有一些不太相同。 Koa2 中的中間件模式移除了一開(kāi)始使用 ES2015 中的生成器實(shí)現(xiàn)的方法,兼容了回調(diào)函數(shù)、 convert 后的生成器以及 async 和 await 。
在 Koa2 官方文檔中給出了一個(gè)關(guān)于中間件的 洋蔥模型 ,如下圖所示:

從圖中我們可以看到,先進(jìn)入 inbound 的中間件函數(shù)在 outbound 中被放到了后面執(zhí)行,那么究竟是為什么呢?帶著這個(gè)問(wèn)題我們?nèi)プx一下 Koa2 的源碼。
在 koa/lib/applications.js 中,先看構(gòu)造函數(shù),其它的都可以不管,關(guān)鍵就是 this.middleware ,它是一個(gè) inbound 隊(duì)列:
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() 來(lái)把中間件放入隊(duì)列中:
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;
}
接著我們看框架對(duì)端口監(jiān)聽(tīng)進(jìn)行了一個(gè)簡(jiǎn)單的封裝:
// 封裝之前 http.createServer(app.callback()).listen(...)
listen(...args) {
debug('listen');
const server = http.createServer(this.callback());
return server.listen(...args);
}
中間件的管理關(guān)鍵就在于 this.callback() ,看一下這個(gè)方法:
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 方法實(shí)際上是 Koa2 的一個(gè)核心模塊 koa-compose (https://github.com/koajs/compose),在這個(gè)模塊中封裝了中間件執(zhí)行的方法:
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 通過(guò)遞歸對(duì)中間件隊(duì)列進(jìn)行了 反序遍歷 ,生成了一個(gè) Promise 鏈,接下來(lái),只需要調(diào)用 Promise 就可以執(zhí)行中間件函數(shù)了:
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);
}
從源碼中可以發(fā)現(xiàn), next() 中返回的是一個(gè) Promise ,所以通用的中間件寫(xiě)法是:
app.use((ctx, next) => {
const start = new Date();
return next().then(() => {
const ms = new Date() - start;
console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
});
});
當(dāng)然如果要用 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 的項(xiàng)目中間件是基于生成器的,需要使用 koa-convert 來(lái)進(jìn)行平滑升級(jí):
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`);
}));
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
- Node.js使用Middleware中間件教程詳解
- node.js?express和koa中間件機(jī)制和錯(cuò)誤處理機(jī)制
- node.js使用express-fileupload中間件實(shí)現(xiàn)文件上傳
- 淺談node.js中間件有哪些類(lèi)型
- node.js中路由,中間件,get請(qǐng)求和post請(qǐng)求的參數(shù)詳解
- node.js中express中間件body-parser的介紹與用法詳解
- node.js 中間件express-session使用詳解
- node.js cookie-parser 中間件介紹
- Node.js的中間件及使用方法詳解
相關(guān)文章
node Buffer緩存區(qū)常見(jiàn)操作示例
這篇文章主要介紹了node Buffer緩存區(qū)常見(jiàn)操作,涉及node.js操作Buffer緩存的創(chuàng)建、寫(xiě)入、讀取、轉(zhuǎn)換等相關(guān)實(shí)現(xiàn)技巧,需要的朋友可以參考下2019-05-05
使用nodeAPI時(shí)遇到過(guò)異步問(wèn)題解決
這篇文章主要為大家介紹了使用nodeAPI時(shí)遇到過(guò)異步問(wèn)題解決,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-01-01
Node.js實(shí)現(xiàn)用戶(hù)身份驗(yàn)證和授權(quán)的示例代碼
在web開(kāi)發(fā)中,我們常常需要對(duì)一些敏感的url進(jìn)行訪(fǎng)問(wèn)權(quán)限控制,本文主要介紹了Node.js實(shí)現(xiàn)用戶(hù)身份驗(yàn)證和授權(quán)的示例代碼,具有一定的參考價(jià)值,感興趣的了解一下2024-02-02
Node.js控制臺(tái)彩色輸出的方法與原理實(shí)例詳解
這篇文章主要給大家介紹了關(guān)于Node.js控制臺(tái)彩色輸出的方法與原理的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用Node.js具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-12-12
nestjs搭建HTTP與WebSocket服務(wù)詳細(xì)過(guò)程
這篇文章主要介紹了nestjs搭建HTTP與WebSocket服務(wù)詳細(xì)過(guò)程的相關(guān)資料,需要的朋友可以參考下2022-11-11
Node.js中使用計(jì)時(shí)器定時(shí)執(zhí)行函數(shù)詳解
這篇文章主要介紹了Node.js中使用計(jì)時(shí)器定時(shí)執(zhí)行函數(shù)詳解,本文使用了Node.js中的setTimeout和setInterval函數(shù),需要的朋友可以參考下2014-08-08

