初衷
個人覺得學習原始碼需要帶著目的去看, 才能達到效果, 但是公司又沒有上Node, 沒有實踐怎麼辦呢?最近發現通過除錯Koa2原始碼也是個不錯的法子。
準備工作
- 安裝node
- 安裝vscode
- 學習如何在vscode下除錯
複製程式碼
關於Node下除錯推薦閱讀下:《Node.js 除錯指南》。
我們這裡只需要學習如何在vscode下除錯即可。 具體就不詳情說了, 見連結, 有問題我們可以討論。
從Hello World開始學習
// 安裝koa2, nodemon等後, 來個入門的hello world
const Koa = require('koa')
const app = new Koa()
const port = 3000
app.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.html
module.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();
}
複製程式碼