Skip to main content

代码结构

Redux 常见问题:代码结构

我的文件结构应该是什么样的?我应该如何在项目中组织 action creators 和 reducers?selectors 应该放在哪里?

由于 Redux 仅仅是一个数据存储库,它并没有对项目应该如何组织有直接的规定。不过,大多数 Redux 开发者倾向于采用以下一些常见的模式:

  • Rails 风格:将 “actions”、“constants”、“reducers”、“containers” 和 “components” 分别放在不同的文件夹
  • “功能文件夹” / “领域” 风格:按功能或领域划分文件夹,可能在每个文件夹内按文件类型再细分子文件夹
  • “Ducks/Slices”:类似领域风格,但明确将 actions 和 reducers 结合起来,通常会在同一文件中定义它们

一般建议 selectors 与 reducers 一起定义并导出,然后在其他地方重用(比如在 mapStateToProps 函数中,在异步 action creators 或 sagas 中使用),以便将所有知道状态树具体形状的代码都放在 reducer 文件中。

tip

我们特别推荐将逻辑组织成“功能文件夹”,将某个功能相关的所有 Redux 逻辑放在一个“slice/ducks”文件中

示例请参见本节:

详细说明:示例文件结构

一个示例文件结构可能如下所示:

  • /src
    • index.tsx:入口文件,渲染 React 组件树
    • /app
      • store.ts:store 配置
      • rootReducer.ts:根 reducer(可选)
      • App.tsx:根 React 组件
    • /common:hooks、通用组件、工具函数等
    • /features:包含所有“功能文件夹”
      • /todos:单个功能文件夹
        • todosSlice.ts:Redux reducer 逻辑及相关 actions
        • Todos.tsx:React 组件

/app 包含整个应用的设置和依赖其他文件夹的布局代码。

/common 包含真正通用且可复用的工具和组件。

/features 中的文件夹包含与某个具体功能相关的所有功能代码。在这个示例里,todosSlice.ts 是一个“duck”风格文件,包含对 RTK 的 createSlice() 函数的调用,并导出切片 reducer 和 action creators。

虽然最终你如何在磁盘上组织代码并不重要,但需要牢记 actions 和 reducers 不应被孤立看待。完全可以(且鼓励)在某个文件夹定义的 reducer 响应在另一个文件夹定义的 action。

进一步信息

文档

相关文章

讨论

我应该如何在 reducers 和 action creators 之间拆分逻辑?“业务逻辑”应该放哪里?

并没有唯一清晰的答案告诉你哪些逻辑应该放在 reducer,哪些应该放在 action creator。一些开发者倾向于编写“肥”的 action creators,搭配“瘦”的 reducers,它们只是简单地把 action 中的数据合并进对应状态。另一些则倾向于将 actions 尽可能保持精简,尽量减少在 action creator 中使用 getState()。(本问题中,其他异步做法比如 sagas 和 observables 都归类为“action creator”范畴。)

将更多逻辑放在 reducers 里可能有如下几个好处。首先,action 类型将更加语义化和有意义(比如 "USER_UPDATED" 代替 "SET_STATE")。其次,将更多逻辑放在 reducers 里可以使得更多功能受益于时间旅行调试。

下面这条评论很好地总结了这种二分法:

问题在于应该把什么放到 action creator,什么放到 reducer 里,是选择“肥”action 还是“瘦”action。如果把所有逻辑都放到 action creator,你最终会得到“肥”action objects,它们基本上声明对状态的更新。Reducers 会变得简单、纯粹,只是做加、删、改的操作,且易于组合。但你的业务逻辑大多不会在这里体现。 如果把更多逻辑放到 reducer,你会得到“瘦”action objects,业务逻辑大多集中在一个地方,但 reducer 变得难以组合,因为你可能需要其他分支的信息。你最终会得到大型 reducer 或者需要从更高状态层级传入附加参数的 reducer。

tip

我们建议尽可能多的逻辑放到 reducers 中。有时你可能需要一些逻辑在 action 创建之前帮忙准备数据,但大部分工作应该由 reducers 来完成。

进一步信息

文档

相关文章

讨论

为什么要使用 action creators?

Redux 并不要求必须使用 action creators。你可以用任何适合你的方式创建 actions,包括简单地把对象字面量传递给 dispatch。action creators 来源于 Flux 架构,并被 Redux 社区采纳,因为它们带来了若干优势。

action creators 更易于维护。对一个 action 的更新可以集中操作,一处修改全局生效。所有该 action 的实例都确保形状相同且拥有相同的默认值。

action creators 可测试。内联 action 的正确性必须手动验证,而 action creator 像普通函数一样,可以编写一次测试自动运行。

action creators 易于文档化。action creator 的参数体现了 action 的依赖。将 action 定义集中起来,为注释和文档提供了便捷位置。内联的 action 难以捕获和传达这些信息。

action creators 是更强大的抽象层。创建一个 action 往往需对数据进行转换或执行 AJAX 请求。action creator 提供了统一接口隐藏这些细节。这一抽象让组件发送 action 时不必关心其创建过程的复杂性。

进一步信息

相关文章

讨论

websocket 及其他持久连接应该放在哪里?

Middleware 是在 Redux 应用中处理 websocket 等持久连接的正确位置,原因如下:

  • Middleware 生命周期与应用相同
  • 类似于 store,整个应用通常只需要一个连接实例
  • Middleware 能监听所有 dispatch 的 action,也能自己 dispatch action。这样 middleware 可以将 dispatch 的 action 转成 websocket 发送的消息,收到 websocket 消息时再 dispatch 新的 action
  • websocket 连接实例不可序列化,因此不适合放到 store state 中

请参阅这个示例了解如何让 socket middleware 和 Redux action 交互。

市面上有许多 websocket 及类似连接的 middleware 现成可用,见下方链接。

如何在非组件文件中使用 Redux store?

每个应用应该只有唯一一个 Redux store,从应用架构角度看,它是单例。当和 React 一起使用时,store 通过在根组件 <App> 外层包裹 <Provider store={store}> 实现注入,因此只有应用启动配置代码需要直接引入 store。

但有时代码库的其他部分也需要与 store 交互。

你应该避免在其他代码文件中直接导入 store。虽然有时候可以用,但常常会导致循环引用错误。

一些解决方案包括:

  • 将依赖 store 的逻辑写成 thunk,在组件中 dispatch 该 thunk
  • dispatch 的引用从组件传给相关函数作为参数
  • 将逻辑写成 middleware,在 store 配置时加入
  • 在应用创建时,把 store 实例注入相关文件

一个常见场景是在 Axios 拦截器中读取存储在 Redux state 中的 API 授权信息(比如 token)。拦截器文件需要引用 store.getState(),但同时要被 API 层文件导入,这会导致循环导入。

你可以在拦截器文件中导出一个 injectStore 函数,如下:

common/api.js
let store

export const injectStore = _store => {
store = _store
}

axiosInstance.interceptors.request.use(config => {
config.headers.authorization = store.getState().auth.token
return config
})

然后在入口文件中将 store 注入 API 配置:

index.js
import store from './app/store'
import { injectStore } from './common/api'
injectStore(store)

这样,只有应用启动配置部分需要导入 store,避免了文件依赖图中的循环依赖。