start 基於 koa 2.11 按以下流程分析: app.use() use 方法定義在 中: this.middleware 這就是一個數組,用來存放所有中間件,然後按順序執行。 app.listen() 這個方法定義在 中: this.callback() this.handleReques ...
start
基於 koa 2.11 按以下流程分析:
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);
app.use()
use 方法定義在 koa/lib/application.js
中:
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
這就是一個數組,用來存放所有中間件,然後按順序執行。
this.middleware = [];
app.listen()
這個方法定義在 koa/lib/application.js
中:
listen(...args) {
debug('listen');
// 創建 http 服務並監聽
const server = http.createServer(this.callback());
return server.listen(...args);
}
this.callback()
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.handleRequest
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
方法將響應發出。
compose(this.middleware)
compose 函數通過koa-compose
引入:
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()
沒有調用 next()
// 沒有調用 next() 函數
app.use((ctx, next) => {
console.log('Start');
ctx.body = { text: 'test' };
console.log('End');
});
因為 next 函數本質上就是通過dispatch(i + 1)
來調用下一個中間件,如果沒有調用 next 函數,就無法執行下一個中間件,那麼就代表當前中間件流程執行結束。
多次調用 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