基於 koa 2.11 按如下流程分析:git
const Koa = require('koa'); const app = new Koa(); const one = (ctx, next) => { console.log('1-Start'); next(); ctx.body = { text: 'one' }; console.log('1-End'); } const two = (ctx, next) => { console.log('2-Start'); next(); ctx.body = { text: 'two' }; console.log('2-End'); } const three = (ctx, next) => { console.log('3-Start'); ctx.body = { text: 'three' }; next(); console.log('3-End'); } app.use(one); app.use(two); app.use(three); app.listen(3000);
use 方法定義在 koa/lib/application.js
中:github
use(fn) { // check middleware type, must be a function if (typeof fn !== 'function') throw new TypeError('middleware must be a function!'); // 兼容 generator 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; }
this.middleware數組
這就是一個數組,用來存放全部中間件,而後按順序執行。promise
this.middleware = [];
這個方法定義在 koa/lib/application.js
中:閉包
listen(...args) { debug('listen'); // 建立 http 服務並監聽 const server = http.createServer(this.callback()); return server.listen(...args); }
this.callback()app
callback() { // 處理中間件 const fn = compose(this.middleware); if (!this.listenerCount('error')) this.on('error', this.onerror); const handleRequest = (req, res) => { // 建立 Context const ctx = this.createContext(req, res); // 執行中間件處理請求和響應 return this.handleRequest(ctx, fn); }; return handleRequest; }
this.handleRequestkoa
handleRequest(ctx, fnMiddleware) { const res = ctx.res; res.statusCode = 404; const onerror = err => ctx.onerror(err); // 將響應發出的函數 const handleResponse = () => respond(ctx); onFinished(res, onerror); // 這裏會將 ctx 傳給中間件進行處理, // 當中間件流程走完後, // 會執行 then 函數將響應發出 return fnMiddleware(ctx).then(handleResponse).catch(onerror); }
respond(ctx)函數
function respond(ctx) { // 省略其餘代碼 // ... // 發出響應 res.end(body); }
捋一捋流程,由上面的代碼能夠知道,存放中間的數組是經過 compose
方法進行處理,而後返回一個fnMiddleware
函數,接着將 Context 傳遞給這個函數來進行處理,當fnMiddleware
執行完畢後就用respond
方法將響應發出。ui
compose 函數經過koa-compose
引入:this
const compose = require('koa-compose');
compose 定義在koajs/compose/index.js
下
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) { // 這個 index 是標識上一次執行的中間件是第幾個 let index = -1 // 執行第一個中間件 return dispatch(0) function dispatch (i) { // 檢查中間件是否已經執行過, // 舉個例子,當執行第一個中間件時 dispatch(0), // i = 0, index = -1, 說明沒有執行過, // 而後 index = i, 而 index 經過閉包保存, // 若是執行了屢次,就會報錯 if (i <= index) return Promise.reject(new Error('next() called multiple times')) index = i // 經過傳入的索引從數組中獲取中間件 let fn = middleware[i] // 若是當前索引等於中間件數組的長度, // 說明已經中間件執行完畢, // fn 爲 fnMiddleware(ctx) 時沒有傳入的第二個參數, // 即 fn = undefined if (i === middleware.length) fn = next // fn 爲 undefined, 返回一個已經 reolved 的 promise if (!fn) return Promise.resolve() try { // 執行中間件函數並將 dispatch 做爲 next 函數傳入 return Promise.resolve(fn(context, dispatch.bind(null, i + 1))); } catch (err) { return Promise.reject(err) } } } }
如今來捋一下 fnMiddleware
的執行流程:
// fnMiddleware 接收兩個參數 function (context, next) { // .... } // 將 context 傳入,並無傳入 next, // 因此第一次執行時是沒有傳入 next 的 fnMiddleware(ctx).then(handleResponse).catch(onerror);
next == undefined
時會結束中間件執行,流程以下:
function dispatch (i) { //... // 經過傳入的索引從數組中獲取中間件, // 可是由於已經執行完了全部中間件, // 因此當前 i 已經等於數組長度, // 即 fn = undefined let fn = middleware[i] // 若是當前索引等於中間件數組的長度, // 說明已經中間件執行完畢, // 又由於 fnMiddleware(ctx) 時沒有傳入的第二個參數 next, // 因此 fn = undefined if (i === middleware.length) fn = next // fn 爲 undefined, 返回一個已經 reolved 的 promise // 中間件執行流程結束 if (!fn) return Promise.resolve() // ... }
上面先說告終束流程,如今說一下如何順序執行,造成洋蔥模型:
function dispatch (i) { // ...省略其餘代碼 try { // 分步驟說明 // 首先經過 bind 將 dispatch 構建爲 next 函數 const next = dispatch.bind(null, i + 1); // 將 ctx, next 傳入執行當前中間件, // 當在中間件中調用 next() 時, // 本質上是調用 diapatch(i + 1), // 也就是從數組中獲取下一個中間件進行執行, // 在這時,會中斷當前中間件的執行流程轉去執行下一個中間件, // 只有當下一箇中間件執行完畢,纔會恢復當前中間件的執行 const result = fn(context, next); // 中間件執行完畢,返回已經 resolve 的 promise, // 那麼上一個中間件接着執行剩下的流程, // 這樣就造成了洋蔥模型 return Promise.resolve(result); } catch (err) { return Promise.reject(err) } }
開頭的例子執行結果以下:
const one = (ctx, next) => { console.log('1-Start'); next(); ctx.body = { text: 'one' }; console.log('1-End'); } const two = (ctx, next) => { console.log('2-Start'); next(); ctx.body = { text: 'two' }; console.log('2-End'); } const three = (ctx, next) => { console.log('3-Start'); ctx.body = { text: 'three' }; next(); console.log('3-End'); } // 1-Start // 2-Start // 3-Start // 3-End // 2-End // 1-End // 而 ctx.body 最終爲 { text: 'one' }
// 沒有調用 next() 函數 app.use((ctx, next) => { console.log('Start'); ctx.body = { text: 'test' }; console.log('End'); });
由於 next 函數本質上就是經過dispatch(i + 1)
來調用下一個中間件,若是沒有調用 next 函數,就沒法執行下一個中間件,那麼就表明當前中間件流程執行結束。
app.use((ctx, next) => { console.log('Start'); ctx.body = { text: 'test' }; // 屢次調用 next 函數 next(); // 本質上是 dispatch(i + 1) next(); // 本質上是 dispatch(i + 1) console.log('End'); });
這裏假設 next
爲 dispatch(3)
,那麼 index
就爲 2,第一次執行 next 函數時,會發生以下邏輯:
// index == 2 // i == 3 // 不會報錯 if (i <= index) return Promise.reject(new Error('next() called multiple times')) // 賦值後 index 爲 3 了 index = i
假設第三個中間件是最後一箇中間件,那麼執行完第一次 next 函數會當即執行第二個 next 函數,依然執行這個邏輯,可是 index 已經爲 3 了,因此會致使報錯:
// index == 3 // i == 3 // 報錯 if (i <= index) return Promise.reject(new Error('next() called multiple times')) index = i