陌上人如玉
公子世无双

3.8 Redux 中间件

3.8 Redux 中间件

一句话:中间件是「在 dispatch 与 reducer 之间插入的一层」,用来处理「非普通对象」的 action(如函数、Promise)或加入横切逻辑(日志、埋点、持久化);它通过柯里化的函数签名 store => next => action => {} 串联成链。

一、为什么需要中间件

// Redux 的 dispatch 默认只接受「普通对象」action
store.dispatch({ type: 'todos/add', payload: todo });   // ✅
store.dispatch(() => {});                                // ❌ 报错
store.dispatch(Promise.resolve());                       // ❌ 报错

// 报错信息:
// Actions must be plain objects. Use custom middleware for async actions.
问题:异步操作怎么办?

❌ 组件里直接写异步(问题多):
   useEffect(() => {
     api.getTodos().then((data) => store.dispatch({ type: 'todos/set', payload: data }));
   }, []);
   → 逻辑散落在组件里;多个组件要重复写;无法复用;难以测试

✅ 用中间件把「异步逻辑」从组件里抽出来:
   dispatch(fetchTodos())        ← 派发一个「函数」
   → thunk 中间件拦截 → 执行这个函数(在里面做请求与 dispatch)

中间件解决的其它问题:
   · 日志:记录每次 dispatch 的 action 与前后状态
   · 埋点:所有 action 自动上报
   · 持久化:状态变化时写入 localStorage
   · 崩溃上报:捕获 action 处理中的错误

二、中间件原理

// 一个中间件的标准签名(三层柯里化)
const myMiddleware = (store) => (next) => (action) => {
  // ① store:{ getState, dispatch }(注意 dispatch 是【被增强过的】)
  // ② next:调用链中的下一个中间件(或最终的 store.dispatch)
  // ③ action:本次派发的 action

  console.log('before', action);

  const result = next(action);          // ⭐ 交给下一个中间件 / reducer

  console.log('after', store.getState());

  return result;                        // ⭐ 必须返回(调用方可能需要)
};
为什么要柯里化三层?

① (store) => ...     —— Redux 创建 store 时把 store 传进来(只需一次)
② (next) => ...      —— applyMiddleware 在「组装中间件链」时把 next 传进来(只需一次)
③ (action) => ...    —— 每次 dispatch 时执行

好处:
· ① 和 ② 只在初始化时执行一次(可以在这里做「初始化工作」)
· ③ 是「每次调用都执行」的热路径
· next 的引用在链组装后就固定了 → 每次 dispatch 只是沿着链传递
// 简化版 applyMiddleware(理解「链」是怎么组装的)
function applyMiddleware(...middlewares) {
  return (createStore) => (reducer, preloadedState) => {
    const store = createStore(reducer, preloadedState);

    // ⭐ dispatch 初始为「只能抛错的占位」(因为中间件里不能直接用它)
    let dispatch = () => {
      throw new Error('Dispatching while constructing your middleware is not allowed.');
    };

    const middlewareAPI = {
      getState: store.getState,
      dispatch: (action, ...args) => dispatch(action, ...args),   // 延迟绑定
    };

    // 每个中间件先「吃」掉 store(第一层)
    const chain = middlewares.map((m) => m(middlewareAPI));

    // ⭐ 从右到左组合,得到最终增强的 dispatch
    dispatch = compose(...chain)(store.dispatch);

    return { ...store, dispatch };
  };
}

function compose(...fns) {
  if (fns.length === 0) return (arg) => arg;
  if (fns.length === 1) return fns[0];
  return fns.reduce((a, b) => (...args) => a(b(...args)));
}

三、手写几个中间件

// ① 日志中间件
const logger = (store) => (next) => (action) => {
  console.group(action.type);
  console.log('dispatching:', action);
  console.log('prev state:', store.getState());

  const result = next(action);

  console.log('next state:', store.getState());
  console.groupEnd();

  return result;
};

// ② 崩溃上报中间件(捕获 action 处理中的异常)
const crashReporter = (store) => (next) => (action) => {
  try {
    return next(action);
  } catch (err) {
    console.error('Caught an exception!', err);
    reportError(err, { action, state: store.getState() });
    throw err;                       // 重新抛出,不吞掉错误
  }
};

// ③ 埋点中间件(只上报感兴趣的 action)
const analytics = (store) => (next) => (action) => {
  if (typeof action === 'object' && action.meta?.track) {
    track(action.type, action.payload);
  }
  return next(action);
};

// ④ 持久化中间件(防抖写入)
const persistence = (store) => {
  let timer;
  return (next) => (action) => {
    const result = next(action);

    clearTimeout(timer);
    timer = setTimeout(() => {
      const state = store.getState();
      localStorage.setItem('app-state', JSON.stringify({ cart: state.cart }));
    }, 300);

    return result;
  };
};
// ⑤ 组装(applyMiddleware 的顺序 = 执行顺序)
const store = createStore(
  rootReducer,
  applyMiddleware(logger, crashReporter, analytics, persistence)
);

四、thunk:最常用的异步中间件

// thunk 的核心逻辑(非常短)
const thunk = ({ dispatch, getState }) => (next) => (action) => {
  if (typeof action === 'function') {
    // ⭐ 如果 action 是函数,就调用它(把 dispatch 与 getState 传进去)
    return action(dispatch, getState);
  }
  return next(action);               // 否则交给下一个中间件
};
// 用法:action creator 返回一个函数
// ① 基础:条件式 dispatch(先读状态再决定)
const addTodoIfNotExists = (text) => (dispatch, getState) => {
  const { todos } = getState();
  if (todos.some((t) => t.text === text)) return;      // ⭐ 能读状态
  dispatch({ type: 'todos/add', payload: { text } });
};

// ② 异步请求(loading → success / error)
const fetchTodos = (userId) => async (dispatch, getState) => {
  dispatch({ type: 'todos/loading' });

  try {
    const data = await api.getTodos(userId);
    dispatch({ type: 'todos/success', payload: data });
  } catch (err) {
    dispatch({ type: 'todos/error', payload: String(err) });
  }
};

// ③ 按需读取最新状态(避免组件传参)
const saveDraft = () => async (dispatch, getState) => {
  const { editor } = getState();
  await api.saveDraft(editor.content);
  dispatch({ type: 'editor/markSaved' });
};

// ④ 返回 Promise(让组件可以 await 结果)
const submitOrder = () => async (dispatch, getState) => {
  const order = await api.submit(getState().cart);
  dispatch({ type: 'cart/clear' });
  return order;                                       // ⭐ 组件能拿到返回值
};
// 组件里使用
function TodoList() {
  const dispatch = useDispatch();
  const { items, status } = useAppSelector((s) => s.todos);

  useEffect(() => {
    dispatch(fetchTodos('me'));
  }, [dispatch]);

  return status === 'loading' ? <Skeleton /> : <List items={items} />;
}

// 需要拿到结果时
const handleSubmit = async () => {
  const order = await dispatch(submitOrder()).unwrap?.() ?? await dispatch(submitOrder());
  navigate(`/order/${order.id}`);
};

五、thunk 的三态建模

// ① 用「三个 action」表达异步的三个阶段(最传统的写法)
const initialState = { items: [], status: 'idle', error: null };

function todosReducer(state = initialState, action) {
  switch (action.type) {
    case 'todos/loading':
      return { ...state, status: 'loading', error: null };
    case 'todos/success':
      return { ...state, status: 'success', items: action.payload };
    case 'todos/error':
      return { ...state, status: 'error', error: action.payload };
    default:
      return state;
  }
}
⚠️ 注意:不要用「多个布尔值」表达状态
❌ { isLoading: false, isError: false, isSuccess: false, data: null }
   → 会产生「同时 loading 且 error」这类非法组合

✅ { status: 'idle' | 'loading' | 'success' | 'error', data, error }

用 RTK 时这一套由 createAsyncThunk 自动生成(pending / fulfilled / rejected 三个 action),见 3.9。

六、其它中间件与选型

中间件 用途 特点
redux-thunk 函数式 action(异步、条件 dispatch) 极简单(十几行),RTK 内置
redux-saga 用 generator 编排复杂副作用 能力强(可取消、竞态、并发编排),学习曲线陡
redux-observable 用 RxJS 处理 action 流 适合「事件流」类需求,生态较小
redux-persist 自动持久化 store 配置化,比手写持久化中间件省事
redux-logger 打印 action 与状态变化 开发环境常用(RTK 的 DevTools 已覆盖大部分)
选型建议:
① 大多数项目 → thunk(简单够用,RTK 已内置)
② 需要「取消请求、竞态处理、复杂的并发编排」→ saga 或 RTK Query
③ 只是要「请求 + 缓存」→ 直接用 RTK Query 或 TanStack Query(不需要 saga)

七、中间件的执行顺序

applyMiddleware(logger, crashReporter, analytics)
组装后的调用链(dispatch 时的顺序):

dispatch(action)
  → logger(before)
    → crashReporter(try)
      → analytics(上报)
        → store.dispatch(真正的 reducer 处理)
      ← analytics(after)
    ← crashReporter(catch)
  ← logger(after state)

规则:
① 数组中【靠前的】中间件在「进入时」先执行
② 数组中【靠后的】中间件在「退出时」后执行
③ 因此 logger 放在最前面 → 能看到最完整的信息(包括其它中间件的影响)
④ crashReporter 应该放在【靠前】的位置 → 能捕获后面所有中间件的异常

八、常见坑

// ① 忘记写 return next(action)
const bad = (store) => (next) => (action) => {
  next(action);                       // ❌ 没有 return → 调用方拿不到返回值
};

// ② 在中间件里直接调用 store.dispatch 造成「循环」
const bad2 = (store) => (next) => (action) => {
  store.dispatch({ type: 'something' });   // ⚠️ 会重新走一遍整个链(可能死循环)
  return next(action);
};
// ✅ 需要「再派发」时用 next 或明确的「不拦截」逻辑

// ③ 中间件里「吞掉」action 而不调 next
const swallow = (store) => (next) => (action) => {
  if (action.type === 'blocked') return;   // ⚠️ 这个 action 不会到达 reducer(有时是故意的)
  return next(action);
};

// ④ 在中间件构造阶段(第一、二层)调用 dispatch
const bad3 = (store) => {
  store.dispatch({ type: 'init' });        // ❌ 抛错:Dispatching while constructing
  return (next) => (action) => next(action);
};

// ⑤ 认为 thunk 只能做异步
dispatch((d, g) => { if (g().cond) d({ type: 'x' }); });   // ✅ 条件式同步 dispatch 也常用

// ⑥ thunk 里忘记返回 Promise → 组件无法 await
const noReturn = () => async (dispatch) => { await api.post(); };   // ⚠️ 没有 return
// ✅ 需要结果时显式 return

// ⑦ 用 thunk 处理「服务端数据缓存」→ 手写缓存/失效(应该用 TanStack Query)
// ⑧ 中间件顺序写错(crashReporter 放在最后 → 捕获不到前面的异常)
// ⑨ 在中间件里做「重计算」(每次 dispatch 都执行)
// ⑩ 用 saga 做「只发请求」的活(过度设计,thunk 就够)
// ⑪ 忘记把中间件加进 applyMiddleware → 对应功能静默失效

九、面试延伸

  1. 「Redux 中间件是什么?解决什么问题?」
    中间件是「插在 dispatch 与 reducer 之间的一层逻辑」。它解决两类问题:① 支持非普通对象的 action——Redux 的 dispatch 默认只接受普通对象,异步操作无法直接派发,中间件可以拦截「函数」或「Promise」并自行处理(如 thunk);② 插入横切关注点——日志、埋点、崩溃上报、持久化、状态校验等,这些逻辑如果写在每个 action creator 里会重复且难维护。

  2. 「中间件的签名为什么是三层柯里化?」
    (store) => (next) => (action) => {} 的三层对应三个不同时机:① 第一层 (store) 在 store 创建时执行一次(可以拿到 getState 与增强后的 dispatch);② 第二层 (next) 在 组装中间件链时执行一次(next 指向链中的下一个中间件);③ 第三层 (action) 在每次 dispatch 时执行(热路径)。这样可以「在初始化时做一次性准备、在每次派发时只做必要的事」。

  3. 「next 和 store.dispatch 有什么区别?」
    next 是「链中的下一个中间件」——调用它表示「我这个中间件处理完了,交给下一个」;store.dispatch 是「从链的头部重新开始」——调用它会再次经过所有中间件(包括当前这个),因此可能导致重复处理或死循环。所以中间件里应该调 next(action),只有「想额外派发一个新 action 并让它走完整流程」时才用 dispatch。

  4. 「thunk 的原理是什么?为什么能处理异步?」
    thunk 中间件的逻辑非常短:如果 action 是函数,就调用它并把 dispatch 与 getState 传进去;否则交给 next。因此「异步」并不是 thunk 的能力,而是「你在那个函数里怎么写」——你可以 await 一个请求、然后再 dispatch 结果。thunk 的价值是「把异步编排从组件里抽出来」,让组件只负责 dispatch(fetchTodos())。

  5. 「中间件的执行顺序是怎样的?」
    applyMiddleware(a, b, c) 会按数组顺序组合成 a(b(c(store.dispatch)))。因此 dispatch 时:「进入阶段」按数组顺序(a → b → c → reducer),「退出阶段」反向(reducer → c → b → a)。这也决定了放置策略:logger 放最前(能看到完整信息)、crashReporter 放靠前(能捕获后面所有中间件的异常)。

  6. 「thunk 和 saga 怎么选?」
    按复杂度选:thunk(RTK 内置)——极简、适合「发请求 → dispatch 结果」「条件式 dispatch」这类常见需求,占 90% 场景;saga——基于 generator,能表达「取消、竞态(race/takeLatest)、并发编排(all)、防抖节流」等复杂副作用,适合「大量交错的异步流程」,但学习成本高、代码量大。如果只是「请求 + 缓存」,两者都不需要——直接用 RTK Query 或 TanStack Query。

  7. 「中间件能修改 action 吗?」
    能。中间件可以在调用 next 之前替换 action(如把「函数」替换成「普通对象」),也可以包裹它(如加 meta、加时间戳、加 traceId)。这正是 thunk 的做法——把「函数 action」拦截下来执行,并不把函数本身传给 next(因为 reducer 只能处理普通对象)。不过要谨慎:随意改造 action 会让数据流难以追踪。

  8. 「为什么『在中间件里直接 dispatch』是危险的?」
    因为 store.dispatch 是「链的入口」——在中间件里调用它,会让新 action 重新走一遍所有中间件(包括当前这个)。如果那个 action 又被同一个中间件再次 dispatch,就会无限递归(栈溢出)。正确做法是:① 需要「继续传递」用 next(action);② 需要「额外派发」时,仔细确认它不会被当前中间件再次拦截(或加条件判断)。

一句话速记

中间件是「dispatch 与 reducer 之间的一层」,用来支持非普通对象的 action(异步)与插入横切逻辑(日志/埋点/持久化);签名是 (store) => (next) => (action) => {}(三层对应「初始化一次 / 组装时一次 / 每次派发」);thunk 的原理就是「action 是函数就调用它,把 dispatch 与 getState 传进去」,异步能力来自你在那个函数里怎么写;中间件里要调 next(action) 而不是 store.dispatch(否则会重走全链甚至死循环);applyMiddleware(a, b, c) 的进入顺序与数组一致、退出顺序相反(logger 放最前、crashReporter 放靠前);只做「请求 + 缓存」的话 thunk/saga 都不需要,直接用查询库。

赞(0) 打赏
未经允许不得转载:陌上寒 » 3.8 Redux 中间件

评论 抢沙发

觉得文章有用就打赏一下文章作者

非常感谢你的打赏,我们将继续给力更多优质内容,让我们一起创建更加美好的网络世界!

微信扫一扫

支付宝扫一扫