内容概要
回顾真实 React+Redux 应用各部分协同工作与优化要点
那信息量很大,但你已经完成了!希望你现在对一个更真实的 React+Redux 应用中各个部分如何协同工作有了更好的理解,而不仅仅是一个简单的待办事项列表示例。
让我们回顾一下你现在应该已经熟悉的几个关键点:
项目规划与设置
Create-React-App 是创建新 React 项目的绝佳工具,它自带良好的构建配置,并将构建过程抽象化,让你无需关心细节。
Yarn 包管理器支持“离线镜像”功能。你可以将 NPM 包的 tarball 文件保存到代码仓库中的某个文件夹,并提交该文件夹。这样,当其他人克隆仓库时,就能获得与你完全一致的包版本,安装速度更快,且在离线环境或不同操作系统平台上都能正常工作。
虽然 Create-React-App 本身不包含 Redux,但我们很容易在 CRA 项目中集成 Redux。
我们还可以利用 Webpack 的“热模块替换”功能,在保存更改时动态替换文件,而无需刷新整个页面。
UI 布局与项目结构
Semantic-UI-React 是 Semantic-UI UI 工具包针对 React 的优秀适配版本。它并不适合所有应用,但如果你想要一个易于使用、外观良好的工具包(或者只是想替代 Bootstrap),它是个非常好的选择。
我们可以通过跟踪类似“当前选中标签页”这样的“UI 状态”值来控制 UI 的显示内容。遵循“容器/展示组件”模式后,当你需要在组件状态、Redux 或其他数据源之间切换时,会更容易调整数据来源。展示组件本身并不关心数据从哪里来,它只根据父组件传入的 props 来渲染界面。
项目中文件和文件夹的组织方式没有唯一正确答案。但我更倾向于采用“以功能为先”的结构风格。最终,你应该选择最有助于团队成员理解代码位置的方式。
我也推荐使用绝对导入路径,例如 import TabBarContainer from "features/tabs/TabBarContainer",而不是 import TabBarContainer from "../../tabs/TabBarContainer"。相对路径修复起来很麻烦,所以我只在最多跨一级目录时才使用相对路径。
存储“当前激活标签页”这类 UI 状态,放在 React 组件中是可以接受的;当然也可以将其放入 Redux 中。
使用 Redux-ORM
Redux 团队建议将嵌套或关联的数据以“规范化”结构存储,即使用对象作为查找表,以便通过类型和 ID 快速定位项。
虽然可以使用纯 JavaScript 代码管理规范化数据,包括借助 Normalizr 等工具,但我强烈推荐使用 Redux-ORM 作为操作 Redux 存储中规范化数据的抽象层。
使用 Redux-ORM 时,你需要扩展其 Model 类,声明普通数据字段以及模型之间的关系(使用类似数据库的术语)。然后为整个应用创建一个单一的 ORM 实例,并通过 orm.register() 注册 Model 类。最后添加一个 reducer,作为“类数据库状态”或“实体”部分的 Redux store。
标准的 Redux-ORM 使用方式是调用 orm.session(state.entities) 创建一个 Session 实例,然后通过该 Session 访问注册的 Model 类。Model 实例充当对存储中原始 JS 数据的代理,而 Session 内部执行不可变更新。QuerySets 用于过滤集合并获取关联字段的结果。
在我的项目中,我编写了一些记忆化的 selector 函数,减少了每次更新时创建新的 Session 实例的次数,作为微小的性能优化。此外,也可以编写通用的“实体 reducer”,通过类名查找对应的 Model 类型,通过 ID 获取实例,从而实现对任意 Model 类型的处理。
我还为所有 Model 类添加了自定义行为,包括序列化和反序列化原始 JS 数据的功能,生成新实例,级联删除相关数据,以及在同一个 Model 实例的不同版本之间双向复制数据。
加载与显示数据
为了简化客户端开发流程,可以编写一个模拟 API,通过函数返回带有假数据的 Promise。
我们可以使用自定义的 parse() 反序列化函数,帮助将获取的数据加载到 Redux store 中。这些函数本质上只是调用 Model.create() 或 Model.upsert()。
Redux 团队非常鼓励使用独立的“切片 reducer”函数,它们都响应同一个分发的动作,每个切片 reducer 只负责更新自己的状态片段。
我们可以使用 Redux-ORM 的类作为“selector”,在连接组件的 mapState 函数中从 store 中查找所需的数据。这包括获取相关的模型类,并将其中的部分值包含在 mapState 的结果中。
我们只需存储被选中列表项的 ID,而不是整个项。在渲染列表时,将当前选中 ID 与每个项的 ID 进行比较,从而高亮选中行。这个 ID 也可用于详情表单,显示所选项的完整内容。
连接列表、表单与性能优化
建议在合适的地方连接应用中的任何组件。一个很好的例子是同时连接父级列表组件和子级列表项组件。最佳做法是让父级列表将项的 ID 传递给已连接的子级列表项。每个列表项则通过 mapState 的 ownProps 参数来查找 store 中正确的项。
React+Redux 的性能基于对象引用比较。每当动作被分发时,连接的组件都会重新运行 mapState 函数,如果 mapState 返回的任何字段与上次返回的结果不满足 === 相等,则会重新渲染实际包裹的组件。这意味着使用 map() 这类会创建新数组引用的操作,会导致组件每次都重新渲染。同样,使用 Immutable.js 的 toJS() 函数性能较差,因为它开销较大且会产生新的引用。
可以通过以下方式优化 React+Redux 性能:将数据以规范化形式存储,使用记忆化 selector 函数,连接更多组件,并确保每个组件在 mapState 中仅读取其真正需要的数据。
受控输入是使用 React 时必须理解的重要概念。虽然使用受控输入可能需要多写一些代码,但它能让数据处理更加简单清晰。
输入变化事件的结构有多种变体。编写一个工具函数来提取值并返回对象形式,会很有帮助。
我们可以在应用中跟踪如“是否处于编辑状态”之类的标志,并据此启用或禁用表单字段和按钮。
表单变更处理、数据编辑与性能优化
类组件和函数组件各有用途。我倾向于使用函数组件作为视觉信号,表明“这个组件只是输入 props,输出 UI”。虽然可以在函数组件内部定义回调函数,但如果需要处理事件,这通常意味着是时候将该组件转为类组件了。
将表单连接到 Redux 后,文本框等输入项可能会触发过多动作(比如每输入一个字符就触发一次动作)。可以通过封装组件,将变更暂存于其自身状态中,等到用户输入完成后再统一分发 Redux 动作。React 提供的 React.Children 和 React.cloneElement() 等 API,使得组件能够返回渲染出的子组件的新版本。
尽管独立的切片 reducer 是最推荐的做法,但有时也需要多个“顶层” reducer 函数,它们作用于整个状态树而非特定切片。可以使用类似 reduceReducers 的函数按顺序调用多个 reducer,或自行编写自定义逻辑。这在多个“功能 reducer”需要共同操作同一状态切片(如 state.entities)时特别有用。
我们可以使用 Redux-ORM 创建一个通用的“实体更新” reducer,根据类型和 ID 更新任意模型。
表单草稿数据管理
通过使用记忆化 selector,可以略微提升性能:确保每次 state.entities 切片更新时,只创建一个 Redux-ORM Session 实例,而不是在每个 mapState 重新运行时都创建新的实例。
我们还可以添加通用的“删除模型”和“创建模型” reducer 函数,类似于“更新” reducer。
尽管这些“实体 CRUD 操作”是通用的,但其他功能模块可以通过监听相同动作来实现特定行为。
如果你需要编辑某项的属性,建议将该项复制到状态中的一个单独的“草稿 / 编辑中”区域。这样可以在不影响其他 UI 显示原始值的前提下进行编辑。同时,也便于取消或重置编辑,甚至在其他地方复用编辑中的值。
我们可以实现通用逻辑:开始编辑时,将当前实体复制到“编辑中”区域;编辑过程中持续更新;停止编辑时清除。同时可以复用底层的“实体 CRUD”逻辑,构建更高层次的编辑逻辑。
要保存编辑内容,需将“草稿区”的数据复制回“当前实体”区域。可以通过为每个切片创建独立的 Redux-ORM Session,并实现 Model 方法来完成实例间的数据复制。
管理模态框与上下文菜单
React 中的模态框通常通过“门户”技术实现,即向主页面附加一个额外元素,使模态框浮在其他 UI 之上。React 15 提供了一个“不稳定”的门户 API,而 React 16 则引入了官方支持的 API。react-portal 库简化了门户的使用。
使用 Redux 控制模态框的典型方法是创建描述性对象,说明应显示哪种对话框组件及其属性,并将这些描述存入 Redux store。然后由一个组件读取这些描述,从查找表中获取对应的模态组件,并通过门户渲染。通过存储描述数组,可以同时显示多个模态框,只需遍历数组渲染多个组件即可。
某些模态框需要“返回”值,比如颜色选择器。不建议将非可序列化值(如函数)放入 Redux store。更好的做法是:请求显示对话框的代码预先提供一个“结果动作”作为模态框的 prop,模态框关闭后触发该动作。也有 Redux 中间件库支持在显示模态框时返回一个 Promise,并在模态框关闭时解析该 Promise。
其他 UI 组件如上下文菜单和弹出式提示通知,也可以采用相同的策略:在 Redux 中使用描述性对象 + 一个 React 组件进行渲染。
上下文菜单本质上就是一个绝对定位的模态框。可以使用包装组件来处理任意上下文菜单组件的绝对定位。
显示上下文菜单时,可以利用事件中的鼠标坐标来确定菜单的位置。