koa 是一個非常輕量優雅的 node 應用開發框架,趁著雙十一值班的空當閱讀了下其源代碼,其中一些比較有意思的地方整理成文與大家分享一下。
洋蔥型中間件機制的實現原理
我們經常把 koa 中間件的執行機制類比于剝洋蔥,這樣設計其執行順序的好處是我們不再需要手動去管理 request 和 response 的業務執行流程,且一個中間件對于 request 和 response 的不同邏輯能夠放在同一個函數中,可以幫助我們極大的簡化代碼。在了解其實現原理之前,先來介紹一下 koa 的整體代碼結構:
lib|-- application.js|-- context.js|-- request.js|-- response.js
application 是整個應用的入口,提供 koa constructor 以及實例方法屬性的定義。context 封裝了koa ctx 對象的原型對象,同時提供了對 response 和 request 對象下許多屬性方法的代理訪問,request.js 和 response.js 分別定義了ctx request 和 response 屬性的原型對象。
接下來讓我們來看 application.js中的一段代碼:
listen(...args) { debug('listen'); const server = http.createServer(this.callback()); return server.listen(...args);}callback() { const fn = compose(this.middleware); if (!this.listenerCount('error')) this.on('error', this.onerror); const handleRequest = (req, res) => { const ctx = this.createContext(req, res); return this.handleRequest(ctx, fn); }; return handleRequest;}handleRequest(ctx, fnMiddleware) { const res = ctx.res; res.statusCode = 404; const onerror = err => ctx.onerror(err); const handleResponse = () => respond(ctx); onFinished(res, onerror); return fnMiddleware(ctx).then(handleResponse).catch(onerror);}上述代碼展示了 koa 的基本原理,在其實例方法 listen 中對 http.createServer 進行了封裝 ,然后在回調函數中執行 koa 的中間件,在 callback 中,this.middleware 為業務定義的中間件函數所構成的數組,compose 為 koa-compose 模塊提供的方法,它對中間件進行了整合,是構建 koa 洋蔥型中間件模型的奧妙所在。從 handleRequest 方法中可以看出 compose 方法執行返回的是一個函數,且該函數的執行結果是一個 promise。接下來我們就來一探究竟,看看 koa-compose 是如何做到這些的,其 源代碼和一段 koa 中間件應用示例代碼如下所示:
// compose源碼function compose (middleware) { if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!') for (const fn of middleware) { if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!') } return function (context, next) { // last called middleware # let index = -1 return dispatch(0) function dispatch (i) { if (i <= index) return Promise.reject(new Error('next() called multiple times')) index = i let fn = middleware[i] if (i === middleware.length) fn = next if (!fn) return Promise.resolve() try { return Promise.resolve(fn(context, dispatch.bind(null, i + 1))); } catch (err) { return Promise.reject(err) } } }}/*** 中間件應用示例代碼*/let Koa = require('koa')let app = new Koa()app.use(async function ware0 (ctx, next) { await setTimeout(function () { console.log('ware0 request') }, 0) next() console.log('ware0 response')})app.use(function ware1 (ctx, next) { console.log('ware1 request') next() console.log('ware1 response')})// 執行結果ware0 requestware1 requestware1 responseware0 response從上述 compose 的源碼可以看出,每個中間件所接受的 next 函數入參都是在 compose 返回函數中定義的 dispatch 函數,dispatch接受下一個中間件在 middlewares 數組中的索引作為入參,該索引就像一個游標一樣,每當 next 函數執行后,游標向后移一位,以獲取 middlaware 數組中的下一個中間件函數 進行執行,直到數組中最后一個中間件也就是使用 app.use 方法添加的最后一個中間件執行完畢之后再依次 回溯執行。整個流程實際上就是函數的調用棧,next 函數的執行就是下一個中間件的執行,只是 koa 在函數基礎上加了一層 promise 封裝以便在中間件執行過程中能夠將捕獲到的異常進行統一處理。 以上述編寫的應用示例代碼作為例子畫出函數執行調用棧示意圖如下:

整個 compose 方法的實現非常簡潔,核心代碼僅僅 17 行而已,還是非常值得圍觀學習的。
generator函數類型中間件的執行
v1 版本的 koa 其中間件主流支持的是 generator 函數,在 v2 之后改而支持 async/await 模式,如果依舊使用 generator,koa 會給出一個 deprecated 提示,但是為了向后兼容,目前 generator 函數類型的中間件依然能夠執行,koa 內部利用 koa-convert 模塊對 generator 函數進行了一層包裝,請看代碼:
function convert (mw) { // mw為generator中間件 if (typeof mw !== 'function') { throw new TypeError('middleware must be a function') } if (mw.constructor.name !== 'GeneratorFunction') { // assume it's Promise-based middleware return mw } const converted = function (ctx, next) { return co.call(ctx, mw.call(ctx, createGenerator(next))) } converted._name = mw._name || mw.name return converted}function * createGenerator (next) { return yield next()}從上面代碼可以看出,koa-convert 在 generator 外部包裹了一個函數來提供與其他中間件一致的接口,內部利用 co 模塊來執行 generator 函數,這里我想聊的就是 co 模塊的原理,generator 函數執行時并不會立即執行其內部邏輯,而是返回一個遍歷器對象,然后通過調用該遍歷器對象的 next 方法來執行,generator 函數本質來說是一個狀態機,如果內部有多個 yield 表達式,就需要 next 方法執行多次才能完成函數體的執行,而 co 模塊的能力就是實現 generator 函數的 自動執行,不需要手動多次調用 next 方法,那么它是如何做到的呢?co 源碼如下:
function co(gen) { var ctx = this; var args = slice.call(arguments, 1); // we wrap everything in a promise to avoid promise chaining, // which leads to memory leak errors. // see https://github.com/tj/co/issues/180 return new Promise(function(resolve, reject) { if (typeof gen === "function") gen = gen.apply(ctx, args); if (!gen || typeof gen.next !== "function") return resolve(gen); onFulfilled(); /** * @param {Mixed} res * @return {Promise} * @api private */ function onFulfilled(res) { var ret; try { ret = gen.next(res); } catch (e) { return reject(e); } next(ret); } /** * @param {Error} err * @return {Promise} * @api private */ function onRejected(err) { var ret; try { ret = gen.throw(err); } catch (e) { return reject(e); } next(ret); } /** * Get the next value in the generator, * return a promise. * * @param {Object} ret * @return {Promise} * @api private */ function next(ret) { if (ret.done) return resolve(ret.value); // toPromise是一個函數,返回一個promise示例 var value = toPromise.call(ctx, ret.value); if (value && isPromise(value)) return value.then(onFulfilled, onRejected); return onRejected( new TypeError( "You may only yield a function, promise, generator, array, or object, " + 'but the following object was passed: "' + String(ret.value) + '"' ) ); } });}從 co 源碼來看,它先是手動執行了一次onFulfilled 函數來觸發 generator 遍歷器對象的 next 方法,然后利用promise的onFulfilled 函數去自動完成剩余狀態機的執行,在onRejected 中利用遍歷器對象的 throw 方法拋出執行上一次 yield 過程中遇到的異常,整個實現過程可以說是相當簡潔優雅。
結語
通過上面的例子可以看出 promise 的能量是非常強大的,koa 的中間件實現和 co 模塊的實現都是基于 promise,除了應用于日常的異步流程控制,在開發過程中我們還可以大大挖掘其潛力,幫助我們完成一些自動化程序工作流的事情。
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持武林網。
新聞熱點
疑難解答