代码结构
本页面由 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:
/srcindex.tsx: Entry point file that renders the React component tree/appstore.ts: store setuprootReducer.ts: root reducer (optional)App.tsx: root React component
/common: hooks, generic components, utils, etc/features: contains all "feature folders"/todos: a single feature foldertodosSlice.ts: Redux reducer logic and associated actionsTodos.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。
扩展阅读
文档
文章
-
如何扩展 React 应用(配套演讲:扩展 React 应用)
讨论
如何在 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 函数:
let store
export const injectStore = _store => {
store = _store
}
axiosInstance.interceptors.request.use(config => {
config.headers.authorization = store.getState().auth.token
return config
})
在入口文件中向 API 设置层注入 store:
import store from './app/store'
import { injectStore } from './common/api'
injectStore(store)
此方案确保仅应用初始化代码需要导入 store,文件依赖链避免了循环依赖问题。