這篇文章會講些什么?
- 如何從零開始完成一個涵蓋Koa核心功能的Node.js類庫
- 從代碼層面解釋Koa一些代碼寫法的原因:如中間件為什么必須調(diào)用next函數(shù)柠衅、ctx是怎么來的和一個請求是什么關(guān)系
我們知道Koa類庫主要有以下幾個重要特性:
- 支持洋蔥圈模型的中間件機制
- 封裝request、response提供context對象述么,方便http操作
- 異步函數(shù)蝌数、中間件的錯誤處理機制
第一步:基礎(chǔ)Server運行
目標:完成基礎(chǔ)可行新的Koa Server
- 支持app.listen監(jiān)聽端口啟動Server
- 支持app.use添加類middleware處理函數(shù)
核心代碼如下:
class Koa {
private middleware: middlewareFn = () => {};
constructor() {}
listen(port: number, cb: noop) {
const server = http.createServer((req, res) => {
this.middleware(req, res);
});
return server.listen(port, cb);
}
use(middlewareFn: middlewareFn) {
this.middleware = middlewareFn;
return this;
}
}
const app = new Koa();
app.use((req, res) => {
res.writeHead(200);
res.end("A request come in");
});
app.listen(3000, () => {
console.log("Server listen on port 3000");
});
第二步:洋蔥圈中間件機制實現(xiàn)
目標:接下來我們要完善listen和use方法,實現(xiàn)洋蔥圈中間件模型
如下面代碼所示度秘,在這一步中我們希望app.use能夠支持添加多個中間件顶伞,并且中間件是按照洋蔥圈(類似深度遞歸調(diào)用)的方式順序執(zhí)行
app.use(async (req, res, next) => {
console.log("middleware 1 start");
// 具體原因我們會在下面代碼實現(xiàn)詳細講解
await next();
console.log("middleware 1 end");
});
app.use(async (req, res, next) => {
console.log("middleware 2 start");
await next();
console.log("middleware 2 end");
});
app.use(async (req, res, next) => {
res.writeHead(200);
res.end("An request come in");
await next();
});
app.listen(3000, () => {
console.log("Server listen on port 3000");
});
上述Demo有三個需要我們注意的點:
- 在中間件中next()函數(shù)必須且只能調(diào)用一次
- 調(diào)用next函數(shù)時必須使用await
我們會在接下來的代碼中逐個分析這些使用方法的原因,下面我們來看一看具體怎么實現(xiàn)這種洋蔥圈機制:
class Koa {
...
use(middlewareFn: middlewareFn) {
// 1剑梳、調(diào)用use時唆貌,使用數(shù)組存貯所有的middleware
this.middlewares.push(middlewareFn);
return this;
}
listen(port: number, cb: noop) {
// 2、 通過composeMiddleware將中間件數(shù)組轉(zhuǎn)換為串行[洋蔥圈]調(diào)用的函數(shù)垢乙,在createServer中回調(diào)函數(shù)中調(diào)用
// 所以真正的重點就是 composeMiddleware锨咙,如果做到的,我們接下來看該函數(shù)的實現(xiàn)
// BTW: 從這里可以看到 fn 是在listen函數(shù)被調(diào)用之后就生成了追逮,這就意味著我們不能在運行時動態(tài)的添加middleware
const fn = composeMiddleware(this.middlewares);
const server = http.createServer(async (req, res) => {
await fn(req, res);
});
return server.listen(port, cb);
}
}
// 3酪刀、洋蔥圈模型的核心:
// 入?yún)ⅲ核惺占闹虚g件
// 返回:串行調(diào)用中間件數(shù)組的函數(shù)
function composeMiddleware(middlewares: middlewareFn[]) {
return (req: IncomingMessage, res: ServerResponse) => {
let start = -1;
// dispatch:觸發(fā)第i個中間件執(zhí)行
function dispatch(i: number) {
// 剛開始可能不理解這里為什么這么判斷,可以看完整個函數(shù)在來思考這個問題
// 正常情況下每次調(diào)用前 start < i钮孵,調(diào)用完next() 應(yīng)該 start === i
// 如果調(diào)用多次next()骂倘,第二次及以后調(diào)用因為之前已完成start === i賦值,所以會導(dǎo)致 start >= i
if (i <= start) {
return Promise.reject(new Error("next() call more than once!"));
}
if (i >= middlewares.length) {
return Promise.resolve();
}
start = i;
const middleware = middlewares[i];
// 重點來了0拖@浴!
// 取出第i個中間件執(zhí)行,并將dispatch(i+1)作為next函數(shù)傳給各下一個中間件
return middleware(req, res, () => {
return dispatch(i + 1);
});
}
return dispatch(0);
};
}
主要涉及到Promise幾個知識點:
- async 函數(shù)返回的是一個Promise對象【所以的中間件都會返回一個promise對象】
- async 函數(shù)內(nèi)部遇到 await 調(diào)用時會暫停執(zhí)行await函數(shù)荧库,等待返回結(jié)果后繼續(xù)向下執(zhí)行
- async 函數(shù)內(nèi)部發(fā)生錯誤會導(dǎo)致返回的Promise變?yōu)閞eject狀態(tài)
現(xiàn)在我們在回顧之前提出的幾個問題:
-
koa中間件中為什么必須且只能調(diào)用一次next函數(shù)
可以看到如果不調(diào)用next堰塌,就不會觸發(fā)dispatch(i+1),下一個中間件就沒辦法觸發(fā)电爹,造成假死狀態(tài)最終請求超時 調(diào)用多次next則會導(dǎo)致下一個中間件執(zhí)行多次
-
next() 調(diào)用為什么需要加 await
這也是洋蔥圈調(diào)用機制的核心蔫仙,當執(zhí)行到 await next(),會執(zhí)行next()【調(diào)用下一個中間件】等待返回結(jié)果丐箩,在接著向下執(zhí)行
第三步:Context提供
目標:封裝Context摇邦,提供request、response的便捷操作方式
// 1屎勘、 定義KoaRequest施籍、KoaResponse、KoaContext
interface KoaContext {
request?: KoaRequest;
response?: KoaResponse;
body: String | null;
}
const context: KoaContext = {
get body() {
return this.response!.body;
},
set body(body) {
this.response!.body = body;
}
};
function composeMiddleware(middlewares: middlewareFn[]) {
return (context: KoaContext) => {
let start = -1;
function dispatch(i: number) {
// ..省略其他代碼..
// 2概漱、所有的中間件接受context參數(shù)
middleware(context, () => {
return dispatch(i + 1);
});
}
return dispatch(0);
};
}
class Koa {
private context: KoaContext = Object.create(context);
listen(port: number, cb: noop) {
const fn = composeMiddleware(this.middlewares);
const server = http.createServer(async (req, res) => {
// 3丑慎、利用req、res創(chuàng)建context對象
// 這里需要注意:context是創(chuàng)建一個新的對象瓤摧,而不是直接賦值給this.context
// 因為context適合請求相關(guān)聯(lián)的竿裂,這里也保證了每一個請求都是一個新的context對象
const context = this.createContext(req, res);
await fn(context);
if (context.response && context.response.res) {
context.response.res.writeHead(200);
context.response.res.end(context.body);
}
});
return server.listen(port, cb);
}
// 4、創(chuàng)建context對象
createContext(req: IncomingMessage, res: ServerResponse): KoaContext {
// 為什么要使用Object.create而不是直接賦值照弥?
// 原因同上需要保證每一次請求request腻异、response、context都是全新的
const request = Object.create(this.request);
const response = Object.create(this.response);
const context = Object.create(this.context);
request.req = req;
response.res = res;
context.request = request;
context.response = response;
return context;
}
}
第四步:異步函數(shù)錯誤處理機制
目標:支持通過 app.on("error")这揣,監(jiān)聽錯誤事件處理異常
我們回憶下在Koa中如何處理異常悔常,代碼可能類似如下:
app.use(async (context, next) => {
console.log("middleware 2 start");
// throw new Error("出錯了");
await next();
console.log("middleware 2 end");
});
// koa統(tǒng)一錯誤處理:監(jiān)聽error事件
app.on("error", (error, context) => {
console.error(`請求${context.url}發(fā)生了錯誤`);
});
從上面的代碼可以看到核心在于:
- Koa實例app需要支持事件觸發(fā)、事件監(jiān)聽能力
- 需要我們捕獲異步函數(shù)異常给赞,并觸發(fā)error事件
下面我們看具體代碼如何實現(xiàn):
// 1机打、繼承EventEmitter,增加事件觸發(fā)片迅、監(jiān)聽能力
class Koa extends EventEmitter {
listen(port: number, cb: noop) {
const fn = composeMiddleware(this.middlewares);
const server = http.createServer(async (req, res) => {
const context = this.createContext(req, res);
// 2残邀、await調(diào)用fn,可以使用try catch捕獲異常障涯,觸發(fā)異常事件
try {
await fn(context);
if (context.response && context.response.res) {
context.response.res.writeHead(200);
context.response.res.end(context.body);
}
} catch (error) {
console.error("Server Error");
// 3罐旗、觸發(fā)error時提供context更多信息,方面日志記錄唯蝶,定位問題
this.emit("error", error, context);
}
});
return server.listen(port, cb);
}
}
總結(jié)
至此我們已經(jīng)使用TypeScript完成簡版Koa類庫九秀,支持了
- 洋蔥圈中間件機制
- Context封裝request、response
- 異步異常錯誤處理機制
完整Demo代碼可以參考koa2-reference
更多精彩文章粘我,歡迎大家Star我們的倉庫鼓蜒,我們每周都會推出幾篇高質(zhì)量的大前端領(lǐng)域相關(guān)文章痹换。