中間件是 Koa 中的核心概念,必須完全掌握。
一、什么是中間件
先來看下面這段代碼:
app.use(async (ctx, next) => {
await next();
ctx.response.type = "text/html";
ctx.response.body = "<h1>Hello World</h1>";
});
上述代碼的作用是:每收到一個 HTTP 請求,Koa 應用服務端都會調(diào)用 async 箭頭函數(shù)進行響應,然后返回響應內(nèi)容給客戶端。
在上述代碼中,app 是一個 Koa 實例對象,它用 use 方法注冊了一個函數(shù),這個函數(shù)就是中間件。所以,我們可以下定義,中間件就是一個函數(shù),而且 Koa 應用自動給這個函數(shù)傳入了兩個默認參數(shù)。那么,Koa 應用程序其實就是一個包含一組中間件函數(shù)的對象。
中間件參數(shù)
- 第一個參數(shù):ctx
這是一個上下文對象,用于處理 HTTP 請求和響應。
- 第二個參數(shù):next
這是一個函數(shù),執(zhí)行后返回一個 Promise 對象。它的作用是將程序的執(zhí)行權(quán)交給下一個中間件,等下一個以及后面的中間件全部執(zhí)行結(jié)束后,再回到當前中間件繼續(xù)執(zhí)行。
二、中間件的最佳實踐
2.1 命名中間件
我們可以給中間件進行命名,這樣在 Debug 時就會顯示函數(shù)名,有助于定位調(diào)試。
// 定義中間件,命名為 logger
async function logger(ctx, next) {
// do something
};
}
// 注冊中間件
app.use(logger)
2.2 中間件選項
在創(chuàng)建公共中間件時,可以將中間件包裝在另外一個函數(shù)中,這有利于功能擴展。
// 定義 logger 函數(shù),用于擴展功能
function logger(format) {
format = format || ":method :url";
// 返回一個中間件函數(shù)
return async function (ctx, next) {
const str = format
.replace(":method", ctx.method)
.replace(":url", ctx.url);
console.log(str);
await next();
};
}
// 注冊中間件
app.use(logger());
app.use(logger(":method"));
app.use(logger(":method :url"));
問題:運行上述代碼,控制臺會輸出哪些內(nèi)容?
三、執(zhí)行順序
下面通過具體代碼來演示中間件的執(zhí)行順序,代碼如下:
const Koa = require("koa");
const app = new Koa();
// the first middleware
app.use(async function (ctx, next) {
console.log(">> one"); // 1
await next(); // 2
console.log("<< one"); // 3
});
// the second middleware
app.use(async function (ctx, next) {
console.log(">> two"); // 4
ctx.body = "two"; // 5
await next(); // 6
console.log("<< two"); // 7
});
// the third middleware
app.use(async function (ctx, next) {
console.log(">> three");// 8
await next(); // 9
console.log("<< three");// 10
});
app.listen(3000, () => {
console.log("[demo] server is running at http://localhost:3000");
});
用 Postman 訪問 http://localhost:3000,控制臺輸出結(jié)果如下:
>> one
>> two
>> three
<< three
<< two
<< one
通過輸入結(jié)果發(fā)現(xiàn),三個中間件函數(shù)體的具體執(zhí)行順序如下:
1 ?? 2 ?? 4 ?? 5 ?? 6 ?? 8 ?? 9 ?? 10 ?? 7 ?? 3
問題:將第二個中間件中的 next 那以上代碼注釋后再次運行,控制臺輸出結(jié)果是什么?
解析:執(zhí)行順序為 1 ?? 2 ?? 4 ?? 5 ?? 7 ?? 3,第三個中間件沒有執(zhí)行到
提示
使用瀏覽器訪問,會發(fā)現(xiàn)控制臺輸出了兩次結(jié)果,這是因為訪問http://localhost:3000后,瀏覽器還自動請求了http://localhost:3000/favicon.ico
通過上述代碼演示,我們知道了中間件的處理流程大致分為三部分:
- 前期處理
- 交給其他中間件處理并等待(這一步就是調(diào)用了 next 方法)
- 后期處理
Koa 應用程序由一組中間件組成,所以整個的處理過程就類似于先進后出的堆棧結(jié)構(gòu),可以用如下這張洋蔥切面圖形象地來解釋多個不同功能的中間件的執(zhí)行順序。

四、中間件組合
有時候需要將多個中間件組合起來作為一個中間件,以便于重用和導出,這就需要使用 koa-compose
提示
koa 依賴koa-compose,安裝 koa 時已經(jīng)自動下載到node_modules中,無須單獨安裝
下面看如下代碼:
const compose = require("koa-compose");
// random 中間件
async function random(ctx, next) {
if ("/random" == ctx.path) {
ctx.body = Math.floor(Math.random() * 10);
} else {
console.log("random middleware");
await next();
}
}
// backwards 中間件
async function backwards(ctx, next) {
if ("/backwards" == ctx.path) {
ctx.body = "sdrawkcab";
} else {
console.log("backwards middleware");
await next();
}
}
// pi 中間件
async function pi(ctx, next) {
if ("/pi" == ctx.path) {
ctx.body = String(Math.PI);
} else {
console.log("backwards middleware");
await next();
}
}
// 將三個中間件組合成一個中間件
const all = compose([random, backwards, pi]);
// 注冊中間件
app.use(all);
- 用瀏覽器訪問
http://localhost:3000,控制臺輸出結(jié)果如下:
random middleware
backwards middleware
pi middleware
- 用瀏覽器訪問
http://localhost:3000/backwards,控制臺輸出結(jié)果如下:
random middleware
- 當變換組合順序,比如
compose([random, pi, backwards]),然后再次訪問上述兩個 URL 地址,控制臺輸出結(jié)果是什么?
五、實戰(zhàn)演練
在實際應用中,經(jīng)常需要記錄服務器的響應時間,即服務器從接收到 HTTP 請求到返回響應內(nèi)容給客戶端所耗的時長。下面使用 Koa 的中間件機制實現(xiàn)這一功能,具體代碼如下:
const Koa = require("koa");
const app = new Koa();
// logger
app.use(async (ctx, next) => {
await next();
const rt = ctx.response.get("X-Response-Time");
console.log(`${ctx.method} ${ctx.url} - 響應時間: ${rt}`);
});
// x-response-time
app.use(async (ctx, next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
ctx.response.set("X-Response-Time", `${ms}ms`);
});
// response
app.use(ctx => {
ctx.body = "<h1>Hello World</h1>";
});
app.listen(3000, () => {
console.log("[demo] server is running at http://localhost:3000")
})
按照業(yè)務需求,上述代碼中使用了三個中間件,各自的都有不同的功能:
- logger 中間件:用于控制臺打印請求的響應時間
- x-response-time 中間件:用于設置響應頭信息
- response 中間件:用于返回響應內(nèi)容給客戶端
用瀏覽器訪問 http://loacalhost:3000/user,控制臺輸出結(jié)果如下:
GET /user - 響應時間: 0ms