跳至主内容
非官方测试版翻译

本页面由 PageTurner AI 翻译(测试版)。未经项目官方认可。 发现错误? 报告问题 →

代码分割

在大型 Web 应用中,通常需要将应用代码拆分成多个可按需加载的 JS 包。这种称为"代码分割"的策略,通过减小必须加载的初始 JS 负载体积,有助于提升应用性能。

要在 Redux 中实现代码分割,我们需要能够动态向 store 添加 reducer。然而 Redux 实际上只有一个根 reducer 函数。该根 reducer 通常在应用初始化时通过调用 combineReducers() 或类似函数生成。要动态添加更多 reducer,我们需要重新调用该函数来重新生成根 reducer。下文将讨论解决此问题的几种方案,并介绍两个提供此功能的库。

基本原理

使用 replaceReducer

Redux store 暴露了 replaceReducer 函数,它用新的根 reducer 函数替换当前活动的根 reducer 函数。调用该函数会交换内部 reducer 函数引用,并分发 action 帮助新添加的切片 reducer 完成自我初始化:

const newRootReducer = combineReducers({
existingSlice: existingSliceReducer,
newSlice: newSliceReducer
})

store.replaceReducer(newRootReducer)

Reducer 注入方案

本节介绍几种手动注入 reducer 的实现方法。

定义 injectReducer 函数

我们可能需要从应用任意位置调用 store.replaceReducer()。因此,定义可复用的 injectReducer() 函数很有帮助,该函数保存所有现有切片 reducer 的引用,并将其附加到 store 实例上。

import { createStore } from 'redux'

// Define the Reducers that will always be present in the application
const staticReducers = {
users: usersReducer,
posts: postsReducer
}

// Configure the store
export default function configureStore(initialState) {
const store = createStore(createReducer(), initialState)

// Add a dictionary to keep track of the registered async reducers
store.asyncReducers = {}

// Create an inject reducer function
// This function adds the async reducer, and creates a new combined reducer
store.injectReducer = (key, asyncReducer) => {
store.asyncReducers[key] = asyncReducer
store.replaceReducer(createReducer(store.asyncReducers))
}

// Return the modified store
return store
}

function createReducer(asyncReducers) {
return combineReducers({
...staticReducers,
...asyncReducers
})
}

现在只需调用 store.injectReducer 即可向 store 添加新 reducer。

使用 "Reducer 管理器"

另一种方案是创建 "Reducer 管理器" 对象,它跟踪所有已注册的 reducer 并暴露 reduce() 函数。参考以下示例:

export function createReducerManager(initialReducers) {
// Create an object which maps keys to reducers
const reducers = { ...initialReducers }

// Create the initial combinedReducer
let combinedReducer = combineReducers(reducers)

// An array which is used to delete state keys when reducers are removed
let keysToRemove = []

return {
getReducerMap: () => reducers,

// The root reducer function exposed by this object
// This will be passed to the store
reduce: (state, action) => {
// If any reducers have been removed, clean up their state first
if (keysToRemove.length > 0) {
state = { ...state }
for (let key of keysToRemove) {
delete state[key]
}
keysToRemove = []
}

// Delegate to the combined reducer
return combinedReducer(state, action)
},

// Adds a new reducer with the specified key
add: (key, reducer) => {
if (!key || reducers[key]) {
return
}

// Add the reducer to the reducer mapping
reducers[key] = reducer

// Generate a new combined reducer
combinedReducer = combineReducers(reducers)
},

// Removes a reducer with the specified key
remove: key => {
if (!key || !reducers[key]) {
return
}

// Remove it from the reducer mapping
delete reducers[key]

// Add the key to the list of keys to clean up
keysToRemove.push(key)

// Generate a new combined reducer
combinedReducer = combineReducers(reducers)
}
}
}

const staticReducers = {
users: usersReducer,
posts: postsReducer
}

export function configureStore(initialState) {
const reducerManager = createReducerManager(staticReducers)

// Create a store with the root reducer function being the one exposed by the manager.
const store = createStore(reducerManager.reduce, initialState)

// Optional: Put the reducer manager on the store so it is easily accessible
store.reducerManager = reducerManager
}

现在可以通过调用 store.reducerManager.add("asyncState", asyncReducer) 添加新 reducer。

现在可以通过调用 store.reducerManager.remove("asyncState") 移除 reducer。

Redux Toolkit

Redux Toolkit 2.0 包含专门用于简化 reducer 和中间件代码分割的实用工具,包括完善的 TypeScript 支持(懒加载 reducer 和中间件的常见挑战)。

combineSlices

combineSlices 工具旨在简化 reducer 注入。它还能取代 combineReducers,因为它可以将多个切片和 reducer 组合成单个根 reducer。

初始化时它接收一组切片和 reducer 映射,返回带有注入方法的 reducer 实例。

备注

combineSlices 的"切片"通常由 createSlice 创建,但也可以是任何具有 reducerPathreducer 属性的"类切片"对象(意味着 RTK Query API 实例也兼容)。

const withUserReducer = rootReducer.inject({
reducerPath: 'user',
reducer: userReducer
})

const withApiReducer = rootReducer.inject(fooApi)

为简洁起见,本文档中将这种 { reducerPath, reducer } 结构称为"切片"。

切片会挂载在其 reducerPath 下,reducer 映射对象中的项则挂载在各自键名下。

const rootReducer = combineSlices(counterSlice, baseApi, {
user: userSlice.reducer,
auth: authSlice.reducer
})
// is like
const rootReducer = combineReducers({
[counterSlice.reducerPath]: counterSlice.reducer,
[baseApi.reducerPath]: baseApi.reducer,
user: userSlice.reducer,
auth: authSlice.reducer
})
注意

注意避免命名冲突——后定义的键会覆盖先前的键,但 TypeScript 无法处理这种情况。

切片注入

要注入切片,应在 combineSlices 返回的 reducer 实例上调用 rootReducer.inject(slice)。这会将切片按其 reducerPath 注入到 reducer 集合中,并返回类型化的组合 reducer 实例(该实例已感知切片注入)。

或者,你也可以调用 slice.injectInto(rootReducer),它会返回一个已注入的切片实例。你甚至可以同时使用两种方式,因为每次调用都会返回有用的内容,而且 combineSlices 允许在相同的 reducerPath 下重复注入相同的 reducer 实例而不会产生问题。

const withCounterSlice = rootReducer.inject(counterSlice)
const injectedCounterSlice = counterSlice.injectInto(rootReducer)

典型 reducer 注入与 combineSlice 的"元 reducer"方法的关键区别在于:combineSlice 永远不会调用 replaceReducer。传递给 store 的 reducer 实例始终保持不变。

这样做的结果是:注入切片时不会分发任何 action,因此注入的切片状态不会立即显示在 state 中。只有当有 action 被分发时,该状态才会出现在 store 的 state 中。

不过,为了避免选择器处理可能为 undefined 的状态,combineSlices 提供了一些实用的选择器工具函数

声明懒加载切片

为了让懒加载切片出现在推断的状态类型中,提供了 withLazyLoadedSlices 辅助函数。这允许你声明计划稍后注入的切片,使它们在状态类型中显示为可选属性。

要完全避免将懒加载切片导入组合 reducer 的文件,可以使用模块增强(module augmentation)。

// file: reducer.ts
import { combineSlices } from '@reduxjs/toolkit'
import { staticSlice } from './staticSlice'

export interface LazyLoadedSlices {}

export const rootReducer =
combineSlices(staticSlice).withLazyLoadedSlices<LazyLoadedSlices>()

// file: 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 utility assumes reducer is under slice.reducerPath
export interface LazyLoadedSlices extends WithSlice<typeof counterSlice> {}

// if it's not, just use a normal 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 - initial state used if not in store
)

切片的"已注入"实例对切片选择器执行相同操作——如果传入的状态中不存在,将提供初始状态。

const injectedCounterSlice = counterSlice.injectInto(rootReducer)

console.log(counterSlice.selectors.selectValue({})) // runtime error
console.log(injectedCounterSlice.selectors.selectValue({})) // 0

典型用法

combineSlices 的设计使得切片在需要时立即注入(例如从已加载组件导入选择器或 action 时)。

这意味着典型用法类似以下模式:

// file: reducer.ts
import { combineSlices } from '@reduxjs/toolkit'
import { staticSlice } from './staticSlice'

export interface LazyLoadedSlices {}

export const rootReducer =
combineSlices(staticSlice).withLazyLoadedSlices<LazyLoadedSlices>()

// file: store.ts
import { configureStore } from '@reduxjs/toolkit'
import { rootReducer } from './reducer'

export const store = configureStore({ reducer: rootReducer })

// file: 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

// file: Counter.tsx
// by importing from counterSlice we guarantee
// the injection happens before this component is defined
import { increment, selectValue } from './counterSlice'
import { useAppDispatch, useAppSelector } from './hooks'

export default function Counter() {
const dispatch = usAppDispatch()
const value = useAppSelector(selectValue)
return (
<>
<p>{value}</p>
<button onClick={() => dispatch(increment())}>Increment</button>
</>
)
}

// file: App.tsx
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 = React.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)

// middleware chain is now [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)

// middleware chain is now [logger, thunk, logger]

withMiddleware

withMiddleware 是一个 action 创建函数,分发时会添加包含的所有中间件,并返回带有附加扩展的预定义类型 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 钩子时。

createDispatchWithMiddlewareHookFactory

此方法接收 React 上下文实例,并创建使用该上下文的 createDispatchWithMiddlewareHook 实例(详见提供自定义上下文)。

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])
}

第三方库与框架

以下优质外部库可自动实现上述功能: