跳至主内容

代码结构

非官方测试版翻译

本页面由 PageTurner AI 翻译(测试版)。未经项目官方认可。 发现错误? 报告问题 →

Redux FAQ:代码结构

项目文件结构应该如何组织?如何分组 action 创建函数和 reducer?选择器应该放在哪里?

由于 Redux 只是一个数据存储库,它对项目结构没有硬性要求。不过,大多数 Redux 开发者通常会采用以下几种常见模式:

  • Rails 风格:分别为 "actions"、"constants"、"reducers"、"containers" 和 "components" 创建独立文件夹

  • "功能文件夹"/"领域"风格:为每个功能或领域创建独立文件夹,可能按文件类型划分子文件夹

  • "Ducks/Slices"模式:类似领域风格,但将 action 和 reducer 显式绑定,通常在同一个文件中定义

通常建议将选择器与 reducer 一同定义并导出,然后在其他地方重用(例如在 mapStateToProps 函数、异步 action 创建函数或 sagas 中),这样所有了解状态树实际结构的代码都集中在 reducer 文件中。

技巧

我们特别推荐将逻辑组织到"功能文件夹"中,并将特定功能的所有 Redux 逻辑放在单个"slice/ducks"文件中

查看以下示例:

Detailed Explanation: Example Folder Structure

An example folder structure might look something like:

  • /src
    • index.tsx: Entry point file that renders the React component tree
    • /app
      • store.ts: store setup
      • rootReducer.ts: root reducer (optional)
      • App.tsx: root React component
    • /common: hooks, generic components, utils, etc
    • /features: contains all "feature folders"
      • /todos: a single feature folder
        • todosSlice.ts: Redux reducer logic and associated actions
        • Todos.tsx: a React component

/app contains app-wide setup and layout that depends on all the other folders.

/common contains truly generic and reusable utilities and components.

/features has folders that contain all functionality related to a specific feature. In this example, todosSlice.ts is a "duck"-style file that contains a call to RTK's createSlice() function, and exports the slice reducer and action creators.

虽然代码在磁盘上的布局方式并不重要,但必须注意:不应孤立地考虑 action 和 reducer。完全可能(且鼓励)让定义在某文件夹中的 reducer 响应定义在其他文件夹中的 action。

扩展阅读

文档

文章

讨论

如何在 reducer 和 action creator 之间划分逻辑?业务逻辑应放在哪里?

关于逻辑片段应该放在 reducer 还是 action creator 中,并没有唯一明确的答案。有些开发者倾向于使用"胖" action creator 和"瘦" reducer,即 reducer 只是简单接收 action 中的数据并直接合并到对应状态。另一些开发者则尽量保持 action 精简,并减少在 action creator 中使用 getState()。(就本问题而言,sagas 和 observables 等其他异步方法也属于"action creator"范畴。)

将更多逻辑放入 reducer 有几个潜在优势:action 类型可能更具语义化且更有意义(例如使用 "USER_UPDATED" 而非 "SET_STATE")。此外,reducer 中包含更多逻辑意味着时间旅行调试将影响更多功能。

以下评论精辟地总结了这种二分法:

现在的问题在于如何在 action creator 和 reducer 之间分配内容,本质上是在胖 action 对象和瘦 action 对象之间做选择。如果把所有逻辑都放在 action creator 中,最终会得到臃肿的 action 对象——它们基本是在声明状态更新。Reducer 会变得纯粹、简单,只做"添加这个"、"删除那个"、"更新这些"的操作,这样更容易组合,但业务逻辑基本不会出现在这里。 如果把更多逻辑放进 reducer,你会得到简洁的 action 对象,大部分数据逻辑集中在一处,但由于可能需要其他分支的信息,reducer 会变得难以组合。最终你会得到庞大的 reducer,或者需要从状态树更高层级获取额外参数的 reducer。

技巧

我们建议将尽可能多的逻辑放入 reducer。虽然有时需要在前置逻辑中准备 action 内容,但 reducer 应该承担主要工作。

扩展阅读

文档

文章

讨论

为什么应该使用动作创建器?

Redux 并不强制要求使用动作创建器。您完全可以根据需要自由创建动作,包括直接向 dispatch 传递对象字面量。动作创建器源于 Flux 架构,并被 Redux 社区采纳,因为它们具有以下优势:

动作创建器更易于维护。动作的更新只需在一处进行即可全局生效,所有动作实例都能保证具有相同的结构和默认值。

动作创建器可测试性更强。内联动作的正确性需要手动验证,而动作创建器可以像普通函数一样编写测试并自动运行。

动作创建器更易于文档化。其参数清晰展示了动作的依赖项,集中化的动作定义也为文档注释提供了便利位置。使用内联动作时,这些信息更难记录和传达。

动作创建器是更强大的抽象层。创建动作常涉及数据转换或 AJAX 请求,动作创建器为这些多样化逻辑提供了统一接口。这种抽象让组件可以专注于派发动,而无需了解动作创建的具体细节。

扩展阅读

文章

讨论

WebSocket 等持久化连接应该放在哪里?

在 Redux 应用中,中间件是处理 WebSocket 等持久连接的理想选择,原因如下:

  • 中间件的生命周期与整个应用保持一致

  • 如同 store 本身,整个应用通常只需要单个连接实例

  • 中间件能捕获所有派发的 action 并自行派发新 action。这意味着中间件可以将派发的 action 转换为 WebSocket 消息发送,并在收到 WebSocket 消息时派发新 action。

  • WebSocket 连接实例不可序列化,因此不应将其放入 store 状态中

参考此示例,了解 socket 中间件如何响应 Redux action。

现有许多支持 WebSocket 的中间件库,详见下方链接。

相关库

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

每个应用应仅包含单一 Redux store,这使其成为应用架构中的单例。在 React 中使用时,通过在根组件 <App> 外包裹 <Provider store={store}> 在运行时注入 store,因此只有应用初始化逻辑需要直接导入 store。

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

应避免在其他文件中直接导入 store。虽然某些场景下可行,但这常会导致循环导入依赖错误。

可行的解决方案包括:

  • 将依赖 store 的逻辑编写为 thunk,通过组件派发该 thunk

  • 将组件的 dispatch 引用作为参数传递给相关函数

  • 将逻辑编写为中间件并在初始化时添加到 store

  • 在应用创建时向相关文件注入 store 实例

常见场景是在 Axios 拦截器中读取 Redux 状态中的 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
})

在入口文件中向 API 设置层注入 store:

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

此方案确保仅应用初始化代码需要导入 store,文件依赖链避免了循环依赖问题。