咱們知道Koa類庫主要有如下幾個重要特性:javascript
本文將由淺到深帶領你們使用TS逐步完成一個實現了Koa核心功能的簡易框架前端
目標:完成基礎可行新的Koa Serverjava
核心代碼以下:git
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");
});
複製代碼
目標:接下來咱們要完善listen和use方法,實現洋蔥圈中間件模型github
以下面代碼所示,在這一步中咱們但願app.use可以支持添加多箇中間件,而且中間件是按照洋蔥圈(相似深度遞歸調用)的方式順序執行數組
app.use(async (req, res, next) => {
console.log("middleware 1 start");
// 這裏有兩個須要注意的點:
// 一、next()函數必須且只能調用一次
// 二、調用next函數時必須使用await
// 具體緣由咱們會在下面代碼實現詳細講解
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");
});
複製代碼
下面咱們來看一看具體怎麼實現這種洋蔥圈機制:app
class Koa {
...
use(middlewareFn: middlewareFn) {
// 一、調用use時,使用數組存貯全部的middleware
this.middlewares.push(middlewareFn);
return this;
}
listen(port: number, cb: noop) {
// 二、 經過composeMiddleware將中間件數組轉換爲串行[洋蔥圈]調用的函數,在createServer中回調函數中調用
// 因此真正的重點就是 composeMiddleware,若是作到的,咱們接下來看該函數的實現
// BTW: 從這裏能夠看到 fn 是在listen函數被調用以後就生成了,這就意味着咱們不能在運行時動態的添加middleware
const fn = composeMiddleware(this.middlewares);
const server = http.createServer(async (req, res) => {
await fn(req, res);
});
return server.listen(port, cb);
}
}
// 三、洋蔥圈模型的核心:
// 入參:全部收集的中間件
// 返回:串行調用中間件數組的函數
function composeMiddleware(middlewares: middlewareFn[]) {
return (req: IncomingMessage, res: ServerResponse) => {
let start = -1;
// dispatch:觸發第i箇中間件執行
function dispatch(i: number) {
// 剛開始可能不理解這裏爲何這麼判斷,能夠看完整個函數在來思考這個問題
// 正常狀況下每次調用前 start < i,調用完next() 應該 start === i
// 若是調用屢次next(),第二次及之後調用由於以前已完成start === i賦值,因此會致使 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];
// 重點來了!!!
// 取出第i箇中間件執行,並將dispatch(i+1)做爲next傳給各下一個中間件
// 如今咱們在回顧以前提出的兩個問題:
// 1. koa中間件中爲何必須且只能調用一次next函數
// 能夠看到若是不調用next,下一個中間件就沒辦法觸發,形成假死狀態最終請求超時
// 調用屢次next則會到時下一個中間件執行屢次
// 2. next() 調用爲何須要加 await
// 這也是洋蔥圈調用機制的核心,當執行到 await next(),會執行next()【調用下一個中間件】等待返回結果,在接着向下執行
return middleware(req, res, () => {
return dispatch(i + 1);
});
}
return dispatch(0);
};
}
複製代碼
目標:封裝Context,提供request、response的便捷操做方式框架
// 一、 定義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) {
// ..省略其餘代碼..
// 二、全部的中間件接受context參數
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) => {
// 三、利用req、res建立context對象
// 這裏須要注意:context是建立一個新的對象,而不是直接賦值給this.context
// 由於context適合請求相關聯的,這裏也保證了每個請求都是一個新的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);
}
// 四、建立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;
}
}
複製代碼
目標:支持經過 app.on("error"),監聽錯誤事件處理異常koa
咱們回憶下在Koa中如何處理異常,代碼可能相似以下:異步
app.use(async (context, next) => {
console.log("middleware 2 start");
// throw new Error("出錯了");
await next();
console.log("middleware 2 end");
});
// koa統一錯誤處理:監聽error事件
app.on("error", (error, context) => {
console.error(`請求${context.url}發生了錯誤`);
});
複製代碼
從上面的代碼能夠看到核心在於:
下面咱們看具體代碼如何實現:
// 一、繼承EventEmitter,增長事件觸發、監聽能力
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);
// 二、await調用fn,可使用try catch捕獲異常,觸發異常事件
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");
// 三、觸發error時提供context更多信息,方面日誌記錄,定位問題
this.emit("error", error, context);
}
});
return server.listen(port, cb);
}
}
複製代碼
至此咱們已經使用TypeScript完成簡版Koa類庫,支持了
完整Demo代碼能夠參考koa2-reference
更多精彩文章,歡迎你們Star咱們的倉庫、關注咱們的掘金號,咱們每週都會推出幾篇高質量的大前端領域相關文章。