最近一年零零散散看了不少開源專案的原始碼, 多少也有點心得, 這裡想通過這篇文章總結一下, 這裡以Koa為例, 前段時間其實看過Koa的原始碼, 但是發現理解的有點偏差, 所以重新過一遍.
不得不說閱讀tj的程式碼真的收穫很大, 沒啥奇技淫巧, 程式碼優雅, 設計極好. 註釋什麼的就更不用說了. 總之還是推薦把他的專案都過一遍(逃)
跑通例子
Koa作為一個web框架, 我們要去閱讀它的原始碼肯定是得知道它的用法, Koa的文件也很簡單, 它一開始就提供了一個例子:
const Koa = require(`koa`);
const app = new Koa();
app.use(async ctx => {
ctx.body = `Hello World`;
});
app.listen(3000);
複製程式碼
這是啟動最基本的的web服務, 這個跑起來沒啥問題.
同樣, 文件也提供了作為Koa的核心賣點的中介軟體的基本用法:
const Koa = require(`koa`);
const app = new Koa();
// x-response-time
app.use(async (ctx, next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
ctx.set(`X-Response-Time`, `${ms}ms`);
});
// logger
app.use(async (ctx, next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
console.log(`${ctx.method} ${ctx.url} - ${ms}`);
});
// response
app.use(async ctx => {
ctx.body = `Hello World`;
});
app.listen(3000);
複製程式碼
上面程式碼可能跟我們之前寫的js程式碼常識不太符合了, 因為async/await會暫停作案現場, 類似同步. 也就是碰到await next
, 程式碼會跳出當前中介軟體, 執行下一個, 最終還回原路返回, 依次執行await next
下面的程式碼, 當然這只是一個表述而已, 實際就是一個遞迴返回Promise, 後面會提到.
閱讀目標
好了. 我們知道Koa怎麼用了, 那對於這個框架我們想知道什麼呢. 先看一下原始碼的目錄結構好了:
注意這個compose.js
是我為了方便修改原始碼拉過來的, 其實它是額外的一個包.
application.js
作為入口檔案肯定是個建構函式
context.js
就是ctx
咯
request.js
response.js
那我們讀原始碼總需要一個目標吧, 這篇文章裡我們假定目標就是弄懂Koa的中介軟體原理好了
分析執行流程
好, 目標也有了, 下面正式進入原始碼閱讀狀態. 我們以最簡單的示例程式碼作為入口來切入Koa的執行過程:
const app = new Koa();
複製程式碼
上面我們可以看到Koa是作為建構函式引用的, 那麼我們來看看入口檔案Application.js
匯出了個啥:
module.exports = class Application extends Emitter {
// ...
}
複製程式碼
毫無疑問是可以對應上的, 匯出了一個類.
app.use(async ctx => {
ctx.body = `Hello World`;
});
複製程式碼
看上面的東西似乎進入正題了, 我們知道use就是引用了一箇中介軟體, 那來看看use是個啥玩意:
use(fn) {
if (typeof fn !== `function`) throw new TypeError(`middleware must be a function!`);
if (isGeneratorFunction(fn)) {
deprecate(`Support for generators will be removed in v3. ` +
`See the documentation for examples of how to convert old middleware ` +
`https://github.com/koajs/koa/blob/master/docs/migration.md`);
fn = convert(fn);
}
debug(`use %s`, fn._name || fn.name || `-`);
this.middleware.push(fn);
return this;
}
複製程式碼
太長太臭, 精簡一下
use(fn) {
this.middleware.push(fn);
return this;
}
複製程式碼
emm 這下就很清楚了, 就是維護了一箇中介軟體陣列middleware
, 到這裡不要忘了我們的目標: Koa的中介軟體原理, 既然找到這個中介軟體陣列了, 我們就來看看它是怎麼被呼叫的吧. 全域性搜一下, 我們發現其實就一個方法裡用到了middleware
:
callback() {
const fn = compose(this.middleware);
if (!this.listeners(`error`).length) this.on(`error`, this.onerror);
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
};
return handleRequest;
}
複製程式碼
上面的程式碼可以看到, 似乎有一個compose
對middleware進行處理了, 我們好像離真相越來越近了
function compose (middleware) {
/**
* @param {Object} context
* @return {Promise}
* @api public
*/
return function (context, next) {
// last called middleware #
let index = -1
return dispatch(0)
function dispatch (i) {
index = i
let fn = middleware[i]
if (i === middleware.length) fn = next
if (!fn) return Promise.resolve()
try {
return Promise.resolve(fn(context, function next () {
return dispatch(i + 1)
}))
} catch (err) {
return Promise.reject(err)
}
}
}
}
複製程式碼
刪除邊界條件, 錯誤處理
compose.js
的程式碼很短, 但是還是嫌長怎麼辦, 之前有文章提到的, 刪除邊界條件和異常處理:
function compose (middleware) {
/**
* @param {Object} context
* @return {Promise}
* @api public
*/
return function (context, next) {
let index = -1
return dispatch(0)
function dispatch (i) {
index = i
let fn = middleware[i]
if (!fn) return Promise.resolve()
return Promise.resolve(fn(context, function next () {
return dispatch(i + 1)
}))
}
}
}
複製程式碼
這麼一看就清晰多了, 不就是一個遞迴遍歷middleware
嘛. 似乎跟express有點像.
猜想結論
大膽假設嘛, 前面提到了, await 會暫停執行, 那await next
似乎暫停的就是這裡, 然後不斷遞迴呼叫中介軟體, 然後遞迴中斷了, 程式碼又從一個個的promise裡退出來, 似乎這樣就很洋蔥了.
emm 到底是不是這樣呢, 我也不知道. 比較還想再水一篇文章呢.