Node.js 中介軟體模式

你的肖同學發表於2019-03-04

中介軟體 在 Node.js 中被廣泛使用,它泛指一種特定的設計模式、一系列的處理單元、過濾器和處理程式,以函式的形式存在,連線在一起,形成一個非同步佇列,來完成對任何資料的預處理和後處理。

它的優點在於 靈活性:使用中介軟體我們用極少的操作就能得到一個外掛,用最簡單的方法就能將新的過濾器和處理程式擴充套件到現有的系統上。

常規中介軟體模式

中介軟體模式中,最基礎的組成部分就是 中介軟體管理器,我們可以用它來組織和執行中介軟體的函式,如圖所示:

中介軟體.jpg

要實現中介軟體模式,最重要的實現細節是:

  • 可以通過呼叫use()函式來註冊新的中介軟體,通常,新的中介軟體只能被新增到高壓包帶的末端,但不是嚴格要求這麼做;
  • 當接收到需要處理的新資料時,註冊的中介軟體在意不執行流程中被依次呼叫。每個中介軟體都接受上一個中介軟體的執行結果作為輸入值;
  • 每個中介軟體都可以停止資料的進一步處理,只需要簡單地不呼叫它的毀掉函式或者將錯誤傳遞給回撥函式。當發生錯誤時,通常會觸發執行另一個專門處理錯誤的中介軟體。

至於怎麼處理傳遞資料,目前沒有嚴格的規則,一般有幾種方式

  • 通過新增屬性和方法來增強;
  • 使用某種處理的結果來替換 data;
  • 保證原始要處理的資料不變,永遠返回新的副本作為處理的結果。

而具體的處理方式取決於 中介軟體管理器 的實現方式以及中介軟體本身要完成的任務型別。

舉一個來自於 《Node.js 設計模式 第二版》 的一個為訊息傳遞庫實現 中介軟體管理器 的例子:

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);
    }
}
複製程式碼

接下來只需要建立中介軟體,分別在inboundoutbound中寫入中介軟體函式,然後執行完畢呼叫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 中介軟體一般是這樣的:

function(req, res, next) { ... }
複製程式碼

Koa2 中使用的中介軟體

前面展示的中介軟體模型使用回撥函式實現的,但是現在有一個比較時髦的 Node.js 框架Koa2的中介軟體實現方式與之前描述的有一些不太相同。Koa2中的中介軟體模式移除了一開始使用ES2015中的生成器實現的方法,相容了回撥函式、convert後的生成器以及asyncawait

Koa2官方文件中給出了一個關於中介軟體的 洋蔥模型,如下圖所示:

koa中介軟體.jpg

從圖中我們可以看到,先進入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`);
    });
});
複製程式碼

當然如果要用asyncawait也行:

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`);
}));
複製程式碼

最後,如果覺得文章有點用處的話,求求大佬點個贊!如果發現什麼錯漏也歡迎提出!

相關文章