中间件
Redux 中间件详解
Redux Middleware 的重要职责之一就是对于 dispatch 的封装。我们在服务端开发使用的 Express 或者 Koa 这些框架中经常会使用所谓的中间件(Middleware),此处的中间件指那些在接收到请求之后、进行响应之前所执行的代码。而 Redux 的中间件的概念则很类似于 Express 或者 Koa 中,其为第三方扩展提供了有效的切入点,使得开发者能够方便地在 Action 到达 Reducer 之前被进行适当的处理,并且在状态更新之后能够根据最新的状态再次进行相应的操作。
譬如我们应用中常用的日志功能,需要记录所有的 Action 以及相应的状态变化,我们可以自定义如下的日志中间件:
const logger = (store) => (next) => (action) => {
console.log("dispatching", action);
let result = next(action);
console.log("next state", store.getState());
return result;
};
然后在创建 Store 时,将中间件放入到 createStore
的第二个参数中:
import { createStore, combineReducers, applyMiddleware } from "redux";
let todoApp = combineReducers(reducers);
let store = createStore(
todoApp,
// applyMiddleware() tells createStore() how to handle middleware
applyMiddleware(logger)
);
这样中间件就能正常工作了,其分别在分发 Action 与 状态更新后进行相应的触发操作。
调用流程
Redux 的中间件调用流程也是所谓的洋葱圈模型,即先从外至内,再由内而外的过程。譬如我们定义 3 个中间件并且依次添加到 Store 中:
function middleware1(store) {
return function (next) {
return function (action) {
console.log("A middleware1 开始");
next(action);
console.log("B middleware1 结束");
};
};
}
function middleware2(store) {
return function (next) {
return function (action) {
console.log("C middleware2 开始");
next(action);
console.log("D middleware2 结束");
};
};
}
function middleware3(store) {
return function (next) {
return function (action) {
console.log("E middleware3 开始");
next(action);
console.log("F middleware3 结束");
};
};
}
function reducer(state, action) {
if (action.type === "MIDDLEWARE_TEST") {
console.log("======= G =======");
}
return {};
}
const store = Redux.createStore(
reducer,
Redux.applyMiddleware(middleware1, middleware2, middleware3)
);
store.dispatch({ type: "MIDDLEWARE_TEST" });
最后的控制台输出为:
A middleware1 开始
C middleware2 开始
E middleware3 开始
======= G =======
F middleware3 结束
D middleware2 结束
B middleware1 结束
整个请求的示意图如下:
--------------------------------------
| middleware1 |
| ---------------------------- |
| | middleware2 | |
| | ------------------- | |
| | | middleware3 | | |
| | | | | |
next next next ——————————— | | |
dispatch —————————————> | reducer | — 收尾工作->|
nextState <————————————— | G | | | |
| A | C | E ——————————— F | D | B |
| | | | | |
| | ------------------- | |
| ---------------------------- |
--------------------------------------
顺序 A -> C -> E -> G -> F -> D -> B
\---------------/ \----------/
↓ ↓
更新 state 完毕 收尾工作
Log Middleware
import { applyMiddleware, createStore } from "redux";
// Logger with default options
import logger from "redux-logger";
const store = createStore(reducer, applyMiddleware(logger));
// Note passing middleware as the third argument requires redux@>=3.1.0
或者使用自定义的配置:
import { applyMiddleware, createStore } from "redux";
import { createLogger } from "redux-logger";
const logger = createLogger({
// ...options
});
const store = createStore(reducer, applyMiddleware(logger));
源代码
Redux 中间件的源码也相对简单:
export default function applyMiddleware<Ext, S = any>(
...middlewares: Middleware<any, S, any>[]
): StoreEnhancer<{ dispatch: Ext }>;
export default function applyMiddleware(
...middlewares: Middleware[]
): StoreEnhancer<any> {
return (createStore: StoreCreator) => <S, A extends AnyAction>(
reducer: Reducer<S, A>,
...args: any[]
) => {
const store = createStore(reducer, ...args);
let dispatch: Dispatch = () => {
throw new Error(
"Dispatching while constructing your middleware is not allowed. " +
"Other middleware would not be applied to this dispatch."
);
};
const middlewareAPI: MiddlewareAPI = {
getState: store.getState,
dispatch: (action, ...args) => dispatch(action, ...args),
};
const chain = middlewares.map((middleware) => middleware(middlewareAPI));
// chain 中的函数,(next) => (action) => {}
// compose 的结果就是 m1(m2(dispatch)),dispatch 作为最后一个中间件的 next 函数
// m1 的返回结果就是 (action)=>void
dispatch = compose<typeof dispatch>(...chain)(store.dispatch);
return {
...store,
dispatch,
};
};
}
这里的 compose 函数定义如下:
// export default function compose<R>(...funcs: Function[]): (...args: any[]) => R
export default function compose(...funcs: Function[]) {
if (funcs.length === 0) {
// infer the argument type so it is usable in inference down the line
return <T>(arg: T) => arg;
}
if (funcs.length === 1) {
return funcs[0];
}
// 这里的 b(...args) 相当于 next
return funcs.reduce((a, b) => (...args: any) => a(b(...args)));
}
Redux 中的定时器
组件内定时器
没有什么可以阻止您在组件内部使用计时器的。您可以在 componentDidMount 上启动计时器,也可以在组件控件上启动某个事件,然后在 componentWillUnmount 中停止计时器,也可以在其他事件上启动计时器。例如:
export default class Loading extends Component {
state = {
timer: null,
counter: 0
};
componentDidMount() {
let timer = setInterval(this.tick, 1000);
this.setState({timer});
}
componentWillUnmount() {
this.clearInterval(this.state.timer);
}
tick() {
this.setState({
counter: this.state.counter + 1
});
}
render() {
<div>Loading{"...".substr(0, this.state.counter % 3 + 1)}</div>
}
这种方式优势在于简单直接,组件可以是独立的。缺陷在于状态是内部的,因此很难与其他组件共享、状态是内部的,这使其更难以存储和调和、特定于 redux,但这不会产生动作,也不会存储在中央存储中。
Timers in Actions
另一种选择是在调度动作时触发计时器。使用 redux-thunk 的一个示例如下所示。
let timer = null;
const start = () => (dispatch) => {
clearInterval(timer);
timer = setInterval(() => dispatch(tick()), 1000);
dispatch({ type: TIMER_START });
dispatch(tick());
};
const tick = () => {
type: TIMER_TICK;
};
const stop = () => {
clearInterval(timer);
return { type: TIMER_STOP };
};
这种方式的优势在于您可以跟踪与计时器相关的动作和状态突变、您可以在整个应用程序中共享计时器状态。缺陷在于关闭/打开应用程序时,您仍将不得不处理“重新启动”计时器、与先前的示例相比,它增加了代码的复杂性。如果您的应用程序已经在使用 redux,那么可能还不错。
Global timer
当您的应用程序加载“监视”需要触发进一步操作的应用程序状态时,可以在代码中的某个位置启动全局计时器。例如:
//somewhere when you app starts
setInterval(() => {
let actions = calculate_pending_actions(store.getState());
actions.forEach(dispatch);
}, 50); //something short
function calculate_pending_actions(state) {
let { timer } = state;
let actions = [];
// put all your conditions here...
if (timer.started && new Date().getTime() - timer.startedOn > 60 * 1000) {
actions.push(stop());
}
// etc...
return actions;
}
到目前为止,最后一个选项是最复杂的,并且与游戏的构建方式有些相似。这种方式优点在于无需处理启动/清除计时器,只有一个计时器一直在运行。、整个应用程序的所有计时器逻辑都放在一个地方、这与“反应性”库(如 mobX 或 rxjs)配合使用、易于测试“ calculate_pending_actions”(取决于一个简单的对象,并返回一个动作数组)、调和变得微不足道,在启动计时器之前在商店中加载状态,这可能就是全部!
缺点在于这是最复杂的方法、以特定的时间间隔调度操作可能会比较棘手。