Node.js 中請求的處理
討論 Koa 中間件前,先看原生 Node.js 中是如何創建 server 和處理請求的。
node_server.js
const http = require("http");
const PORT = 3000;
const server = http.createServer((req, res) => {
res.end("hello world!");
});
server.listen(PORT);
console.log(</span>server started at http://localhost:<span class="pl-s1"><span class="pl-pse">${</span><span class="pl-c1">PORT</span><span class="pl-pse">}</span></span><span class="pl-pds">
);
Koa 中請求的處理
Koa 也是通過上面的 http.createServer
創建服務器處理請求的返回 res
。 但在 Koa 的封裝體系下,其提供了十分好用的中間件系統,可對請求 req
及返回 res
進行便捷地處理。
listen(...args) {
debug('listen');
+ const server = http.createServer(this.callback());
return server.listen(...args);
}
Koa 中的 hello world:
server.js
const Koa = require("koa");
const app = new Koa();
app.use(async ctx => {
ctx.body = "Hello World";
});
app.listen(3000);
Koa 中,涉及到對請求返回處理都是通過中間件完成的,像上面為樣,返回頁面一個 Hello World
文本,也是調用 app.use
向 Application
對象注冊了個中間件來完成。
Koa 中間件編寫及使用
Koa 中中間件即一個處理請求的方法,通過調用 app.use(fn)
后,中間件 fn
被保存到了內部一個中間件數組中。
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;
}
通過上面的代碼可看到,注冊的中間件被壓入 Application
對象的 this.middleware
數組。這里有對傳入的方法進行判斷,區分是否為生成器([generator])方法,因為較早版本的 Koa 其中間件是通過生成器來實現的,后面有 async/await
語法后轉向了后者,所以更推薦使用后者,因此這里有廢棄生成器方式的提示。
因為中間件中需要進行的操作是不可控的,完全有可能涉及異步操作,比如從遠端獲取數據或從數據庫查詢數據后返回到 ctx.body
,所以理論上中間件必需是異步函數。
比如實現計算一個請求耗時的中間件,以下分別是通過普通函數配合 Promise 以及使用 async/await
方式實現的版本:
來自官方 README 中使用 Promise 實現中間件的示例代碼
// Middleware normally takes two parameters (ctx, next), ctx is the context for one request,
// next is a function that is invoked to execute the downstream middleware. It returns a Promise with a then function for running code after completion.
app.use((ctx, next) => {
const start = Date.now();
return next().then(() => {
const ms = Date.now() - start;
console.log(</span><span class="pl-s1"><span class="pl-pse">${</span><span class="pl-smi">ctx</span>.<span class="pl-c1">method</span><span class="pl-pse">}</span></span> <span class="pl-s1"><span class="pl-pse">${</span><span class="pl-smi">ctx</span>.<span class="pl-smi">url</span><span class="pl-pse">}</span></span> - <span class="pl-s1"><span class="pl-pse">${</span>ms<span class="pl-pse">}</span></span>ms<span class="pl-pds">
);
});
});
來自官方 README 中使用 async/await
實現中間件的示例代碼
app.use(async (ctx, next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
});
可以看到,一個中間件其簽名是 (ctx,next)=>Promise
,其中 ctx
為請求上下文對象,而 next
是這樣一個函數,調用后將執行流程轉入下一個中間件,如果當前中間件中沒有調用 next
,整個中間件的執行流程則會在這里終止,后續中間件不會得到執行。以下是一個測試。
server.js
app.use(async (ctx, next) => {
console.log(1);
next();
});
app.use(async (ctx, next) => {
console.log(2);
});
app.use(async (ctx, next) => {
console.log(3);
ctx.body = "Hello, world!";
});
執行后控制台輸出:
$ node server.js
1
2
訪問頁面也不會看到 Hello, world!
因為設置響應的代碼 ctx.body = "Hello, world!";
所在的中間件沒有被執行。
compose
下面來看當多次調用 app.use
注冊中間件后,這些中間件是如何被順次執行的。
中間件的執行是跟隨一次請求的。當一個請求來到后台,中間件被順次執行,在各中間件中對請求 request
及 resposne
進行各種處理。
所以從 Koa 中處理請求的地方出發,找到中間件執行的源頭。
通過查看 lib/application.js 中相關代碼:
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;
}
可定位到存儲在 this.middleware
中的中間件數組會傳遞給 compose
方法來處理,處理后得到一個函數 fn
,即這個 compose
方法處理后,將一組中間件函數處理成了一個函數,最終在 handleRequest 處被調用,開啟了中間件的執行流程。
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);
}
即 compose
的簽名長這樣:compose([a, b, c, ...])
,它來自另一個單獨的倉庫 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);
}
}
};
}
這個方法只做了兩件事,
- 定義了一個
dispatch
方法, - 然后調用它
dispatch(0)
這里中間件從數組中取出並順次執行的邏輯便在 dispatch
函數中。
整體方法體中維護了一個索引 index
其初始值為 -1
,后面每調用一次 dispatch
會加 1。當執行 dispatch(0)
時,從中間件數組 middleware
中取出第 0 個中間件並執行,同時將 dispatch(i+1)
作為 next
傳遞到下一次執行。
let fn = middleware[i];
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
所以這里就能理解,為什么中間件中必需調用 next
,否則后續中間件不會執行。
這樣一直進行下去直到所有中間件執行完畢,此時 i === middleware.length
,最后一個中間件已經執行完畢,next
是沒有值的,所以直接 resolve
掉結束中間件執行流程。
if (i === middleware.length) fn = next;
if (!fn) return Promise.resolve();
回到中間件被喚起的地方:
fnMiddleware(ctx)
.then(handleResponse)
.catch(onerror);
中間件完成后,流程到了 handleResponse
。
總結
從中間件執行流程可知道:
- 中間件之間存在順序的問題,先注冊的先執行。
- 中間件中需要調用
next
以保證后續中間件的執行。當然,如果你的中間件會根據一些情況阻止掉后續中間件的執行,那可以不調用next
,比如一個對請求進行權限校驗的中間件可以這么寫:
app.use(async (ctx, next) => {
// 獲取權限數據相關的操作...
if (valid) {
await next();
} else {
ctx.throw(403, "沒有權限!");
}
});