React 小册 | Redux 中间件 👾

3,990 阅读5分钟

本文参考如下文档

redux-thunk

redux-sage

remach

本文代码仓库 github.com/rick-chou/o…

时光机 👾

中间件

上文中 我们介绍了 Redux 的基本用法 但是我们在 Reducer 中都是同步代码

承接上文计数器的 🌰 如果我们想在 Reducer 中将用定时器将每个操作延迟 1s

代码如下

// 创建Reducer 用于管理 View 派发过来的 Action
const reducer = (store = initStore, action: IAction) => {
  switch (action.type) {
    case ACTION_TYPE.ADD_COUNTER: {
      let { count } = store;
      setTimeout(() => {
        count = count + action.payload;
      });
      return { ...store, count };
    }
    case ACTION_TYPE.SUB_COUNTER:
      return { ...store, count: store.count - action.payload };
    default:
      return store;
  }
};

我们用定时器来模拟 真实场景下的一些 api 操作

上文中 我们提到 Redux 有三大原则 它们都只为做一件事

让 Redux 的每一步操作都变得可预测

但是我们无法确定 我们在 Reducer 中书写的异步操作会在什么时候触发 从而改变 Store

这将会是 Redux 的 Store 变得难以预测

我们再来回顾一下 Redux 中的数据流

UI —————> action(plain)—————> reducer —————> state —————> UI

其实 action 是一个原始 js 对象(plain object)且 reducer 是一个纯函数

所以 如果想要实现异步操作 这些具有副作用的行为时

就需要在 action 和 reducer 中间再架设一层处理异步逻辑的中间层

这就是中间件 加入中间件后 Redux 数据流如下

UI —————> action(side function) —————> middleware —————> action(plain) —————> reducer —————> state —————> UI

既然 知道了数据流 我们就可以手动来实现一个中间层

diy middleware

我们可以 dispatch(中间件函数) 然后在中间件函数中处理数据 最后 return 一个干净的纯函数

举个 🌰 以下是我新建的一个 middleware.ts

import { addCounter } from './store';
import { wait } from '@/utils';

export const addCounterAsync = (payload: number) => {
  wait(1000);
  return addCounter(payload);
};

wait 是我写的一个工具函数 用来暂停程序 模拟异步请求

const wait = (delay: number) => {
  var start = new Date().getTime();
  while (new Date().getTime() - start < delay) {
    continue;
  }
};

然后在组件中 我们就可以用 addCounterAsync 代替 addCounter 即可

<Button onClick={() => dispatch(addCounterAsync(payload))}>+</Button>

当然 社区上早已有了很多处理异步 action 的库

下面主要介绍两种中间件 redux-thunkredux-saga 然后安利一下rematch

redux-thunk

改造后的代码如下 省去无关代码

// 导入核心API 创建Store
import { createStore, applyMiddleware, compose } from 'redux';
import thunkMiddleware from 'redux-thunk';

export const _subCounter = (payload: number) => ({
  type: ACTION_TYPE.SUB_COUNTER,
  payload,
});

export const subCounter = (payload: number) => {
  return (dispatch: any, getState: any) =>
    setTimeout(() => {
      let { count } = getState();
      count = count + payload;
      dispatch(_subCounter(count));
    }, 1000);
};

// 创建一个初始化的Store
const initStore: IStore = {
  count: 0,
};

// 创建Reducer 用于管理 View 派发过来的 Action
const reducer = (store = initStore, action: IAction) => {
  switch (action.type) {
    case ACTION_TYPE.SUB_COUNTER:
      return { ...store, count: action.payload };
    default:
      return store;
  }
};

const composeEnhancers =
  (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;

const enhancer = composeEnhancers(applyMiddleware(thunkMiddleware));

// 创建 Store 这里我们还开启了 Redux DEVTools
export const store = createStore(reducer, enhancer);

其他地方正常使用即可

通过 thunk 我们可以在 dispatch 前拦截 action

然后进行网络请求等异步操作 最后让 action 带上我们需要的数据 变成原始的 action(plain object) 再 dispatch 出去

但是这种方法的缺点也很明显 就是将异步请求的操作和 actionCreator 杂糅在了一起

对于每一个需要异步操作的 action 我们都需要在 actionCreator 中写一个对应的函数 这样会使 actionCreator 变的越来越大 不易于管理

所以对应的 又有一种中间件 saga

redux-saga

在 saga 中 side effect 都移到了 saga.js 文件中 不再和 actionCreator 杂糅在一起

saga 的体系庞大完整 但是也相对复杂 还是用一个计数器的 🌰 来康康 saga 的使用

首先是我们的 store 文件

import { createStore, applyMiddleware, compose } from 'redux';
import createSagaMiddleware from 'redux-saga';
import mySaga from './saga';

export interface IStore {
  count: number;
}

export interface IAction {
  type: string;
  [key: string]: any;
}

// 定义我们的 Action Type
export enum ACTION_TYPE {
  ADD_COUNTER = 'ADD_COUNTER',
  SUB_COUNTER = 'SUB_COUNTER',
  ADD_COUNTER_ASYNC = 'ADD_COUNTER_ASYNC',
  SUB_COUNTER_ASYNC = 'SUB_COUNTER_ASYNC',
}

// 注意这边的type类型 和saga文件中takeEvery函数中的类型一致
export const increment = (payload: number) => ({
  type: ACTION_TYPE.ADD_COUNTER_ASYNC,
  payload,
});

export const decrement = (payload: number) => ({
  type: ACTION_TYPE.SUB_COUNTER_ASYNC,
  payload,
});

// 创建一个初始化的Store
const initStore: IStore = {
  count: 0,
};

const reducer = (store = initStore, action: IAction) => {
  switch (action.type) {
    case ACTION_TYPE.ADD_COUNTER:
      return { ...store, count: store.count + action.payload };
    case ACTION_TYPE.SUB_COUNTER:
      return { ...store, count: store.count - action.payload };
    default:
      return store;
  }
};

// 启用redux devtools
const composeEnhancers =
  (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;

// 创建saga中间件对象
const sagaMiddleware = createSagaMiddleware();

// 注册中间件
const enhancer = composeEnhancers(applyMiddleware(sagaMiddleware));

// 创建 Store 这里我们还开启了 Redux DEVTools
export const store = createStore(reducer, enhancer);

// 必须放在createStore后 再启动saga
sagaMiddleware.run(mySaga);

然后是 saga 文件

import { takeEvery, put, delay } from 'redux-saga/effects';
import { ACTION_TYPE } from './store';

// 执行副作用的函数
function* increment(action: any) {
  // 延迟一秒 模拟异步 可以在这里用try catch语法块返回成功/失败的action
  yield delay(1000);
  yield put({
    type: ACTION_TYPE.ADD_COUNTER,
    payload: action.payload,
  });
}

function* decrement(action: any) {
  yield delay(1000);
  yield put({
    type: ACTION_TYPE.SUB_COUNTER,
    payload: action.payload,
  });
}

function* mySaga() {
  // 组件中发起action的type应该和这里的一致
  yield takeEvery(ACTION_TYPE.ADD_COUNTER_ASYNC, increment);
  yield takeEvery(ACTION_TYPE.SUB_COUNTER_ASYNC, decrement);
}

export default mySaga;

rematch

关于 rematch 的介绍 可以参考传送门

rematch 可以简化传统的 redux 使我们免去写 actionType actionCreator

我们用 rematch 来实现计数器的 🌰

首先是 store

const count = {
  state: {
    count: 0,
  },
  reducers: {
    setCount(state, count) {
      return { ...state, count };
    },
  },
  // 可以在这里处理副作用 例如发起网络请求
  effects: (dispatch) => ({
    async increment(payload, rootState) {
      let { count } = rootState.count;
      count += payload;
      dispatch.count.setCount(count);
    },
  }),
};

export default count;

如果我们有多个 store 我们就可以在一个文件中统一管理我们的 store

export { default as count } from './count';
export { default as total } from './total';
export ...

init 函数会帮助我们创建一个 store 同时它会帮助我们自动开启 redux devtools 调试工具

import { init } from '@rematch/core';
import * as models from './model';

export const store = init({ models });

业务组件

import React from 'react';
import { Provider, useSelector, useDispatch } from 'react-redux';
import { store } from './models';

const Counter = () => {
  const counter = useSelector((state) => state.count);
  const dispatch = useDispatch();
  return (
    <>
      <div>{counter.num}</div>
      <button onClick={() => dispatch.count.increment(1)}>+1</button>
      <button onClick={() => dispatch.count.increment(10)}>+10</button>
    </>
  );
};

export default function View() {
  return (
    <Provider store={store}>
      <Counter />
    </Provider>
  );
}