初衷
個人覺得學習原始碼需要帶著目的去看, 才能達到效果, 但是公司又沒有上Node, 沒有實踐怎麼辦呢?最近發現通過除錯Koa2原始碼也是個不錯的法子。
準備工作
- 安裝node- 安裝vscode- 學習如何在vscode下除錯複製程式碼
關於Node下除錯推薦閱讀下:《Node.js 除錯指南》。
我們這裡只需要學習如何在vscode下除錯即可。具體就不詳情說了, 見連結, 有問題我們可以討論。
從Hello World開始學習
// 安裝koa2, nodemon等後, 來個入門的hello worldconst Koa = require('koa')const app = new Koa()const port = 3000app.use(async (ctx, next) =>
{
await next() ctx.response.status = 200 ctx.response.body = 'hello world'
})app.listen(port, () =>
{
console.log(`server is running on the port: ${port
}`)
})複製程式碼
是的沒錯,通過上述這個入門程式碼也能學習到Koa2的原始碼知識。
首先觀察上面用到了的一些API。new Koa()app.use()app.listen()複製程式碼
我們現在開始進入node_modules目錄下找到Koa。
通過package.json得知Koa的入口檔案為
"main": "lib/application.js"複製程式碼
// lib目錄下模組- application.js 例項- context.js 上下文物件- request.js 請求物件- response.js 響應物件複製程式碼
現在我們當然從入口檔案application.js開始。我們的例項程式碼第一行是new koa();
我們肯定是有一個類,這裡是開始的突破點。
// 建構函式繼承node的EventEmitter類 // http://nodejs.cn/api/events.htmlmodule.exports = class Application extends Emitter {
...
}複製程式碼
然後我們去打三個斷點, 分別如下:
之所以在這三個地方打斷點是對應前面提到的執行了Koa2的三個api.通過這三個斷點我們一步步去了解Koa2內部是怎麼執行的。
最開始肯定是執行constructor();
部分註釋見上述截圖。
this.middleware = [];
這個是用來存放通過app.use()註冊的中介軟體的。複製程式碼
重點接下來看app.use()。
很明顯fn指的就是:
async (ctx, next) =>
{
await next() ctx.response.status = 200 ctx.response.body = 'hello world'
}複製程式碼
在use(fn)方法中主要做了以下事情:
1. 錯誤校驗, fn必須是函式, 否則給出錯誤提示2. fn不推薦使用生成器函式, v2版本Koa2會進行轉化, 但是v3就會不支援生成器函式了, 這裡主要是對koa1的向下相容。3. 儲存註冊的中介軟體3. return this. 支援鏈式呼叫複製程式碼
這個時候你可以看懂this大概有這些屬性:
Application {
_events:Object {
} _eventsCount:0 _maxListeners:undefined context:Object {
} env:"development" middleware:Array(1) [] proxy:false request:Object {
} response:Object {
} subdomainOffset:2 Symbol(util.inspect.custom):inspect() {
…
} __proto__:EventEmitter
}複製程式碼
然後進入listen(), 這裡有一段this.callback(), 我們需要去這個方法下打斷點看執行了什麼。
// 其實就是http.createServer(app.callback()).listen(...)的語法糖listen(...args) {
debug('listen');
const server = http.createServer(this.callback());
return server.listen(...args);
}複製程式碼
// callback()做了以下幾件事:1. 通過compose合併中介軟體2. 為應用註冊error事件的監聽器3. 返回一個請求處理函式handleRequest複製程式碼
接下來我們看看this.createContext()和this.handleRequest(),分別打斷點看程式碼。
note: 提一個小問題, node應該經常會發生埠占用問題。複製程式碼
每次請求都會建立一個上下文物件。
handleRequest(ctx, fnMiddleware) {
const res = ctx.res;
res.statusCode = 404;
// 錯誤處理 const onerror = err =>
ctx.onerror(err);
const handleResponse = () =>
respond(ctx);
// 通過第三方庫on-finished監聽http response,當請求結束時執行回撥,這裡傳入的回撥是context.onerror(err),即當錯誤發生時才執行。 onFinished(res, onerror);
// 即將所有中介軟體執行(傳入請求上下文物件ctx),之後執行響應處理函式(respond(ctx)),當丟擲異常時同樣使用onerror(err)處理。 return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}複製程式碼
對respond打斷點
/** * Response helper. * 在所有中介軟體執行完之後執行 */function respond(ctx) {
// allow bypassing koa // 通過設定ctx.respond = false來跳過這個函式,但不推薦這樣子 if (false === ctx.respond) return;
const res = ctx.res;
// 上下文物件不可寫時也會退出該函式 if (!ctx.writable) return;
let body = ctx.body;
const code = ctx.status;
// ignore body // 當返回的狀態碼錶示沒有響應主體時,將響應主體置空: if (statuses.empty[code]) {
// strip headers ctx.body = null;
return res.end();
} // 當請求方法為HEAD時,判斷響應頭是否傳送以及響應主體是否為JSON格式,若滿足則設定響應Content-Length: if ('HEAD' == ctx.method) {
if (!res.headersSent &
&
isJSON(body)) {
ctx.length = Buffer.byteLength(JSON.stringify(body));
} return res.end();
} // status body // 當返回的狀態碼錶示有響應主體,但響應主體為空時,將響應主體設定為響應資訊或狀態碼。並當響應頭未傳送時設定Content-Type與Content-Length: if (null == body) {
if (ctx.req.httpVersionMajor >
= 2) {
body = String(code);
} else {
body = ctx.message || String(code);
} if (!res.headersSent) {
ctx.type = 'text';
ctx.length = Buffer.byteLength(body);
} return res.end(body);
} // 對不同的響應主體進行處理 // responses if (Buffer.isBuffer(body)) return res.end(body);
if ('string' == typeof body) return res.end(body);
if (body instanceof Stream) return body.pipe(res);
// body: json body = JSON.stringify(body);
if (!res.headersSent) {
ctx.length = Buffer.byteLength(body);
} res.end(body);
}複製程式碼
錯誤處理
onerror(err) {
// 當err不為Error型別時丟擲異常。 if (!(err instanceof Error)) throw new TypeError(util.format('non-error thrown: %j', err));
// 當 err.status 是 404 或 err.expose 是 true 時預設錯誤處理程式也不會輸出錯誤 if (404 == err.status || err.expose) return;
// 預設情況下,將所有錯誤輸出到 stderr,除非 app.silent 為 true if (this.silent) return;
const msg = err.stack || err.toString();
console.error();
console.error(msg.replace(/^/gm, ' '));
console.error();
}複製程式碼