中间件

Redux 中间件详解

Redux Middleware 的重要职责之一就是对于 dispatch 的封装。我们在服务端开发使用的 Express 或者 Koa 这些框架中经常会使用所谓的中间件(Middleware),此处的中间件指那些在接收到请求之后、进行响应之前所执行的代码。而 Redux 的中间件的概念则很类似于 Express 或者 Koa 中,其为第三方扩展提供了有效的切入点,使得开发者能够方便地在 Action 到达 Reducer 之前被进行适当的处理,并且在状态更新之后能够根据最新的状态再次进行相应的操作。

Redux 中间件示意

譬如我们应用中常用的日志功能,需要记录所有的 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

Logger for Redux

或者使用自定义的配置:

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”(取决于一个简单的对象,并返回一个动作数组)、调和变得微不足道,在启动计时器之前在商店中加载状态,这可能就是全部!

缺点在于这是最复杂的方法、以特定的时间间隔调度操作可能会比较棘手。

上一页