理解 Redux 的中间件
理解 Redux 的中间件
将该思想抽象出来,其实和 Redux 就无关了。问题变成,怎样实现在截获函数的执行,以在其执行前后添加自己的逻辑。
为了演示,我们准备如下的示例代码来模拟 Redux dispatch action 的场景:
const store = {
dispatch: action => {
console.log("dispating action:", action);
}
};
store.dispatch({ type: "FOO" });
store.dispatch({ type: "BAR" });
我们最终需要实现的效果是 Redux 中 applyMiddleware(...middlewares)
的效果,接收一个中间件数据(函数数组),执行真正的 dispatch 前顺次执行这些中间件。
以打日志为例,我们想在调用 dispatch 时进行日志输出。
尝试1 - 手动
直接的做法就是手动进行。
console.log("before dispatch `FOO`");
store.dispatch({ type: "FOO" });
console.log("before dispatch `FOO`");
console.log("before dispatch `BAR`");
store.dispatch({ type: "BAR" });
console.log("before dispatch `BAR`");
但其实这并不算一个系统的解决方案,至少需要摆脱手动这种方式。
尝试2 - 包装
既然所有 dispatch 操作都会打日志,完全有理由抽取一个方法,将 dispatch 进行包装,在这个方法里来做这些事情。
function dispatchWithLog(action) {
console.log(`before dispatch ${action.type}`);
store.dispatch(action);
console.log(`after dispatch ${action.type}`);
}
但调用的地方也得变,不能直接使用原始的 store.disatch
而需要使用封装后的 dispatchWithLog
:
- store.dispatch({ type: "FOO" });
- store.dispatch({ type: "BAR" });
+ dispatchWithLog({ type: "FOO" });
+ dispatchWithLog({ type: "BAR" });
尝试3 - 替换实现/Monkeypatching
如果我们直接替换掉原始函数的实现,便可以做到调用的地方不受影响而实现新增的 log 功能,虽然修改别人提供的方法容易引起 bug 且不太科学。
const original = store.dispatch;
store.dispatch = function log(action) {
console.log(`before dispatch ${action.type}`);
original(action);
console.log(`after dispatch ${action.type}`);
};
store.dispatch({ type: "FOO" });
store.dispatch({ type: "BAR" });
尝试4 - 多个函数的截获
除了添加 log,如果还想对每次 dispatch 进行错误监控,只需要拿到前面已经替换过实现的 dispatch 方法再次进行替换包装即可。
const original = store.dispatch;
store.dispatch = function log(action) {
console.log(`before dispatch ${action.type}`);
original(action);
console.log(`after dispatch ${action.type}`);
};
const next = store.dispatch;
store.dispatch = function report(action) {
console.log("report middleware");
try {
next(action);
} catch (error) {
console.log(`error while dispatching ${action.type}`);
}
};
所以针对单个功能的中间件,我们可以提取出其大概的样子来了:
function middleware(store) {
const next = store.dispatch;
store.dispatch = function(action) {
// 中间件中其他逻辑
next(action);
// 中间件中其他逻辑
};
}
改写日志和错误监控为如下:
function log(store) {
const next = store.dispatch;
store.dispatch = function(action) {
console.log(`before dispatch ${action.type}`);
next(action);
console.log(`after dispatch ${action.type}`);
};
}
function report(store) {
const next = store.dispatch;
store.dispatch = function(action) {
console.log("report middleware");
try {
next(action);
} catch (error) {
console.log(`error while dispatching ${action.type}`);
}
};
}
然后按需要应用上述中间件即可:
log(store);
report(store);
上面中间件的调用可专门编写一个方法来做:
function applyMiddlewares(store, middlewares) {
middlewares.forEach(middleware => middleware(store));
}
隐藏 Monkeypatching
真实场景下,各中间件由三方编写,如果每个中间件都直接去篡改 store.dispatch
不太科学也不安全。如此的话,中间件只需要关注新添加的逻辑,将新的 dispatch 返回即可,由框架层面拿到这些中间件后逐个调用并重写原来的 dispatch
,将篡改的操作收敛。
所以中间件的模式更新成如下:
function middleware(store) {
const next = store.dispatch;
- store.dispatch = function(action) {
+ return function(action) {
// 中间件中其他逻辑
next(action);
// 中间件中其他逻辑
};
}
改写 log
和 report
中间件:
function log(store) {
const next = store.dispatch;
- store.dispatch = function(action) {
+ return function(action) {
console.log(`before dispatch ${action.type}`);
next(action);
console.log(`after dispatch ${action.type}`);
};
}
function report(store) {
const next = store.dispatch;
- store.dispatch = function(action) {
+ return function(action) {
console.log("report middleware");
try {
next(action);
} catch (error) {
console.log(`error while dispatching ${action.type}`);
}
};
}
更新 applyMiddlewares
方法:
function applyMiddlewares(store, middlewares) {
middlewares.forEach(middleware => {
store.dispatch = middleware(store);
});
}
最后,应用中间件:
applyMiddlewares(store, [log, report]);
进一步优化
之所以在应用中间件过程中每次都重新给 store.dispatch
赋值,是想让后续中间件在通过 store.dispatch
访问时,能够拿到前面中间件修改过的 dispatch
函数。
如果中间件中不是直接从 store
身上去获取 store.dispatch
,而是前面已经执行过的中间件将新的 dispatch
传递给中间件,则可以避免每次对 store.dispatch
的赋值。
function applyMiddlewares(store, middlewares) {
store.dispatch = middlewares.reduce(
(next, middleware) => middleware(next),
store.dispatch
);
}
忽略掉实际源码中的一些差异,以上,大致就是 Redux 中间件的创建和应用了。
测试
function m1(next) {
return function(action) {
console.log(`1 start`);
next(action);
console.log(`1 end`);
};
}
function m2(next) {
return function(action) {
console.log(`2 start`);
next(action);
console.log(`2 end`);
};
}
function m3(next) {
return function(action) {
console.log(`3 start`);
next(action);
console.log(`3 end`);
};
applyMiddlewares(store, [m1, m2, m3]);
store.dispatch({ type: "FOO" });
store.dispatch({ type: "BAR" });
}
输出结果:
3 start
2 start
1 start
dispating action: { type: 'FOO' }
1 end
2 end
3 end
3 start
2 start
1 start
dispating action: { type: 'BAR' }
1 end
2 end
3 end