koa 是一個很是輕量優雅的 node 應用開發框架,趁着雙十一值班的空當閱讀了下其源代碼,其中一些比較有意思的地方整理成文與你們分享一下。javascript
咱們常常把 koa 中間件的執行機制類比於剝洋蔥,這樣設計其執行順序的好處是咱們再也不須要手動去管理 request 和 response 的業務執行流程,且一箇中間件對於 request 和 response 的不一樣邏輯可以放在同一個函數中,能夠幫助咱們極大的簡化代碼。在瞭解其實現原理以前,先來介紹一下 koa 的總體代碼結構:java
lib
|-- application.js
|-- context.js
|-- request.js
|-- response.js
複製代碼
application 是整個應用的入口,提供 koa constructor 以及實例方法屬性的定義。context 封裝了koa ctx 對象的原型對象,同時提供了對 response 和 request 對象下許多屬性方法的代理訪問,request.js 和 response.js 分別定義了ctx request 和 response 屬性的原型對象。node
接下來讓咱們來看 application.js中的一段代碼:git
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 中間件應用示例代碼以下所示:github
// 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 request
ware1 request
ware1 response
ware0 response
複製代碼
從上述 compose 的源碼能夠看出,每一箇中間件所接受的 next 函數入參都是在 compose 返回函數中定義的 dispatch 函數,dispatch接受下一個中間件在 middlewares 數組中的索引做爲入參,該索引就像一個遊標同樣,每當 next 函數執行後,遊標向後移一位,以獲取 middlaware 數組中的下一個中間件函數 進行執行,直到數組中最後一箇中間件也就是使用 app.use 方法添加的最後一箇中間件執行完畢以後再依次 回溯執行。整個流程實際上就是函數的調用棧,next 函數的執行就是下一個中間件的執行,只是 koa 在函數基礎上加了一層 promise 封裝以便在中間件執行過程當中可以將捕獲到的異常進行統一處理。 以上述編寫的應用示例代碼做爲例子畫出函數執行調用棧示意圖以下:
api
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 源碼以下:promise
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 過程當中遇到的異常,整個實現過程能夠說是至關簡潔優雅。bash
經過上面的例子能夠看出 promise 的能量是很是強大的,koa 的中間件實現和 co 模塊的實現都是基於 promise,除了應用於平常的異步流程控制,在開發過程當中咱們還能夠大大挖掘其潛力,幫助咱們完成一些自動化程序工做流的事情。app