【问题标题】:Modularize a Redux Toolkit application模块化 Redux Toolkit 应用程序
【发布时间】:2022-01-20 10:55:59
【问题描述】:

问题

是否可以将依赖于 redux 存储的不同切片的基于 RTK 的应用程序的功能分离到单独的节点包中?假设是这样,最好的方法是什么?

背景

我们有一个基于 Redux Toolkit 的大型且不断增长的应用程序。在可能的情况下,我们尝试将应用程序的各个部分分离到它们自己的节点包中。我们发现这样做有很多好处,包括:

  • 代码库的可维护性
  • 对应用程序内依赖项的细粒度控制
  • 可测试性

对于横切事物(例如日志记录、http 请求、路由等)来说,这样做很容易。但我们希望更进一步,将应用程序的“功能”模块化。例如,让我们的应用程序的“地址簿”功能与“消息”功能位于不同的模块中,它们都通过“应用程序”包组合在一起。

我们在这里看到的好处是我们在其他代码库中发现的,并且已经在其他地方进行了讨论。 (例如,here for iOS)。但是,简而言之:(1)您可以查看和控制应用程序内的依赖关系。例如,您可以轻松查看“消息”功能是否依赖于“地址簿”功能,并明确决定如何通过导出的内容将一个功能公开给另一个功能; (2) 您可以构建应用程序的完全可测试的子部分,只需拥有一个仅包含您想要测试的内容的“预览”包,例如,您可以拥有一个仅依赖于“联系”功能,用于构建和测试; (3) 无需编译(TS/babel)、打包/压缩、单元测试每个部分,可以加快CI/CD时间; (4) 您可以利用各种分析工具更详细地了解每个功能的发展情况。

很可能还有其他方法可以实现这些目标,有些人可能不同意这是一个好方法的前提。这不是问题的重点,但我对它可能是最佳答案的可能性持开放态度(例如,具有丰富 Redux 经验的人可能会解释为什么这是一个坏主意)。

问题

我们一直在努力想出一个使用 Redux Toolkit 的好方法。问题似乎归结为——有没有一种好方法可以模块化(通过单独的节点包)RTK 中使用的各种“切片”? (这可能适用于其他 Redux 实现,但我们在 RTK 上投入了大量资金)。

拥有一个导出 redux 存储将使用的各种项目的包很容易,即切片状态、动作创建者、异步 thunk 和选择器。然后 RTK 将在更高级别的应用程序中很好地组合这些内容。换句话说,您可以轻松地拥有一个包含商店的“应用程序”包,然后是一个导出“联系人”切片的“联系人”包,以及它的伴随操作、thunk、选择器等。

如果您还希望 使用 slice 的该部分的组件和钩子与 slice 存在于同一包中,例如,在“联系人”包中,问题就来了。这些组件/钩子需要访问 global 调度和 global useSelector 钩子才能真正起作用,但这仅存在于“app”组件中,即将各种功能包组合在一起的功能。

考虑的可能性

  1. 我们可以从“更高”级别的“app”包中导出全局调度和 useSelector,但是我们的子组件现在依赖于更高级别的包。这意味着我们不能再构建由不同排列的子包组成的替代更高级别的包。

  2. 我们可以使用单独的商店。这在过去是 discussed 关于 Redux 的,但一直不鼓励,although there is some suggestion 如果你想实现模块化,那可能没问题。这些讨论也有些老了。

问题(再次)

是否可以将依赖于 redux 存储的不同切片的基于 RTK 的应用程序的功能分离到单独的节点包中?假设是这样,最好的方法是什么?

虽然我主要感兴趣的是是否/如何在 RTK 中完成此操作,但我也对答案感兴趣——尤其是来自在大型应用程序上使用 RTK/redux 经验的人——关于这是否不好想法以及采取了哪些其他方法来实现模块化的好处。

【问题讨论】:

    标签: redux react-redux


    【解决方案1】:

    这个问题在其他情况下也出现过,最值得注意的是如何编写需要知道给定切片的状态附加到根状态对象的位置的选择器函数。早在 2016 年,Randy Coulman 就该主题发表了一系列出色且富有洞察力的博文,并在 2018 年发布了涵盖多个相关方面的后续博文 - 请参阅 Solving Circular Dependencies in Modular Redux 获取该博文以及指向之前博文的链接。

    我的一般想法是,您需要让这些模块提供一些方法,允许注入根 dispatch 或向模块询问其提供的部分,然后在应用程序级别将它们连接在一起。我自己不需要处理任何这些,但我同意这可能是使用 Redux 的较弱方面之一,因为架构方面。

    对于一些相关的现有技术,您可能想查看这些库:

    也许也值得在 RTK“讨论”区域提出同样的问题,以便我们进一步讨论。

    【讨论】:

    • 根据您的建议,我已在 RTK 讨论中提出此问题。如果有任何进一步的发展,我也会发回给 SO。
    【解决方案2】:

    按照@markerikson 的建议,这是我想出的解决方案。基本思想涉及依赖注入模型,其中更高级别的“应用程序”包:

    • 导入“功能”包的切片
    • 用它组成一个商店
    • 调用同样从“功能”包中导出的initialize 函数,该函数注入调度和状态(实际上是包装它们的钩子,但您可以采用任何一种方式)。

    最后一部分是允许“功能”包与“应用”包保持分离的原因。

    “功能”包还具有一些类型,将根状态和应用调度接口定义为包括至少本地状态和该功能调度的类型。

    这里是关键代码,使用 create-react-app 中的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 repository 中提供了此解决方案的工作示例。

    【讨论】:

      猜你喜欢
      • 2021-07-30
      • 2011-03-27
      • 2013-08-15
      • 2014-01-25
      • 2013-06-19
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-10-01
      相关资源
      最近更新 更多