代码拆分
在大型 Web 应用中,通常希望将应用代码拆分成多个 JS 包,以便按需加载。这种策略称为“代码拆分”,通过减少必须获取的初始 JS 负载大小,帮助提高应用的性能。
要在 Redux 中进行代码拆分,我们需要能够向 store 动态添加 reducer。不过,Redux 实际上只有一个根 reducer 函数。该函数通常会在应用初始化时通过调用 combineReducers() 或类似函数生成。若要动态添加更多 reducer,就需要再次调用该函数重新生成根 reducer。Redux Toolkit 提供了可自动处理此过程的工具,也有一些第三方库提供类似功能。
基本原理
使用 replaceReducer
Redux store 提供了一个 replaceReducer 函数,用于用新的根 reducer 函数替换当前的活动根 reducer。调用它会替换内部的 reducer 函数引用,并派发一个动作来帮助新添加的 slice reducers 初始化自己:
const newRootReducer = combineReducers({
existingSlice: existingSliceReducer,
newSlice: newSliceReducer
})
store.replaceReducer(newRootReducer)
Redux Toolkit
Redux Toolkit 2.0 包含一些实用工具,用于简化 reducers 和中间件的代码拆分,包括对 Typescript 的良好支持(这对于懒加载 reducers 和中间件是一个常见挑战)。
combineSlices
combineSlices 工具旨在简化 reducer 注入。它也可以取代 combineReducers,用于将多个 slice 和 reducer 合并为一个根 reducer。
在设置时,它接受一组切片和 reducer 映射,返回附带注入方法的 reducer 实例。
combineSlices 的“切片”通常通过 createSlice 创建,但可以是任何带有 reducerPath 和 reducer 属性的“类似切片”的对象(意味着 RTK Query API 实例也兼容)。
const withUserReducer = rootReducer.inject({
reducerPath: 'user',
reducer: userReducer
})
const withApiReducer = rootReducer.inject(fooApi)
为简单起见,这里文档将 { reducerPath, reducer } 的形状称作“切片”。
切片将挂载在它们的 reducerPath 下,reducer 映射中的项目将挂载到各自的 key 下。
const rootReducer = combineSlices(counterSlice, baseApi, {
user: userSlice.reducer,
auth: authSlice.reducer
})
// 等同于
const rootReducer = combineReducers({
[counterSlice.reducerPath]: counterSlice.reducer,
[baseApi.reducerPath]: baseApi.reducer,
user: userSlice.reducer,
auth: authSlice.reducer
})
注意避免命名冲突 —— 后面的 key 会覆盖前面的,但 Typescript 无法对此进行检测。
切片注入
要注入切片,应调用从 combineSlices 返回的 reducer 实例上的 rootReducer.inject(slice)。这会将切片注入到其 reducerPath 处,并返回已注入的组合 reducer 实例,类型知道该切片已注入。
或者,可以调用 slice.injectInto(rootReducer),这会返回一个已知被注入的切片实例。甚至可能要同时调用两者,因为每个调用都会返回有用的内容,并且 combineSlices 允许在相同的 reducerPath 注入相同的 reducer 实例,且不会有问题。
const withCounterSlice = rootReducer.inject(counterSlice)
const injectedCounterSlice = counterSlice.injectInto(rootReducer)
与典型的 reducer 注入不同,combineSlice 的“元 reducer”方式不会调用 replaceReducer。传递给 store 的 reducer 实例不会改变。
因此,注入切片时不会派发动作,因此注入的切片状态不会立即出现在状态中。状态只会在派发动作时显示在 store 的状态中。
不过,为避免选择器必须处理可能的 undefined 状态,combineSlices 提供了一些有用的选择器工具。
声明懒加载切片
为了让懒加载切片显示在推断的状态类型中,提供了 withLazyLoadedSlices 辅助函数。你可以声明想要以后注入的切片,使它们作为可选项显示在状态类型中。
要完全避免在组合 reducer 文件中导入懒加载切片,可以使用模块增强。
// 文件:reducer.ts
import { combineSlices } from '@reduxjs/toolkit'
import { staticSlice } from './staticSlice'
export interface LazyLoadedSlices {}
export const rootReducer =
combineSlices(staticSlice).withLazyLoadedSlices<LazyLoadedSlices>()
// 文件:counterSlice.ts
import type { WithSlice } from '@reduxjs/toolkit'
import { createSlice } from '@reduxjs/toolkit'
import { rootReducer } from './reducer'
interface CounterState {
value: number
}
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 } as CounterState,
reducers: {
increment: state => void state.value++
},
selectors: {
selectValue: state => state.value
}
})
declare module './reducer' {
// WithSlice 工具假设 reducer 位于 slice.reducerPath 下
export interface LazyLoadedSlices extends WithSlice<typeof counterSlice> {}
// 如果不是,可以用普通 key
export interface LazyLoadedSlices {
aCounter: CounterState
}
}
const injectedCounterSlice = counterSlice.injectInto(rootReducer)
const injectedACounterSlice = counterSlice.injectInto(rootReducer, {
reducerPath: 'aCounter'
})
选择器工具
除了 inject,组合的 reducer 实例还有 .selector 方法用于包裹选择器。它将状态对象包装在一个 Proxy 中,并为已注入但尚未在状态中出现的 reducer 提供初始状态。
调用 inject 的结果类型会保证被注入的切片在调用选择器时总是已定义。
const selectCounterValue = (state: RootState) => state.counter?.value // number | undefined
const withCounterSlice = rootReducer.inject(counterSlice)
const selectCounterValue = withCounterSlice.selector(
state => state.counter.value // number - 如果不在 store,则使用初始状态
)
切片的“注入”实例对于切片选择器也做同样的处理 —— 如果传入的状态未包含对应切片,则使用初始状态。
const injectedCounterSlice = counterSlice.injectInto(rootReducer)
console.log(counterSlice.selectors.selectValue({})) // 运行时错误
console.log(injectedCounterSlice.selectors.selectValue({})) // 0
典型用法
combineSlices 设计为切片在需要时立即被注入(即从已加载组件导入选择器或动作时)。
因此,典型用法大致如下。
// 文件:reducer.ts
import { combineSlices } from '@reduxjs/toolkit'
import { staticSlice } from './staticSlice'
export interface LazyLoadedSlices {}
export const rootReducer =
combineSlices(staticSlice).withLazyLoadedSlices<LazyLoadedSlices>()
// 文件:store.ts
import { configureStore } from '@reduxjs/toolkit'
import { rootReducer } from './reducer'
export const store = configureStore({ reducer: rootReducer })
// 文件:counterSlice.ts
import type { WithSlice } from '@reduxjs/toolkit'
import { createSlice } from '@reduxjs/toolkit'
import { rootReducer } from './reducer'
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
increment: state => void state.value++
},
selectors: {
selectValue: state => state.value
}
})
export const { increment } = counterSlice.actions
declare module './reducer' {
export interface LazyLoadedSlices extends WithSlice<typeof counterSlice> {}
}
const injectedCounterSlice = counterSlice.injectInto(rootReducer)
export const { selectValue } = injectedCounterSlice.selectors
// 文件:Counter.tsx
// 通过从 counterSlice 导入,确保
// 注入在组件定义前发生
import { increment, selectValue } from './counterSlice'
import { useAppDispatch, useAppSelector } from './hooks'
export default function Counter() {
const dispatch = useAppDispatch()
const value = useAppSelector(selectValue)
return (
<>
<p>{value}</p>
<button onClick={() => dispatch(increment())}>递增</button>
</>
)
}
// file: App.tsx
import { lazy } from 'react'
import { Provider } from 'react-redux'
import { store } from './store'
// lazily importing the component means that the code
// doesn't actually get pulled in and executed until the component is rendered.
// this means that the inject call only happens once Counter renders
const Counter = lazy(() => import('./Counter'))
function App() {
return (
<Provider store={store}>
<Counter />
</Provider>
)
}
createDynamicMiddleware
createDynamicMiddleware 实用工具创建一个“元中间件”,允许在 store 初始化后注入中间件。
import { createDynamicMiddleware, configureStore } from '@reduxjs/toolkit'
import logger from 'redux-logger'
import reducer from './reducer'
const dynamicMiddleware = createDynamicMiddleware()
const store = configureStore({
reducer,
middleware: getDefaultMiddleware =>
getDefaultMiddleware().concat(dynamicMiddleware.middleware)
})
dynamicMiddleware.addMiddleware(logger)
addMiddleware
addMiddleware 会将中间件实例追加到由动态中间件实例处理的中间件链中。中间件按注入顺序应用,且通过函数引用存储(相同中间件无论注入多少次只应用一次)。
重要的是,所有注入的中间件都包含在原始动态中间件实例内部。
import { createDynamicMiddleware, configureStore } from '@reduxjs/toolkit'
import logger from 'redux-logger'
import reducer from './reducer'
const dynamicMiddleware = createDynamicMiddleware()
const store = configureStore({
reducer,
middleware: getDefaultMiddleware =>
getDefaultMiddleware().concat(dynamicMiddleware.middleware)
})
dynamicMiddleware.addMiddleware(logger)
// 中间件链现在是 [thunk, logger]
如果想对顺序有更精细控制,可以使用多个实例。
import { createDynamicMiddleware, configureStore } from '@reduxjs/toolkit'
import logger from 'redux-logger'
import reducer from './reducer'
const beforeMiddleware = createDynamicMiddleware()
const afterMiddleware = createDynamicMiddleware()
const store = configureStore({
reducer,
middleware: getDefaultMiddleware =>
getDefaultMiddleware()
.prepend(beforeMiddleware.middleware)
.concat(afterMiddleware.middleware)
})
beforeMiddleware.addMiddleware(logger)
afterMiddleware.addMiddleware(logger)
// 中间件链现在是 [logger, thunk, logger]
withMiddleware
withMiddleware 是一个动作创建函数,调度时会让中间件添加任何包括的中间件,并返回一个预类型化的 dispatch,带有任何添加的扩展。
const listenerDispatch = store.dispatch(
withMiddleware(listenerMiddleware.middleware)
)
const unsubscribe = listenerDispatch(addListener({ actionCreator, effect }))
// ^? () => void
这主要在非 React 场景下有用。对于 React,更适合使用React 集成。
React 集成
从 @reduxjs/toolkit/react 入口导入时,动态中间件实例将附加几个额外方法。
createDispatchWithMiddlewareHook
该方法调用 addMiddleware,并返回一个类型认识到已注入中间件的 useDispatch 版本。
import { createDynamicMiddleware } from '@reduxjs/toolkit/react'
const dynamicMiddleware = createDynamicMiddleware()
const useListenerDispatch = dynamicMiddleware.createDispatchWithMiddlewareHook(
listenerMiddleware.middleware
)
function Component() {
const dispatch = useListenerDispatch()
useEffect(() => {
const unsubscribe = dispatch(addListener({ actionCreator, effect }))
return unsubscribe
}, [dispatch])
}
中间件在调用 createDispatchWithMiddlewareHook 时注入,不是在调用 useDispatch hook 时注入。
createDispatchWithMiddlewareHookFactory
此方法接收一个 React context 实例,并创建一个使用该 context 的 createDispatchWithMiddlewareHook 实例。(请参阅提供自定义 Context。)
import { createContext } from 'react'
import { createDynamicMiddleware } from '@reduxjs/toolkit/react'
import type { ReactReduxContextValue } from 'react-redux'
const context = createContext<ReactReduxContextValue | null>(null)
const dynamicMiddleware = createDynamicMiddleware()
const createDispatchWithMiddlewareHook =
dynamicMiddleware.createDispatchWithMiddlewareHookFactory(context)
const useListenerDispatch = createDispatchWithMiddlewareHook(
listenerMiddleware.middleware
)
function Component() {
const dispatch = useListenerDispatch()
useEffect(() => {
const unsubscribe = dispatch(addListener({ actionCreator, effect }))
return unsubscribe
}, [dispatch])
}