中間件是 Koa 中的核心概念赛不,必須完全掌握碑宴。
一裆甩、什么是中間件
先來看下面這段代碼:
app.use(async (ctx, next) => {
await next();
ctx.response.type = "text/html";
ctx.response.body = "<h1>Hello World</h1>";
});
上述代碼的作用是:每收到一個 HTTP 請求之斯,Koa 應(yīng)用服務(wù)端都會調(diào)用 async 箭頭函數(shù)進(jìn)行響應(yīng)光涂,然后返回響應(yīng)內(nèi)容給客戶端庞萍。
在上述代碼中,app 是一個 Koa 實例對象忘闻,它用 use 方法注冊了一個函數(shù)钝计,這個函數(shù)就是中間件。所以齐佳,我們可以下定義私恬,中間件就是一個函數(shù),而且 Koa 應(yīng)用自動給這個函數(shù)傳入了兩個默認(rèn)參數(shù)炼吴。那么本鸣,Koa 應(yīng)用程序其實就是一個包含一組中間件函數(shù)的對象。
中間件參數(shù)
- 第一個參數(shù):ctx
這是一個上下文對象硅蹦,用于處理 HTTP 請求和響應(yīng)荣德。
- 第二個參數(shù):next
這是一個函數(shù),執(zhí)行后返回一個 Promise 對象提针。它的作用是將程序的執(zhí)行權(quán)交給下一個中間件命爬,等下一個以及后面的中間件全部執(zhí)行結(jié)束后,再回到當(dāng)前中間件繼續(xù)執(zhí)行辐脖。
二饲宛、中間件的最佳實踐
2.1 命名中間件
我們可以給中間件進(jìn)行命名,這樣在 Debug 時就會顯示函數(shù)名嗜价,有助于定位調(diào)試艇抠。
// 定義中間件,命名為 logger
async function logger(ctx, next) {
// do something
};
}
// 注冊中間件
app.use(logger)
2.2 中間件選項
在創(chuàng)建公共中間件時久锥,可以將中間件包裝在另外一個函數(shù)中家淤,這有利于功能擴(kuò)展。
// 定義 logger 函數(shù)瑟由,用于擴(kuò)展功能
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 應(yīng)用程序由一組中間件組成眼溶,所以整個的處理過程就類似于先進(jìn)后出的堆棧結(jié)構(gòu),可以用如下這張洋蔥切面圖形象地來解釋多個不同功能的中間件的執(zhí)行順序厘惦。
四偷仿、中間件組合
有時候需要將多個中間件組合起來作為一個中間件,以便于重用和導(dǎo)出宵蕉,這就需要使用 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
- 當(dāng)變換組合順序,比如
compose([random, pi, backwards])
稼稿,然后再次訪問上述兩個 URL 地址薄榛,控制臺輸出結(jié)果是什么们颜?
五锌介、實戰(zhàn)演練
在實際應(yīng)用中咒锻,經(jīng)常需要記錄服務(wù)器的響應(yīng)時間也物,即服務(wù)器從接收到 HTTP 請求到返回響應(yīng)內(nèi)容給客戶端所耗的時長。下面使用 Koa 的中間件機(jī)制實現(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} - 響應(yīng)時間: ${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è)務(wù)需求炫彩,上述代碼中使用了三個中間件对途,各自的都有不同的功能:
- logger 中間件:用于控制臺打印請求的響應(yīng)時間
- x-response-time 中間件:用于設(shè)置響應(yīng)頭信息
- response 中間件:用于返回響應(yīng)內(nèi)容給客戶端
用瀏覽器訪問 http://loacalhost:3000/user
改执,控制臺輸出結(jié)果如下:
GET /user - 響應(yīng)時間: 0ms