最近一年零零散散看了很多開源項目的源碼, 多少也有點心得, 這裏想經過這篇文章總結一下, 這裏以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 究竟是不是這樣呢, 我也不知道. 比較還想再水一篇文章呢.