Express與Koa中介軟體機制分析(一)

fisher-zh發表於2019-03-13

提到 Node.js 開發,不得不提目前炙手可熱的2大框架 Express 和 Koa。

Express 是一個保持最小規模的靈活的 Node.js Web 應用程式開發框架,為 Web 和移動應用程式提供一組強大的功能。目前使用人數眾多。

Koa 是一個新的 web 框架,由 Express 幕後的原班人馬打造, 致力於成為 web 應用和 API 開發領域中的一個更小、更富有表現力、更健壯的基石。 通過利用 async 函式,Koa 幫你丟棄回撥函式,並有力地增強錯誤處理。 Koa 並沒有捆綁任何中介軟體, 而是提供了一套優雅的方法,幫助您快速而愉快地編寫服務端應用程式。

相信對這兩大框架有一些瞭解的人都或多或少的會了解其中介軟體機制,Express 為線型模型,而 Koa 則為洋蔥型模型。這個系列的部落格主要講解 Express 和 Koa 的中介軟體機制,本篇將主要講解 Express 的中介軟體機制。

Express 中介軟體

connect 曾經是 express 3.x 之前的核心,而 express 4.x 已經把 connect 移除,在 express 中自己實現了 connect 的介面,所以我們本篇中的原始碼解釋將直接使用 connect 原始碼。

示例

下面將使用 Express 實現一個簡單的 demo 來進行中介軟體機制的講解

var express = require('express');

var app = express();
app.use(function (req, res, next) {
    console.log('第一個中介軟體start');
    setTimeout(() => {
        next();
    }, 1000)
    console.log('第一個中介軟體end');
});
app.use(function (req, res, next) {
    console.log('第二個中介軟體start');
    setTimeout(() => {
        next();
    }, 1000)
    console.log('第二個中介軟體end');
});
app.use('/foo', function (req, res, next) {
    console.log('介面邏輯start');
    next();
    console.log('介面邏輯end');
});
app.listen(4000);
複製程式碼

此時的輸出比較符合我們對 Express 線性的理解,其輸出為

第一個中介軟體start
第一個中介軟體end
第二個中介軟體start
第二個中介軟體end
介面邏輯start
介面邏輯end
複製程式碼

但是,如果我們取消掉中介軟體內部的非同步處理直接呼叫 next()

var express = require('express');

var app = express();
app.use(function (req, res, next) {
    console.log('第一個中介軟體start');
    next()
    console.log('第一個中介軟體end');
});
app.use(function (req, res, next) {
    console.log('第二個中介軟體start');
    next()
    console.log('第二個中介軟體end');
});
app.use('/foo', function (req, res, next) {
    console.log('介面邏輯start');
    next();
    console.log('介面邏輯end');
});
app.listen(4000);
複製程式碼

輸出結果為

第一個中介軟體start
第二個中介軟體start
介面邏輯start
介面邏輯end
第二個中介軟體end
第一個中介軟體end
複製程式碼

這種結果不是和 Koa 的輸出很相似嗎?是的,但是它和剝洋蔥模型還是不一樣的,其實這種輸出的結果是由於程式碼的同步執行導致的,並不是說 Express 不是線性的模型。

當我們的中介軟體內沒有進行非同步操作時,其實我們的程式碼最後是以下面這種方式執行的

app.use(function middleware1(req, res, next) {
    console.log('第一個中介軟體start')
        // next()
        (function (req, res, next) {
            console.log('第二個中介軟體start')
                // next()
                (function (req, res, next) {
                    console.log('介面邏輯start')
                        // next()
                        (function handler(req, res, next) {
                            // do something
                        })()
                    console.log('介面邏輯end')
                })()
            console.log('第二個中介軟體end')
        })()
    console.log('第一個中介軟體end')
})
複製程式碼

導致這種執行方式的其實就是 connect 的實現方式,接下來我們進行其原始碼的解析

connect 原始碼解析

connect的原始碼僅有200多行,但是這裡講解只選擇其中部分核心程式碼,並非完整程式碼,檢視全部程式碼請移步 github

中介軟體的掛載主要依賴 proto.use 和 proto.handle,這裡我們刪掉部分 if 判斷以使我們更專注於其內部原理的實現

proto.use = function use(route, fn) {
    var handle = fn;
    var path = route;

    // 這裡是對直接填入回撥函式的進行容錯處理
    // default route to '/'
    if (typeof route !== 'string') {
        handle = route;
        path = '/';
    }
    .
    .
    .
    this.stack.push({ route: path, handle: handle });

    return this;
};
複製程式碼

proto.use 主要將我們需要掛載的中介軟體儲存在其自身 stack 屬性上,同時進行部分相容處理,這一塊比較容易理解。其中介軟體機制的核心為 proto.handle 內部 next 方法的實現。

proto.handle = function handle(req, res, out) {
    var index = 0;
    var stack = this.stack;

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

在刪除到部分非核心程式碼後,可以清晰的看到,proto.handle 的核心就是 next 方法的實現和遞迴呼叫,對存在於 stack 中的中介軟體取出、執行。

這裡便可以解釋上文中非同步和非非同步過程中所輸出的結果的差異了。

  • 當有非同步程式碼時,將會直接跳過繼續執行,此時的 next 方法並未執行,需要等待當前佇列中的事件全部執行完畢,所以此時我們輸出的資料是線性的。
  • 當 next 方法直接執行時,本質上所有的程式碼都已經為同步,所以層層巢狀,最外層的肯定會在最後,輸出了類似剝洋蔥模型的結果。

總結

connect 的實現其基本原理是維護一個 stack 陣列,將所需要掛載的中介軟體處理後全部 push 到陣列內,之後在陣列內迴圈執行 next 方法,直至所有中介軟體掛載完畢,當然這過程中會做一些異常、相容等的處理。

後續

Express與Koa中介軟體機制分析(二) 主要分析 Koa2 的中介軟體實現機制

相關文章