问题
是否可以将依赖于redux存储的不同切片的基于RTK的应用程序的功能分离为单独的节点包?如果是这样,最好的方法是什么?
背景
我们有一个基于Redux Toolkit的大型且不断增长的应用程序。在可能的情况下,我们尝试将应用程序的各个部分分离到它们自己的节点包中。我们发现这样做有很多好处,包括:
- 代码库的可维护性
- 对应用程序内部依赖关系的细粒度控制
- 可测试性
对于交叉事务,如日志记录、http请求、路由等,这样做很容易;特征";我们的应用程序。例如;地址簿";我们的应用程序的功能存在于不同的模块中,例如;消息";特征,它们都通过";应用程序";包裹
我们在这里看到的好处是我们在其他代码库中发现的,并在其他地方进行了讨论。(例如,此处适用于iOS)。但是,简而言之:(1)您可以查看和控制应用程序内部的依赖关系。例如,你可以很容易地看到;消息";特征取决于";地址簿";功能,并明确决定如何通过导出内容将一个功能暴露给另一个功能;(2) 你可以构建应用程序的完全可测试的子部分,只需拥有一个";预览";只包含你想要测试的东西的包,例如,你可以有一个";联系人应用程序";只依赖于";"联系人";用于构建和测试的功能;(3) 您可以通过不需要编译(TS/babel)、打包/缩小和单元测试每个部分来加快CI/CD时间;(4) 您可以利用各种分析工具来获得每个功能如何发展的更细粒度的图片。
很可能还有其他方法可以实现这些目标,有些人可能不同意这是一个好方法的前提。这不是问题的重点,但我对这可能是最好的答案持开放态度(例如,一些有丰富Redux经验的人可能会解释为什么这是个坏主意)。
问题
我们一直在努力想出一个好方法来使用Redux Toolkit做到这一点。问题似乎可以归结为——是否有一种好的方法来模块化(通过单独的节点包)各种";切片";用于RTK?(这可能适用于其他Redux实现,但我们在RTK方面投入了大量资金)。
有一个包可以导出redux存储将使用的各种项,即切片状态、操作创建者、异步thunks和选择器,这很容易。然后RTK将在更高级别的应用程序中很好地组合这些。换句话说,你可以很容易地拥有一个";应用程序";容纳商店的包装;联系人";导出";联系人";切片及其伴随的操作、thunks、选择器等
如果您还希望使用切片的那部分的组件和钩子与切片位于同一个包中,那么问题就来了,例如,在";联系人";包裹这些组件/钩子将需要访问全局调度和全球useSelector
钩子才能真正工作,但这只存在于";应用程序";组件,即将各种功能包组合在一起的功能。
考虑的可能性
-
我们可以从";较高的";电平";应用程序";包,但我们的子组件现在依赖于更高级别的包。这意味着我们不能再构建替代的更高级别的包来组成不同排列的子包。
-
我们可以使用单独的商店。这在过去已经讨论过关于Redux的问题,但一直被劝阻,尽管有人建议,如果你试图实现模块化,这可能是可以的。这些讨论也有些陈旧。
问题(再次)
是否可以将依赖于redux存储的不同切片的基于RTK的应用程序的功能分离为单独的节点包?如果是这样,最好的方法是什么?
虽然我主要感兴趣的是如果/如何在RTK中实现这一点,但我也感兴趣的答案——尤其是那些在大型应用程序上有RTK/redux经验的人——关于这是否是坏主意,以及采取了哪些其他方法来实现模块化的好处。
这个问题在其他上下文中也出现过,尤其是如何编写选择器函数,这些函数需要知道给定切片的状态附加到根状态对象的位置。Randy Coulman在2016年就这个话题发表了一系列出色而富有洞察力的博客文章,并在2018年发表了一篇后续文章,涵盖了几个相关方面-请参阅该文章的解决模块化Redux中的循环依赖性以及之前文章的链接。
我的总体想法是,您需要让这些模块提供一些方法,允许注入根dispatch
或向模块询问其提供的部分,然后在应用程序级别将这些部分连接在一起。我自己还没有处理过这些问题,但我同意这可能是由于架构方面的原因,使用Redux的较弱方面之一。
对于一些相关的现有技术,你可能想看看这些库:
- https://github.com/ioof-holdings/redux-dynostore(已弃用/未维护,但相关)
- https://github.com/microsoft/redux-dynamic-modules(在这一点上也可能尚未维护-似乎仍然依赖React Redux v5)
- https://github.com/fostyfost/redux-eggs(全新-作者最近刚刚在RTK的"讨论"部分发布了这篇文章)
也可能值得在RTK";讨论";区域,以便我们可以进一步讨论。
根据@markerikson的建议,以下是我提出的解决方案。基本思想涉及一个依赖注入模型,其中更高级别的"应用程序"包:
- 导入"feature"包的切片
- 用它组成一个商店
- 调用一个
initialize
函数,该函数也是从"feature"包导出的,该包注入调度和状态(实际上是包装它们的钩子,但您可以用任何一种方式)
最后一部分是允许"功能"包与"应用"包保持不耦合的原因。
"功能"包还有一些类型,将根状态和应用程序调度接口定义为至少包含该功能的本地状态和调度的类型。
以下是关键代码,使用create-react应用程序中的redux-typescript
模板作为起点,并将counter
功能提取到一个单独的包中。代码在counterSlice
模块中
// RootStateInterface is defined as including at least this slice and any other slices that
// might be added by a calling package
type RootStateInterface = { counter: CounterState } & Record<string, any>;
// A version of AppThunk that uses the RootStateInterface just defined
type AppThunkInterface<ReturnType = void> = ThunkAction<
ReturnType,
RootStateInterface,
unknown,
Action<string>
>;
// A version of use selector that includes the RootStateInterface we just defined
export let useSliceSelector: TypedUseSelectorHook<RootStateInterface> =
useSelector;
// This function would configure a "local" store if called, but currently it is
// not called, and is just used for type inference.
const configureLocalStore = () =>
configureStore({
reducer: { counter: counterSlice.reducer },
});
// Infer the type of the dispatch that would be needed for a store that consisted of
// just this slice
type SliceDispatch = ReturnType<typeof configureLocalStore>["dispatch"];
// AppDispatchInterface is defined as including at least this slices "local" dispatch and
// the dispatch of any slices that might be added by the calling package.
type AppDispatchInterface = SliceDispatch & ThunkDispatch<any, any, any>;
export let useSliceDispatch = () => useDispatch<AppDispatchInterface>();
// Allows initializing of this package by a calling package with the "global"
// dispatch and selector hooks of that package, provided they satisfy this packages
// state and dispatch interfaces--which they will if the imported this package and
// used it to compose their store.
export const initializeSlicePackage = (
useAppDispatch: typeof useSliceDispatch,
useAppSelector: typeof useSliceSelector
) => {
useSliceDispatch = useAppDispatch;
useSliceSelector = useAppSelector;
};
此rush存储库中提供了此解决方案的一个工作示例。