本文档深入讲解 Redux
applyMiddleware的设计目标、中间件链执行流程、源码核心思想、关键设计点和常见中间件实现方式。重点用流程图把dispatch(action)如何一层层穿过 middleware,最终到达 reducer 的过程讲清楚。
目录
- [为什么 Redux 需要中间件](#为什么 Redux 需要中间件 "#1-%E4%B8%BA%E4%BB%80%E4%B9%88-redux-%E9%9C%80%E8%A6%81%E4%B8%AD%E9%97%B4%E4%BB%B6")
- 先看最终效果:中间件链全景图
- [Redux 原始数据流](#Redux 原始数据流 "#3-redux-%E5%8E%9F%E5%A7%8B%E6%95%B0%E6%8D%AE%E6%B5%81")
- [applyMiddleware 到底做了什么](#applyMiddleware 到底做了什么 "#4-applymiddleware-%E5%88%B0%E5%BA%95%E5%81%9A%E4%BA%86%E4%BB%80%E4%B9%88")
- 中间件函数签名拆解
- [compose 组合机制详解](#compose 组合机制详解 "#6-compose-%E7%BB%84%E5%90%88%E6%9C%BA%E5%88%B6%E8%AF%A6%E8%A7%A3")
- [dispatch 被增强的完整流程](#dispatch 被增强的完整流程 "#7-dispatch-%E8%A2%AB%E5%A2%9E%E5%BC%BA%E7%9A%84%E5%AE%8C%E6%95%B4%E6%B5%81%E7%A8%8B")
- 常见中间件执行顺序
- [手写 applyMiddleware](#手写 applyMiddleware "#9-%E6%89%8B%E5%86%99-applymiddleware")
- 典型中间件实现
- 关键设计点与源码思想
- 常见踩坑与最佳实践
- 面试与实战总结
1. 为什么 Redux 需要中间件
1.1 Redux 原本只处理同步 action
Redux 的核心非常纯粹:
text
UI 触发 action
↓
store.dispatch(action)
↓
reducer 根据 action 计算新 state
↓
store 通知订阅者
↓
React 重新渲染
原始 Redux 只要求 dispatch 接收普通对象:
js
store.dispatch({
type: 'user/login',
payload: { username: 'Tom' },
})
也就是说,默认情况下 Redux 只认识这种 action:
text
普通 JS 对象 plain object
1.2 真实业务不只是同步改状态
真实前端业务里,dispatch 之前或之后经常要做很多副作用:
- 打日志;
- 异步请求接口;
- 埋点上报;
- 错误捕获;
- 权限校验;
- action 格式校验;
- loading 状态管理;
- promise 展开;
- thunk 函数执行;
- crash report 上报。
如果这些逻辑都写在组件里,组件会越来越乱:
js
async function handleLogin() {
console.log('login start')
dispatch({ type: 'login/start' })
try {
const user = await api.login()
dispatch({ type: 'login/success', payload: user })
} catch (err) {
reportError(err)
dispatch({ type: 'login/fail', error: err })
}
}
中间件就是为了解决这个问题:
text
把 dispatch 前后通用的副作用逻辑,从组件和 reducer 中抽离出来。
1.3 中间件解决的核心问题
text
组件只负责发出意图
↓
中间件负责处理副作用、异步、日志、监控、拦截
↓
reducer 仍然保持纯函数,只负责计算 state
一句话:
text
Redux middleware 是 dispatch 的增强器。
它不改 reducer,不改 state 结构,而是改造 dispatch 的行为。
2. 先看最终效果:中间件链全景图
2.1 加了中间件后的完整链路
假设我们注册了三个中间件:
js
const store = createStore(
reducer,
applyMiddleware(logger, thunk, crashReporter)
)
最终 dispatch 流程大概是:
text
组件调用 dispatch(action)
│
▼
┌──────────────────────────────┐
│ logger middleware │
│ - 记录 action 前 state │
│ - 调用 next(action) │
│ - 记录 action 后 state │
└───────────────┬──────────────┘
│ next(action)
▼
┌──────────────────────────────┐
│ thunk middleware │
│ - 如果 action 是函数,就执行它 │
│ - 如果 action 是对象,就放行 │
└───────────────┬──────────────┘
│ next(action)
▼
┌──────────────────────────────┐
│ crashReporter middleware │
│ - try/catch 包住后续 dispatch │
│ - 捕获异常并上报 │
└───────────────┬──────────────┘
│ next(action)
▼
┌──────────────────────────────┐
│ 原始 store.dispatch │
│ - 校验 action │
│ - 调用 reducer │
│ - 更新 state │
│ - 通知 listener │
└───────────────┬──────────────┘
│
▼
React 重新渲染
2.2 洋葱模型图
Redux 中间件链很像洋葱模型:
text
dispatch(action)
│
▼
┌────────────────┐
│ middleware A │
│ before │
│ ┌───────────┴──────┐
│ │ middleware B │
│ │ before │
│ │ ┌─────────────┴────┐
│ │ │ middleware C │
│ │ │ before │
│ │ │ ┌─────────────┴───┐
│ │ │ │ originalDispatch │
│ │ │ │ reducer 更新 state│
│ │ │ └─────────────┬───┘
│ │ │ after │
│ │ └─────────────┬────┘
│ │ after │
│ └───────────┬──────┘
│ after │
└────────────────┘
执行顺序:
text
进入顺序:A before → B before → C before → reducer
返回顺序:C after → B after → A after
2.3 大白话理解
中间件链像公司审批流:
text
员工提交申请 action
↓
主管 logger 看一眼并记录
↓
HR thunk 判断是不是特殊申请
↓
法务 crashReporter 兜底异常
↓
老板 reducer 真正拍板修改 state
↓
结果再一层层返回给前面的人
每个中间件都有三个选择:
- 放行:
next(action); - 拦截:不调用
next(action); - 改造:修改 action 后再
next(newAction); - 延迟:异步完成后再 dispatch 新 action;
- 扩展:在
next(action)前后加逻辑。
3. Redux 原始数据流
3.1 不使用中间件时
text
React Component
│
│ store.dispatch(action)
▼
┌─────────────────┐
│ originalDispatch│
└────────┬────────┘
│
▼
┌─────────────────┐
│ reducer │
│ state + action │
│ => nextState │
└────────┬────────┘
│
▼
┌─────────────────┐
│ store.state │
│ 被替换为 nextState│
└────────┬────────┘
│
▼
┌─────────────────┐
│ listeners │
│ 通知订阅者 │
└────────┬────────┘
│
▼
React-Redux 触发组件更新
3.2 原始 dispatch 的特点
Redux 原始 dispatch 做的事情很少:
text
1. 检查 action 是不是普通对象
2. 检查 action.type 是否存在
3. 调用 reducer(currentState, action)
4. 保存新的 state
5. 通知订阅函数
6. 返回 action
它不负责:
- 请求接口;
- 等待 Promise;
- 执行函数 action;
- 记录日志;
- 捕获业务异常;
- 自动重试;
- 埋点上报。
这些能力都通过 middleware 扩展。
4. applyMiddleware 到底做了什么
4.1 applyMiddleware 的定位
applyMiddleware 不是 middleware 本身,而是一个 store enhancer。
text
middleware:增强 dispatch 的具体插件
applyMiddleware:把多个 middleware 组合起来的工具
store enhancer:增强 createStore 能力的高阶函数
调用方式:
js
const store = createStore(reducer, applyMiddleware(thunk, logger))
本质上等价于:
text
createStore 被 applyMiddleware 包了一层
↓
先创建原始 store
↓
再用 middleware 链替换 store.dispatch
↓
返回增强后的 store
4.2 核心流程图
text
applyMiddleware(logger, thunk, crashReporter)
│
▼
返回 enhancer:函数(createStore) => 新 createStore
│
▼
createStore(reducer, preloadedState)
│
▼
先调用原始 createStore 创建 store
│
▼
取出 store.getState 和原始 dispatch
│
▼
构造 middlewareAPI = { getState, dispatch }
│
▼
依次调用 middleware(middlewareAPI)
│
▼
得到 chain = [loggerFn, thunkFn, crashReporterFn]
│
▼
compose(...chain)(store.dispatch)
│
▼
得到增强版 dispatch
│
▼
返回 { ...store, dispatch: enhancedDispatch }
4.3 源码简化版
Redux applyMiddleware 核心可以简化成:
js
function applyMiddleware(...middlewares) {
return function enhancer(createStore) {
return function newCreateStore(reducer, preloadedState) {
const store = createStore(reducer, preloadedState)
let dispatch = () => {
throw new Error('Dispatching while constructing middleware is not allowed')
}
const middlewareAPI = {
getState: store.getState,
dispatch: (action, ...args) => dispatch(action, ...args),
}
const chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)
return {
...store,
dispatch,
}
}
}
}
这里最关键的两行:
js
const chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)
含义:
text
第一行:给每个 middleware 注入 getState 和 dispatch
第二行:把多个 middleware 包成一条 dispatch 调用链
5. 中间件函数签名拆解
5.1 标准 middleware 长什么样
Redux middleware 是三层函数:
js
const middleware = storeAPI => next => action => {
// before
const result = next(action)
// after
return result
}
完整类型可以理解为:
text
middleware({ getState, dispatch })(next)(action)
三层分别是:
text
第一层:拿到 storeAPI
第二层:拿到 next,也就是下一个 middleware 的 dispatch
第三层:拿到 action,真正处理本次 dispatch
5.2 三层函数分别解决什么问题
text
middleware = storeAPI => next => action => result
│ │ │
│ │ └─ 本次派发的 action
│ └───────── 下一个中间件,或者原始 dispatch
└──────────────────── getState 和 dispatch
第一层:storeAPI
js
const middleware = ({ getState, dispatch }) => next => action => {}
用于:
- 读取当前 state;
- 派发新的 action;
- 在异步任务里继续 dispatch;
- 根据 state 判断是否拦截。
第二层:next
js
const middleware = storeAPI => next => action => {}
next 表示链路中的下一棒:
text
当前 middleware 调用 next(action),action 才能继续往后走。
如果不调用 next(action),action 就被拦截。
第三层:action
js
const middleware = storeAPI => next => action => {}
本次 dispatch 的内容,可以是:
- 普通对象;
- 函数;
- Promise;
- 特殊协议对象;
- 被中间件转换后的新 action。
最终原始 dispatch 只接受普通对象。
5.3 为什么要设计成三层函数
因为 Redux 要分阶段完成三件事:
text
创建 store 时:注入 getState、dispatch
组合链路时:注入 next
真正 dispatch 时:传入 action
如果写成一个函数,很难同时做到:
- 初始化时拿 store;
- 组合时拿下一个 middleware;
- 执行时拿 action;
- 支持优雅的函数组合。
三层函数本质是函数式编程里的柯里化设计。
6. compose 组合机制详解
6.1 compose 是什么
compose 的作用是把多个函数从右到左组合起来。
js
compose(f, g, h)(x)
等价于:
js
f(g(h(x)))
Redux 中:
js
compose(logger, thunk, crashReporter)(store.dispatch)
等价于:
js
logger(thunk(crashReporter(store.dispatch)))
最终得到的是:
text
被 logger 包住、里面是 thunk、再里面是 crashReporter、最里面是原始 dispatch 的增强 dispatch。
6.2 compose 流程图
text
原始函数:
logger
thunk
crashReporter
originalDispatch
组合后:
logger(
thunk(
crashReporter(
originalDispatch
)
)
)
视觉化:
text
┌────────────────────────────────────────┐
│ logger │
│ next = thunk(...) │
│ ┌──────────────────────────────────┐ │
│ │ thunk │ │
│ │ next = crashReporter(...) │ │
│ │ ┌────────────────────────────┐ │ │
│ │ │ crashReporter │ │ │
│ │ │ next = originalDispatch │ │ │
│ │ └────────────────────────────┘ │ │
│ └──────────────────────────────────┘ │
└────────────────────────────────────────┘
6.3 compose 简化实现
js
function compose(...funcs) {
if (funcs.length === 0) {
return arg => arg
}
if (funcs.length === 1) {
return funcs[0]
}
return funcs.reduce(
(a, b) => (...args) => a(b(...args))
)
}
核心:
text
把 [A, B, C] 组合成 A(B(C(dispatch)))
6.4 为什么注册顺序和执行顺序看起来不同
注册:
js
applyMiddleware(A, B, C)
组合:
text
A(B(C(originalDispatch)))
执行:
text
dispatch(action)
↓
A 收到 action
↓
A 调 next(action)
↓
B 收到 action
↓
B 调 next(action)
↓
C 收到 action
↓
C 调 next(action)
↓
originalDispatch 收到 action
所以:
text
中间件进入顺序和注册顺序一致。
中间件返回顺序和注册顺序相反。
7. dispatch 被增强的完整流程
7.1 初始化阶段
text
createStore(reducer, applyMiddleware(A, B, C))
│
▼
applyMiddleware 创建 enhancer
│
▼
enhancer 包装原始 createStore
│
▼
原始 createStore 创建 store
│
▼
准备 middlewareAPI
│
▼
A(middlewareAPI), B(middlewareAPI), C(middlewareAPI)
│
▼
得到 chain: [A1, B1, C1]
│
▼
compose(A1, B1, C1)(store.dispatch)
│
▼
得到 enhancedDispatch
│
▼
返回增强后的 store
7.2 运行阶段
当组件调用:
js
store.dispatch({ type: 'todo/add', payload: '学习 Redux' })
实际执行的是增强后的 dispatch:
text
store.dispatch(action)
│
▼
┌────────────────────┐
│ A middleware │
│ before │
│ next(action) │
│ after │
└─────────┬──────────┘
│
▼
┌────────────────────┐
│ B middleware │
│ before │
│ next(action) │
│ after │
└─────────┬──────────┘
│
▼
┌────────────────────┐
│ C middleware │
│ before │
│ next(action) │
│ after │
└─────────┬──────────┘
│
▼
┌────────────────────┐
│ originalDispatch │
│ reducer 更新 state │
└────────────────────┘
7.3 带 before/after 的详细执行顺序
假设三个中间件都长这样:
js
const A = store => next => action => {
console.log('A before')
const result = next(action)
console.log('A after')
return result
}
注册:
js
applyMiddleware(A, B, C)
输出顺序:
text
A before
B before
C before
reducer run
C after
B after
A after
流程图:
text
调用方向:
A before ──→ B before ──→ C before ──→ reducer
│
返回方向:
A after ←── B after ←── C after ←────────────┘
8. 常见中间件执行顺序
8.1 logger 中间件
js
const logger = store => next => action => {
console.log('prev state', store.getState())
console.log('action', action)
const result = next(action)
console.log('next state', store.getState())
return result
}
它的关键是:
text
next(action) 前:拿到旧 state
next(action) 后:拿到新 state
8.2 thunk 中间件
js
const thunk = ({ dispatch, getState }) => next => action => {
if (typeof action === 'function') {
return action(dispatch, getState)
}
return next(action)
}
它让 dispatch 支持函数:
js
dispatch(async (dispatch, getState) => {
dispatch({ type: 'login/start' })
const user = await api.login()
dispatch({ type: 'login/success', payload: user })
})
8.3 crashReporter 中间件
js
const crashReporter = store => next => action => {
try {
return next(action)
} catch (err) {
reportError(err, {
action,
state: store.getState(),
})
throw err
}
}
它的作用:
text
包住后续中间件和 reducer,一旦出错就捕获并上报。
8.4 顺序为什么重要
不同顺序会产生不同效果。
logger 在 thunk 前面
js
applyMiddleware(logger, thunk)
如果 dispatch 一个函数:
js
dispatch(fetchUser())
logger 会先看到函数 action。
thunk 在 logger 前面
js
applyMiddleware(thunk, logger)
thunk 会先执行函数 action,函数内部派发的普通对象 action 才会被 logger 记录。
实际开发常见顺序:
js
applyMiddleware(thunk, logger)
这样 logger 更容易记录真正进入 reducer 的普通 action。
9. 手写 applyMiddleware
9.1 最小版本
js
function applyMiddleware(...middlewares) {
return createStore => (reducer, preloadedState) => {
const store = createStore(reducer, preloadedState)
let dispatch = store.dispatch
const middlewareAPI = {
getState: store.getState,
dispatch: action => dispatch(action),
}
const chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)
return {
...store,
dispatch,
}
}
}
9.2 为什么 middlewareAPI.dispatch 要包一层
注意这里:
js
dispatch: action => dispatch(action)
而不是:
js
dispatch: store.dispatch
原因是:
text
middleware 里拿到的 dispatch,应该永远指向最终增强后的 dispatch。
如果直接传 store.dispatch,中间件内部再次 dispatch 时,会绕过其他中间件。
9.3 初始化期间禁止 dispatch
Redux 源码里会先把 dispatch 设置成一个报错函数:
js
let dispatch = () => {
throw new Error('Dispatching while constructing your middleware is not allowed')
}
原因:
text
中间件链还没组装完成,此时 dispatch 行为不完整。
如果中间件在初始化阶段就 dispatch,可能导致:
- 部分中间件没生效;
- 链路顺序混乱;
- 状态更新不可预测;
- 调试困难。
10. 典型中间件实现
10.1 logger:记录 action 和 state
js
const logger = store => next => action => {
const prevState = store.getState()
console.group(action.type)
console.log('prev state', prevState)
console.log('action', action)
const result = next(action)
const nextState = store.getState()
console.log('next state', nextState)
console.groupEnd()
return result
}
适合开发环境,生产环境要控制日志量。
10.2 thunk:支持异步函数 action
js
const thunk = ({ dispatch, getState }) => next => action => {
if (typeof action === 'function') {
return action(dispatch, getState)
}
return next(action)
}
业务使用:
js
const fetchUser = userId => async (dispatch, getState) => {
dispatch({ type: 'user/fetchStart', payload: userId })
try {
const user = await api.getUser(userId)
dispatch({ type: 'user/fetchSuccess', payload: user })
} catch (err) {
dispatch({ type: 'user/fetchFail', error: err })
}
}
10.3 promiseMiddleware:支持 Promise action
js
const promiseMiddleware = store => next => action => {
if (action && typeof action.then === 'function') {
return action.then(store.dispatch)
}
return next(action)
}
可以支持:
js
dispatch(fetch('/api/user').then(res => ({
type: 'user/success',
payload: res.json(),
})))
但生产项目里更推荐 thunk、saga 或 RTK Query,因为 Promise action 的可维护性较弱。
10.4 loadingMiddleware:统一处理 loading
约定 action 带 meta:
js
const action = {
type: 'user/fetch',
payload: api.getUser(),
meta: {
loadingKey: 'userDetail',
},
}
中间件:
js
const loadingMiddleware = ({ dispatch }) => next => async action => {
const loadingKey = action.meta && action.meta.loadingKey
if (!loadingKey) {
return next(action)
}
dispatch({ type: 'loading/start', payload: loadingKey })
try {
const result = await next(action)
return result
} finally {
dispatch({ type: 'loading/end', payload: loadingKey })
}
}
关键点:
- 必须保证 finally 执行;
- loadingKey 要稳定;
- 不要让 loading action 再触发自己造成循环。
10.5 analyticsMiddleware:埋点上报
js
const analyticsMiddleware = store => next => action => {
const result = next(action)
if (action.meta && action.meta.analytics) {
sendAnalytics({
event: action.meta.analytics,
actionType: action.type,
state: store.getState(),
})
}
return result
}
适合:
- 页面行为上报;
- 按钮点击上报;
- 关键业务节点上报;
- 状态变更后上报。
10.6 permissionMiddleware:权限拦截
js
const permissionMiddleware = store => next => action => {
const permission = action.meta && action.meta.permission
if (!permission) {
return next(action)
}
const state = store.getState()
if (!state.auth.permissions.includes(permission)) {
return next({
type: 'permission/denied',
payload: permission,
})
}
return next(action)
}
这类中间件适合拦截业务行为,但不要滥用。组件层和路由层的权限控制仍然要保留。
11. 关键设计点与源码思想
11.1 中间件增强的是 dispatch,不是 reducer
Redux 的核心原则是 reducer 必须纯净:
text
相同 state + action => 相同 nextState
副作用不能放 reducer:
- 不能请求接口;
- 不能改外部变量;
- 不能写 localStorage;
- 不能发埋点;
- 不能生成不可预测随机结果。
中间件把副作用放在 dispatch 链路上,保证 reducer 仍然纯。
11.2 next 和 dispatch 不是同一个东西
这是理解中间件最关键的点。
text
next(action):把 action 交给链路中的下一个中间件
store.dispatch(action):从整条增强链路的最外层重新开始派发
流程图:
text
当前在 middleware B 内部
store.dispatch(newAction)
│
▼
从 A 重新开始:A → B → C → reducer
next(action)
│
▼
继续往后走:C → reducer
所以:
- 想放行当前 action:用
next(action); - 想额外派发一个新 action:用
dispatch(newAction); - 如果用错,可能导致中间件重复执行或死循环。
11.3 中间件可以拦截 action
js
const filterMiddleware = store => next => action => {
if (action.type === 'debug/ignore') {
return
}
return next(action)
}
不调用 next(action),reducer 就永远收不到这个 action。
适合:
- 权限拦截;
- 非法 action 拦截;
- 过期请求拦截;
- 重复请求拦截。
但要慎用,因为拦截会改变数据流,调试时要有日志。
11.4 中间件可以改写 action
js
const normalizeMiddleware = store => next => action => {
if (action.type === 'user/update') {
return next({
...action,
payload: normalizeUser(action.payload),
})
}
return next(action)
}
适合做:
- payload 规范化;
- action meta 补充;
- 兼容老 action;
- 自动加 traceId。
但不要在中间件里做过重业务转换,否则 action 变得不可预测。
11.5 中间件可以延迟 action
js
const delayMiddleware = store => next => action => {
const delay = action.meta && action.meta.delay
if (!delay) {
return next(action)
}
setTimeout(() => {
next(action)
}, delay)
}
注意:延迟后如果组件已经卸载,仍然可能更新状态,所以复杂场景要配合请求取消或状态校验。
11.6 中间件顺序是 API 设计的一部分
中间件顺序不是随便排的。
一般建议:
text
协议转换类:thunk / promise / saga
↓
业务拦截类:permission / auth / dedupe
↓
监控日志类:logger / analytics
↓
异常兜底类:crashReporter 可根据目标放最外层或最内层
具体顺序取决于你想观察的是:
- 原始 action;
- 转换后的 action;
- 进入 reducer 的 action;
- reducer 后的 state。
11.7 middlewareAPI.dispatch 使用闭包保证最终 dispatch
源码里这个设计非常关键:
js
const middlewareAPI = {
getState: store.getState,
dispatch: (action, ...args) => dispatch(action, ...args),
}
dispatch 变量后面会被重新赋值为增强版 dispatch。
因为 JS 闭包捕获的是变量引用,所以中间件内部调用 dispatch 时,拿到的是最终增强后的 dispatch。
流程:
text
初始化 middlewareAPI.dispatch
↓
它引用外层 dispatch 变量
↓
compose 完成后 dispatch 被改成 enhancedDispatch
↓
中间件内部调用 dispatch
↓
实际走 enhancedDispatch 完整链路
11.8 为什么 reducer 执行期间不能 dispatch
Redux 会禁止 reducer 执行期间再次 dispatch。
原因:
text
reducer 正在计算新 state
↓
如果中途又 dispatch
↓
当前 state 到底是哪一个会变得混乱
reducer 必须保持纯函数,不应该在 reducer 中触发新的 action。
12. 常见踩坑与最佳实践
12.1 忘记 return next(action)
错误:
js
const middleware = store => next => action => {
next(action)
}
推荐:
js
const middleware = store => next => action => {
return next(action)
}
原因:
text
dispatch 的返回值可能被调用方依赖。
例如 thunk 会返回异步函数的结果,组件可能会:
js
await dispatch(fetchUser())
12.2 dispatch 和 next 用错导致死循环
错误:
js
const middleware = store => next => action => {
return store.dispatch(action)
}
这会让 action 从最外层重新进入当前中间件,形成死循环。
正确放行:
js
return next(action)
只有额外派发新 action 时才用 store.dispatch(newAction)。
12.3 中间件里吞掉 action
js
const middleware = store => next => action => {
if (action.type === 'x') {
return
}
return next(action)
}
如果是有意拦截,必须写清楚日志或注释。否则 reducer 收不到 action,会很难排查。
12.4 异步中间件没有处理错误
js
const middleware = store => next => async action => {
const result = await api.call()
return next({ type: 'success', payload: result })
}
如果接口失败,可能导致未捕获 Promise rejection。
推荐:
- try/catch;
- fail action;
- 统一错误上报;
- loading finally 关闭;
- 调用方可感知返回值。
12.5 logger 顺序导致看不到真实 action
如果 logger 放在 thunk 前面,它会记录函数 action。
js
applyMiddleware(logger, thunk)
如果想记录 reducer 真正处理的普通 action,更推荐:
js
applyMiddleware(thunk, logger)
12.6 不要把所有业务都塞进 middleware
middleware 适合横切能力:
- 日志;
- 异步协议;
- 监控;
- 错误上报;
- 权限拦截;
- action 规范化。
不适合承载大量具体业务逻辑。具体业务逻辑更适合:
- action creator;
- thunk;
- saga;
- RTK Query;
- service 层。
12.7 生产环境日志要谨慎
logger 在开发环境很好用,但生产环境可能带来:
- 性能损耗;
- 隐私泄漏;
- 日志量过大;
- 控制台污染;
- 敏感信息暴露。
建议:
text
开发环境启用详细 logger
生产环境只上报必要指标和异常
敏感字段要脱敏
13. 面试与实战总结
13.1 一句话解释 middleware
text
Redux middleware 是对 dispatch 的增强,它位于 action 发出和 reducer 执行之间,用来处理异步、日志、错误上报、权限拦截等副作用逻辑。
13.2 一句话解释 applyMiddleware
text
applyMiddleware 是 Redux 提供的 store enhancer,它会先创建原始 store,然后把多个 middleware 通过 compose 组合成一条链,最终替换 store.dispatch。
13.3 一句话解释中间件链
text
中间件链本质是 A(B(C(originalDispatch))) 这样的函数嵌套,action 进入时按注册顺序经过中间件,返回时按相反顺序回到调用方。
13.4 必须掌握的关键点
- middleware 的签名是
storeAPI => next => action => result; applyMiddleware增强的是dispatch;next(action)表示交给下一个中间件;dispatch(action)表示从完整中间件链重新开始;compose(...chain)(store.dispatch)生成最终增强 dispatch;- 注册顺序决定进入顺序,返回顺序相反;
- thunk 让 dispatch 支持函数 action;
- logger 的位置会影响它看到的 action;
- 中间件可以拦截、改写、延迟、扩展 action;
- reducer 必须保持纯函数,副作用放在 middleware 或异步 action 中。
13.5 最终记忆版
text
Redux 的原始 dispatch 只能处理普通对象 action。
applyMiddleware 的作用就是把 dispatch 包装成一条中间件链。
每个 middleware 都是 storeAPI => next => action 三层函数。
storeAPI 让中间件能读 state 和再次 dispatch。
next 代表链路中的下一棒。
action 是本次真正派发的内容。
多个 middleware 通过 compose 组合成:
A(B(C(originalDispatch)))。
所以 action 进入时是 A → B → C → reducer,返回时是 C → B → A。
最关键的区别是:
next(action) 是继续当前链路,dispatch(action) 是从完整链路重新开始。
理解这一点,就理解了 Redux 中间件链的核心。