常用中間件原理淺析
Koa2 中間件原理
/**
* Compose `middleware` returning
* a fully valid middleware comprised
* of all those which are passed.
*
* @param {Array} middleware
* @return {Function}
* @api public
*/
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!');
}
/**
* @param {Object} context
* @return {Promise}
* @api public
*/
return function (context, next) {
// 記錄上一次執行中間件的位置
let index = -1;
return dispatch(0);
function dispatch (i) {
// 理論上 i 會大於 index,因爲每次執行一次都會把 i 遞增
// 如果 小於或等於,則說明 next() 執行了多次
// 比如在第一個中間件中 next() 執行兩次,此時,兩次 dispatch 接收的 i 值都是 2,第一次執行便會 將 2 賦值給 index,第二次執行則會命中 等於 判斷
if (i <= index) return Promise.reject(new Error('next() called multiple times'));
index = i;
// 獲取當前的中間件
let fn = middleware[i];
// 最後一箇中間件中執行 next()
if (i === middleware.length) fn = next;
if (!fn) return Promise.resolve();
try {
return Promise.resolve(fn(context, function next () {
return dispatch(i + 1);
}));
} catch (err) {
return Promise.reject(err);
}
}
}
}
- Koa 中間件串聯調用的核心邏輯在於
dispatch
函數中,每一箇中間件都可以拿到下一個中間件的執行手柄next
- 每個中間件只能執行一次
next
,執行多次會出現上面代碼中所說的問題 - 洋蔥圈模型如何實現 - 關鍵在
async await
的執行機制 - 中間件的基本結構如下
async function commonMiddleware(ctx, next){
try{
// do something
await next()
// do something
}
.catch(err){
// handle err
}
}
Redux 中間件原理
先思考一個問題
如何將多個 平級函數
轉化爲從右到左的 聚合函數
比如
let fn = compose(fn1,fn2,fn3); // 輸出 (...args) => fn1(fn2(fn3(...args)))
// compose聚合函數的順序是從右到左
const compose = function (...funcs) {
return funcs.reduce((fn1, fn2) => {
return (...args) => {
return fn1(fn2(...args));
};
});
}
看一下 Redux 中間件核心處理鏈式調用的函數
function applyMiddleware(...middlewares) {
return (createStore) => (...args) => {
const store = createStore(...args);
let dispatch = () => {
throw new Error(
'Dispatching while constructing your middleware is not allowed. ' +
'Other middleware would not be applied to this dispatch.'
);
};
const middlewareAPI = {
getState: store.getState,
dispatch: (...args) => {
return dispatch(...args);
},
};
// 這裏執行了一層中間件接收了{store.getState,dispatch}參數
const chain = middlewares.map(middleware => middleware(middlewareAPI));
// compose(...chain)(store.dispatch) 相當於fn1(fn2(fn3(store.dispatch)))
// 又執行了一層中間件 這一層接收 next 參數 也就是下一個中間件參數
dispatch = compose(...chain)(store.dispatch);
return { ...store, dispatch };
};
}
由以上中間件的處理方式可以看出,一箇中間件的基本結構如下
// 中間件邏輯代碼需要經過三次柯里化
store => next => action => {
// 中間件邏輯代碼
// do something
next(action);
// do something
}
Action
派發後走同步修改操作,唯一有發揮空間的環節在dispatch
環節- Redux 中間件是對
dispatch
的增強(攔截處理),使其支持函數、Promise
的處理
基於 Promise 鏈表實現中間件模塊
以上兩種方式都是可以在中間件內部手動調用
next
執行下一個中間件
有時候需要中間件列表是一個自動化的流水線,無需額外控制,自動全鏈路執行
比如下面的場景,需要中間件模塊順序執行一下
- 數據格式化 -> 數據編碼 -> 數據解析
- 數據格式化 -> UI組件解析器
const promiseMiddleware = (middlewares: any[], ctx: any) => {
let promise = Promise.resolve(null);
let next;
// 1. 通過bind把執行上下文對象,綁定到中間件第一個參數
middlewares.forEach((fn, i) => {
middlewares[i] = fn.bind(null, ctx);
});
// 2. 通過while循環執行promise實例
while ((next = middlewares.shift())) {
promise = promise.then(next);
}
// 3. 最終返回一個promise實例結果
return promise.then(() => {
return ctx;
});
}
統一中間件的調用流程
class MiddleWare {
middlewares = [];
ctx = {
message: {}
}
// 1. 構造器函數,初始化添加 middlewares
constructor(middlewares) {
super();
this.middlewares = middlewares;
}
// 2. 通過批量添加中間件接口
useBatch(steps) {
if (Array.isArray(steps)) {
this.middlewares = this.middlewares.concat(steps);
} else {
throw TypeError('useBatch args must be an arrary!!!')
}
}
// 3. 核心實現,每個Action都需要進過Dispatch進行觸發
dispatch(message) {
// 3.1 使用Object.create 創建新的 middlewares 和 ctx對象,防止對象引用
let steps = Object.create(this.middlewares);
let ctx = Object.create(this.ctx);
// 3.2 賦值 消息
ctx.message = message;
// 3.3 執行中間件模塊,同時返回一個 promise 實例
return promiseMiddleware(steps, ctx);
}
}
基於 Promise 鏈式調用的還有 axios 的攔截器處理邏輯
總結
- 中間件的編寫邏輯需要結合業務進行取捨
- 核心邏輯在於如何處理中間件的鏈式調用(手動式、自動式)
- 對中間件列表加以封裝,對外提供統一的API調用
參考文章: