代码分割
¥Code Splitting
在大型 Web 应用中,通常需要将应用代码拆分为多个可以按需加载的 JS 包。此策略称为 '代码分割',通过减少必须获取的初始 JS 有效负载的大小,有助于提高应用的性能。
¥In large web applications, it is often desirable to split up the app code into multiple JS bundles that can be loaded on-demand. This strategy, called 'code splitting', helps to increase performance of your application by reducing the size of the initial JS payload that must be fetched.
为了使用 Redux 进行代码分割,我们希望能够动态地将 reducer 添加到存储中。然而,Redux 实际上只有一个根 reducer 功能。该根 reducer 通常是在应用初始化时通过调用 combineReducers()
或类似函数来生成的。为了动态添加更多 reducer,我们需要再次调用该函数来重新生成根 reducer。下面,我们讨论解决此问题的一些方法,并参考提供此功能的两个库。
¥To code split with Redux, we want to be able to dynamically add reducers to the store. However, Redux really only has a single root reducer function. This root reducer is normally generated by calling combineReducers()
or a similar function when the application is initialized. In order to dynamically add more reducers, we need to call that function again to re-generate the root reducer. Below, we discuss some approaches to solving this problem and reference two libraries that provide this functionality.
基本原则
¥Basic Principle
使用 replaceReducer
¥Using replaceReducer
Redux 存储公开了 replaceReducer
函数,该函数用新的根 reducer 函数替换当前活动的根 reducer 函数。调用它将交换内部 reducer 函数引用,并分派一个操作来帮助任何新添加的切片 reducer 初始化自身:
¥The Redux store exposes a replaceReducer
function, which replaces the current active root reducer function with a new root reducer function. Calling it will swap the internal reducer function reference, and dispatch an action to help any newly-added slice reducers initialize themselves:
const newRootReducer = combineReducers({
existingSlice: existingSliceReducer,
newSlice: newSliceReducer
})
store.replaceReducer(newRootReducer)
Reducer 注入方法
¥Reducer Injection Approaches
本节将介绍一些用于注入reducer的手写秘诀。
¥This section will cover some handwritten recipes used to inject reducers.
定义 injectReducer
函数
¥Defining an injectReducer
function
我们可能希望从应用中的任何位置调用 store.replaceReducer()
。因此,定义一个可重用的 injectReducer()
函数来保留对所有现有切片缩减程序的引用并将其附加到存储实例会很有帮助。
¥We will likely want to call store.replaceReducer()
from anywhere in the application. Because of that, it's helpful
to define a reusable injectReducer()
function that keeps references to all of the existing slice reducers, and attach
that to the store instance.
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
即可将新的 reducer 添加到存储中。
¥Now, one just needs to call store.injectReducer
to add a new reducer to the store.
使用 'reducer 管理器'
¥Using a 'Reducer Manager'
另一种方法是创建一个 'reducer 管理器' 对象,该对象跟踪所有已注册的 reducer 并公开 reduce()
函数。考虑以下示例:
¥Another approach is to create a 'Reducer Manager' object, which keeps track of all the registered reducers and exposes a reduce()
function. Consider the following example:
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
}
要添加新的 reducer,现在可以调用 store.reducerManager.add("asyncState", asyncReducer)
。
¥To add a new reducer, one can now call store.reducerManager.add("asyncState", asyncReducer)
.
要删除 reducer,现在可以调用 store.reducerManager.remove("asyncState")
¥To remove a reducer, one can now call store.reducerManager.remove("asyncState")
Redux 工具包
¥Redux Toolkit
Redux Toolkit 2.0 包含一些旨在简化使用 Reducer 和中间件进行代码拆分的实用程序,包括可靠的 Typescript 支持(延迟加载的 Reducer 和中间件的常见挑战)。
¥Redux Toolkit 2.0 includes some utilities designed to simplify code splitting with reducers and middleware, including solid Typescript support (a common challenge with lazy loaded reducers and middleware).
combineSlices
combineSlices
实用程序旨在允许轻松注入 Reducer。它还取代了 combineReducers
,因为它可用于将多个切片和reducer组合成一个根reducer。
¥The combineSlices
utility is designed to allow for easy reducer injection. It also supercedes combineReducers
, in that it can be used to combine multiple slices and reducers into one root reducer.
在设置时,它接受一组切片和 Reducer 映射,并返回带有附加方法的 Reducer 实例以供注入。
¥At setup it accepts a set of slices and reducer maps, and returns a reducer instance with attached methods for injection.
combineSlices
的 "slice" 通常使用 createSlice
创建,但可以是任何具有 reducerPath
和 reducer
属性的 "slice-like" 对象(这意味着 RTK 查询 API 实例也兼容)。
¥A "slice" for combineSlices
is typically created with createSlice
, but can be any "slice-like" object with reducerPath
and reducer
properties (meaning RTK Query API instances are also compatible).
const withUserReducer = rootReducer.inject({
reducerPath: 'user',
reducer: userReducer
})
const withApiReducer = rootReducer.inject(fooApi)
为简单起见,此 { reducerPath, reducer }
形状将在本文档中描述为 "slice"。
¥For simplicity, this { reducerPath, reducer }
shape will be described in these docs as a "slice".
切片将安装在其 reducerPath
上,而来自 Reducer Map 对象的项目将安装在其各自的键下。
¥Slices will be mounted at their reducerPath
, and items from reducer map objects will be mounted under their respective key.
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 无法解决这个问题。
¥Be careful to avoid naming collision - later keys will overwrite earlier ones, but Typescript won't be able to account for this.
切片注入
¥Slice injection
要注入切片,你应该在从 combineSlices
返回的 Reducer 实例上调用 rootReducer.inject(slice)
。这会将其 reducerPath
下的切片注入到 Reducer 集合中,并返回组合 Reducer 的实例,该实例被输入以知道切片已被注入。
¥To inject a slice, you should call rootReducer.inject(slice)
on the reducer instance returned from combineSlices
. This will inject the slice under its reducerPath
into the set of reducers, and return an instance of the combined reducer typed to know that the slice has been injected.
或者,你可以调用 slice.injectInto(rootReducer)
,它返回一个知道它已被注入的切片实例。你甚至可能想要同时执行这两项操作,因为每次调用都会返回一些有用的东西,而 combineSlices
允许在同一个 reducerPath
上注入同一个 Reducer 实例而不会出现问题。
¥Alternatively, you can call slice.injectInto(rootReducer)
, which returns an instance of the slice which is aware it's been injected. You may even want to do both, as each call returns something useful, and combineSlices
allows injection of the same reducer instance at the same reducerPath
without issue.
const withCounterSlice = rootReducer.inject(counterSlice)
const injectedCounterSlice = counterSlice.injectInto(rootReducer)
典型的 Reducer 注入和 combineSlice
的 "meta-reducer" 方法之间的一个主要区别是 replaceReducer
永远不会被 combineSlice
调用。传递给存储的 Reducer 实例不会改变。
¥One key difference between typical reducer injection and combineSlice
's "meta-reducer" approach is that replaceReducer
is never called for combineSlice
. The reducer instance passed to the store doesn't change.
这样做的结果是,在注入切片时不会分派任何操作,因此注入的切片的状态不会立即显示。只有在分派操作时,状态才会显示在存储的状态中。
¥A consequence of this is that no action is dispatched when a slice is injected, and therefore the injected slice's state doesn't show in state immediately. The state will only show in the store's state when an action is dispatched.
然而,为了避免选择器必须考虑可能的 undefined
状态,combineSlices
包含了一些有用的 选择器实用程序。
¥However, to avoid selectors having to account for possibly undefined
state, combineSlices
includes some useful selector utilities.
声明延迟加载的切片
¥Declaring lazy loaded slices
为了使延迟加载的切片显示在推断的状态类型中,提供了 withLazyLoadedSlices
助手。这允许你声明你打算稍后注入的切片,以便它们可以在状态类型中显示为可选项。
¥In order for lazy loaded slices to show up in the inferred state type, a withLazyLoadedSlices
helper is provided. This allows you to declare slices you intend to later inject, so they can show up as optional in the state type.
为了完全避免将惰性切片导入到组合的 Reducer 文件中,可以使用模块扩充。
¥To completely avoid importing the lazy slice into the combined reducer's file, module augmentation can be used.
// 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'
})
选择器实用程序
¥Selector utilities
与 inject
一样,组合的 Reducer 实例还有一个 .selector
方法,可用于封装选择器。它将状态对象封装在 Proxy
中,并为已注入但尚未出现在状态中的任何reducer提供初始状态。
¥As well as inject
, the combined reducer instance has a .selector
method which can be used to wrap selectors. It wraps the state object in a Proxy
, and provides an initial state for any reducers which have been injected but haven't appeared in state yet.
调用 inject
的结果被输入,以知道在调用选择器时始终会定义注入的切片。
¥The result of calling inject
is typed to know that the injected slice will always be defined when the selector is called.
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
)
切片的 "injected" 实例将对切片选择器执行相同的操作 - 如果在传递的状态中不存在,将提供初始状态。
¥An "injected" instance of a slice will do the same thing for slice selectors - initial state will be provided if not present in the state passed.
const injectedCounterSlice = counterSlice.injectInto(rootReducer)
console.log(counterSlice.selectors.selectValue({})) // runtime error
console.log(injectedCounterSlice.selectors.selectValue({})) // 0
典型用法
¥Typical usage
combineSlices
的设计使得切片在需要时立即注入(即从已加载的组件导入选择器或动作)。
¥combineSlices
is designed so that the slice is injected as soon as it's needed (i.e. a selector or action is imported from a component that's been loaded in).
这意味着典型的用法将类似于以下内容。
¥This means that the typical usage will look something along the lines of the below.
// 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
实用程序创建一个 "meta-middleware",允许在存储初始化后注入中间件。
¥The createDynamicMiddleware
utility creates a "meta-middleware" which allows for injection of middleware after store initialisation.
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
将中间件实例附加到由动态中间件实例处理的中间件链中。中间件按注入顺序应用,并按函数引用存储(因此,无论注入多少次,相同的中间件都只应用一次)。
¥addMiddleware
appends the middleware instance to the chain of middlewares handled by the dynamic middleware instance. Middleware is applied in injection order, and stored by function reference (so the same middleware is only applied once regardless of how many times it's injected).
重要的是要记住,所有注入的中间件都将包含在原始动态中间件实例中。
¥It's important to remember that all middlewares injected will be contained within the original dynamic middleware instance.
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]
如果希望对顺序有更好的控制,可以使用多个实例。
¥If it's desired to have more control over the order, multiple instances can be used.
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
是一个动作创建器,当分派时,它会导致中间件添加任何包含的中间件,并返回带有任何添加扩展的预输入版本的 dispatch
。
¥withMiddleware
is an action creator which, when dispatched, causes the middleware to add any middlewares included and returns a pre-typed version of dispatch
with any added extensions.
const listenerDispatch = store.dispatch(
withMiddleware(listenerMiddleware.middleware)
)
const unsubscribe = listenerDispatch(addListener({ actionCreator, effect }))
// ^? () => void
这主要在非 React 上下文中有用。使用 React,使用 React 集成 更有用。
¥This is mainly useful in a non-React context. With React it's more useful to use the react integration.
React 集成
¥React integration
从 @reduxjs/toolkit/react
入口点导入时,动态中间件的实例将附加几个附加方法。
¥When imported from the @reduxjs/toolkit/react
entry point, the instance of dynamic middleware will have a couple of additional methods attached.
createDispatchWithMiddlewareHook
此方法调用 addMiddleware
并返回一个 useDispatch
版本,该版本被键入以了解注入的中间件。
¥This method calls addMiddleware
and returns a version of useDispatch
typed to know about the injected middleware.
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
钩子时注入的。
¥Middleware is injected when createDispatchWithMiddlewareHook
is called, not when the useDispatch
hook is called.
createDispatchWithMiddlewareHookFactory
此方法采用 React 上下文实例并创建使用该上下文的 createDispatchWithMiddlewareHook
实例。(参见 提供自定义上下文)
¥This method take a React context instance and creates an instance of createDispatchWithMiddlewareHook
which uses that context. (see Providing custom 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])
}
第三方库和框架
¥Third-party Libraries and Frameworks
有一些很好的外部库可以帮助你自动添加上述功能:
¥There are a few good external libraries out there that can help you add the above functionality automatically: