koa-compose原始碼從零解析
Koa is a new web framework designed by the team behind Express, which aims to be a smaller, more expressive, and more robust foundation for web applications and APIs. By leveraging async functions, Koa allows you to ditch callbacks and greatly increase error-handling. Koa does not bundle any middleware within its core , and it provides an elegant suite of methods that make writing servers fast and enjoyable.
上面是koa的官網的簡單介紹,只需要關心一點: 中介軟體機制是koa的核心。
可以說,理解了中介軟體也就理解了koa框架的精華。而實現中介軟體機制的關鍵是compose函式。
洋蔥模型的基本介紹
每個中介軟體需要依次處理request和response請求。這種中介軟體模型稱為洋蔥模型(Onion model)

洋蔥模型

中介軟體執行過程
上面的程式碼可以記錄response請求的時間。可以看到,利用koa實現 logger
,程式碼相當簡潔。
compose 1.0 版本實現
五年前,前端沒有async的情況下, compose
的實現其實相當複雜,利用了 Thunk、generator、Co
來進行非同步管理。不過,可以看到即使前端變化非常之大, compose
的核心理念依然沒有發生改變。
-
不考慮任何非同步情況,實現洋蔥模型
function fn1(next) { console.log(1); next(); } function fn2(next) { console.log(2); next(); } function fn3(next) { console.log(3); next(); } middleware = [fn1, fn2, fn3] function compose(middleware){ function dispatch (index){ if(index == middleware.length) return ; var curr; curr = middleware[index]; // 這裡使用箭頭函式,讓函式延遲執行 return curr(() => dispatch(++index)) } dispatch(0) }; compose(middleware);
根據分析,最後實際上將幾個函式通過串聯的方式進行了連線:
對於 fn1
來說,next函式就是 ()=> fn2( () => fn3())
-
考慮無promise的非同步情況。(callback+generator)
當出現generator型別的時候,我們next允許接受Generator型別
function * fn1(next) { console.log(1); //如果沒有yield,就無法進行遞迴呼叫 yield next(); } function * fn2(next) { console.log(2); yield next(); } function * fn3(next) { console.log(3); yield next(); } middleware = [fn1, fn2, fn3] function compose(middleware){ function dispatch (index){ if(index == middleware.length) return ; var curr; function* prev(){ console.log('none'); } curr = middleware[index] console.log(curr); return curr(() => dispatch(++index)) } return dispatch(0) }; compose(middleware)
這時候執行, compose(middleware)
實際上是一個 [GeneratorFunction fn1]
的型別。
如果我們需要達到第一種程式碼的執行效果,手動執行如下:
k0 = compose(middleware).next() k1 = k0.value k2 = k1.next().value k3 = k2.next() //輸出為1 2 3
中介軟體多的話,手動執行就無法實現。可以增加一個自動執行generator的函式:
function co (gen) { let g = gen; function next(nex) { let result = nex.next(); if(result.done) return result.value; if(typeof result.value == 'object') { next(result.value); } } next(g); } //再次執行, 輸出為123 co(compose(middleware))
generator+co的方式實現中介軟體程式碼邏輯相當複雜,上面只是考慮了三種情況下的一種。
compose 2.0 版本實現
-
利用promise實現
function compose(middleware){ function dispatch (index){ if(index == middleware.length) return ; var curr; function prev(){ next; } curr = middleware[index]; // 這裡使用箭頭函式,讓函式延遲執行 return curr(() => dispatch(++index)) } dispatch(0) };
當非同步操作使用 async/await
的時候,上面 compose
的實現已經可以解決非同步問題。( async
函式可以看作同步函式)。但是,非同步操作程式碼,如果丟擲錯誤,上面的程式碼無法對錯誤進行捕捉。
function * fn1(next) { console.log(1); throw new Error('錯誤無法捕捉'); //如果沒有yield,就無法進行遞迴呼叫 yield next(); }
考慮到,async其實返回一個Promise型別,我們將所有的中介軟體函式包裹成一個Promise物件。然後,通過 reject
和 catch
來進行錯誤處理。
function compose(middleware){ function dispatch (index){ if(index == middleware.length) return Promise.resolve(); var curr; function prev(){ next; } curr = middleware[index]; // 修改成Promise物件 return Promise.resolve(curr(() => dispatch(++index))); } dispatch(0) };
-
函式式風格實現
從上面的實現我們可以看出來,以上所有的實現,都無非是把中介軟體函式 fn1
, fn2
, fn3
包裹成下列形式:
那麼,對於習慣使用函數語言程式設計的人來說,這其實是一個右向reduce的過程。
function compose () { return this.middlewares.reduceRight( (a, b) => () => b(a), () => {})(); }
然後,如果需要修改返回型別是Promise型別,那麼可以簡單的修改為:
function compose () { return this.middlewares.reduceRight( (a, b) => () => Promise.resolve(b(a)), () => {})(); }