最近一年零零散散看了很多開源項目的源碼, 多少也有點心得, 這裏想經過這篇文章總結一下, 這裏以Koa爲例, 前段時間其實看過Koa的源碼, 可是發現理解的有點誤差, 因此從新過一遍.git
不得不說閱讀tj的代碼真的收穫很大, 沒啥奇技淫巧, 代碼優雅, 設計極好. 註釋什麼的就更不用說了. 總之仍是推薦把他的項目都過一遍(逃)github
Koa做爲一個web框架, 咱們要去閱讀它的源碼確定是得知道它的用法, Koa的文檔也很簡單, 它一開始就提供了一個例子:web
const Koa = require('koa');
const app = new Koa();
app.use(async ctx => {
ctx.body = 'Hello World';
});
app.listen(3000);
複製代碼
這是啓動最基本的的web服務, 這個跑起來沒啥問題.express
一樣, 文檔也提供了做爲Koa的核心賣點的中間件的基本用法:api
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怎麼用了, 那對於這個框架咱們想知道什麼呢. 先看一下源碼的目錄結構好了:promise
注意這個compose.js
是我爲了方便修改源碼拉過來的, 其實它是額外的一個包.app
application.js
做爲入口文件確定是個構造函數 context.js
就是ctx
咯 request.js
response.js
框架
那咱們讀源碼總須要一個目標吧, 這篇文章裏咱們假定目標就是弄懂Koa的中間件原理好了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 究竟是不是這樣呢, 我也不知道. 比較還想再水一篇文章呢.