koa2原始碼解析
koa版本2.4.1
執行流程
以如下示例程式碼進行說明
const Koa = require("koa");
// 1.執行建構函式
const app = new Koa();
// 2.註冊中介軟體
app.use(async (ctx, next) => {
ctx.body = "Hello World";
});
// 3.啟動指定埠的http服務
app.listen(3000);
複製程式碼
1.建構函式
constructor() {
super(); // 繼承至Emitter
this.proxy = false; // 是否設定代理
this.middleware = []; // 儲存app.use註冊的中介軟體
this.subdomainOffset = 2;
this.env = process.env.NODE_ENV || "development"; // 環境變數
this.context = Object.create(context); // this.context物件之後會新增屬性擴充套件成ctx物件
this.request = Object.create(request);
this.response = Object.create(response);
// context,request,response物件詳細說明見context.js,request.js,response.js
}
複製程式碼
2.註冊中介軟體
app.use(fn)主要就是將fn放入中介軟體陣列中,並通過返回this實現鏈式呼叫
use(fn){
// 省略轉換function*的邏輯
this.middleware.push(fn);
return this;
}
複製程式碼
3.啟動指定埠的http服務
從如下程式碼可以看出app.listen內部還是呼叫原生的http模組來啟動服務
listen(...args) {
const server = http.createServer(this.callback()); // 呼叫原生http.createServer啟動服務
return server.listen(...args);
}
複製程式碼
this.callback執行會返回handleRequest作為http.createServer的引數
callback() {
// 處理中介軟體, 實現洋蔥模型的核心方法
const fn = compose(this.middleware);
// 沒有監聽error事件則繫結預設error事件處理
if (!this.listeners("error").length) this.on("error", this.onerror);
// http.createServer(handleRequest)
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res); // 對原生的req,res進行擴充套件封裝成ctx物件
return this.handleRequest(ctx, fn); // 處理請求(執行中介軟體並設定res物件)
};
return handleRequest;
}
複製程式碼
callback方法是koa對中介軟體處理以及設定響應的核心邏輯
我們先來看下compose(this.middleware)
, compose實現了koa中介軟體呼叫邏輯
// koa-compose模組
function compose(middleware){
return function (context, next) {
let index = -1;
return dispatch(0); // 返回一個函式,用於開始執行第一個中介軟體,可以通過執行next呼叫後續中介軟體
// dispatch會始終返回一個Promise物件,koa中介軟體的非同步處理邏輯核心就是利用Promise鏈
function dispatch(i) {
if (i <= index) {
// 變數index由於js閉包會在中介軟體執行過程中一直存在,用於判斷next是否多次執行
return Promise.reject(new Error("next() called multiple times"));
}
index = i;
let fn = middleware[i];
// 如果所有的中介軟體都已執行完,由於koa執行compose返回的函式fnMiddleware(ctx)並沒有傳next,所以fn為undefined,直接返回Promise.resolve()
if (i === middleware.length) fn = next;
if (!fn) return Promise.resolve();
try {
/*
當前中介軟體被包裹成了Promise物件,並且next中通過dispatch(i+1)來執行下一個中介軟體。需要注意一點next中必須return。因為Promise執行機制是:當promise1物件return另一個pormise2,只有pomrise2狀態變為resolved之後,promise1才會resolved。如果沒有return一個Promise,那麼當前中介軟體執行完之後這個Promise就resolved,後續中介軟體可能就不會執行
*/
return Promise.resolve(
fn(context, function next() {
return dispatch(i + 1);
})
);
} catch (err) {
// 中介軟體執行發生異常時,直接rejected停止後續中介軟體的執行。只需要在最後返回的Promise新增catch,就可以捕獲已經執行過的中介軟體發生異常
return Promise.reject(err);
}
}
}
複製程式碼
compose內部的中介軟體的呼叫邏輯見上文註釋不在複述,下面說一下為什麼koa中介軟體執行是洋蔥模型?
見如下程式碼
app.use(middleware = async (ctx, next) => {
// 程式碼1
await next();
// 程式碼2
});
複製程式碼
當middleware中介軟體執行時,會先執行程式碼1,再執行await next(),await會等到next返回的Promise狀態變為resolve之後再執行程式碼2
執行順序為:程式碼1 => 其他中介軟體(middleware2 => middleware3 => ... ) => 程式碼2
洋蔥是由很多層組成的,你可以把每個中介軟體看作洋蔥裡的一層,根據app.use的呼叫順序中介軟體由外層到裡層組成了整個洋蔥,整個中介軟體執行過程相當於由外到內再到外地穿透整個洋蔥
講完compose,接下來來看下this.callback裡面的handleRequest
方法,呼叫方式http.createServer(handleRequest)
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res); // 對原生的req,res進行擴充套件封裝成ctx物件
return this.handleRequest(ctx, fn); // 處理請求(執行中介軟體並設定res物件)
};
複製程式碼
handleRequest內部呼叫了createContext和handleRequest,
createContext方法會通過在context物件擴充套件一些常用物件生成ctx對像。koa通過攔截get和set操作來實現代理(類似Object.defineProperty)
例如:ctx攔截了body的get和set,實現了對ctx.response的代理。對ctx.body的取值和賦值,實際操作的是ctx.response.body。好處就是將response的邏輯分離到了response.js中
handleRequest方法會呼叫this.handleRequest,程式碼如下
handleRequest(ctx, fnMiddleware) {
const res = ctx.res;
res.statusCode = 404; // 沒有呼叫response.writeHead時的預設響應狀態碼
const onerror = err => ctx.onerror(err); // 中介軟體的錯誤處理
const handleResponse = () => respond(ctx); // 處理請求,根據請求返回正確的狀態碼和內容
onFinished(res, onerror); // Execute a callback when a HTTP request closes, finishes, or errors.
return fnMiddleware(ctx).then(handleResponse).catch(onerror); // fnMiddleware為compose(this.middleware)返回的Promise
}
複製程式碼
fnMiddleware(ctx).then(handleResponse).catch(onerror)
可以理解為3個步驟:
- fnMiddleware(ctx):開始執行第一個中介軟體(可通過next呼叫下一個中介軟體)
- then(handleResponse):一般中介軟體中我們會根據請求來設定ctx.body等欄位,中介軟體呼叫結束之後,koa根據會根據ctx物件來對設定response(響應的相關內容)。例如handleResponse中會通過response.end(body)或者body.pipe(res)來設定響應內容體
- catch(error):捕獲中介軟體執行時可能發生的異常
koa作為web框架,提供了一種可控制非同步流程的中介軟體呼叫方式,並根據中介軟體處理後的結果來設定響應的相關內容
結語
本文大致講了一下koa的執行流程,更多細節見原始碼註釋