Skip to main content

代码拆分

在大型 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 实例。

note

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

注意避免命名冲突 —— 后面的 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 会将中间件实例追加到由动态中间件实例处理的中间件链中。中间件按注入顺序应用,且通过函数引用存储(相同中间件无论注入多少次只应用一次)。

note

重要的是,所有注入的中间件都包含在原始动态中间件实例内部。

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

中间件在调用 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])
}