React 软件架构设计和最佳实践
tao-of-react
最佳实践的一些原则和建议
- Components
- Favor Functional Components
- Write Consistent Components
- Name Components
- Organize Helper Functions
- Don’t Hardcode Markup
- Component Length
- Component Length
- Write Comments
- Use Error Boundaries
- Destructure Props
- Number of Props
- Use Objects Instead of Primitives
- Conditional Rendering
- Avoid Nested Ternary Operators
- Move Lists in Components
- Assign Default Props When Destructuring
- Avoid Nested Render Functions
- Prefer Hooks
- State Management
- Use Reducers
- Use Data Fetching Libraries
- State Management Libraries
- Component Mental models
- Container and Presentational
- Stateless and Stateful
- Application Structure
- Group by Route/Module
- Create a Common Module
- Use Absolute Paths
- Wrap External Components
- Move Components in Folders
- Performance
- Don’t Optimize Prematurely
- Watch Bundle Size
- Rerenders - Callbacks, Arrays and Objects
- Testing
- Don’t Rely on Snapshot Tests
- Test Correct Rendering
- Validate State and Events
- Test Edge Cases
- Write Integration Tests
- Styling
- Use CSS-in-JS
- Keep Styled Components Together
- Data Fetching
- Use a Data Fetching Library
Components
优先选择函数式组件
优先选择函数式组件,它有更简单的语法,没有生命周期方法,构造函数或者 boilerplate(模板)。你可以用更少的字符来表达相同的逻辑,而不会失去可读性。
除非你需要一个错误边界,否则函数式组件应该是你的首选方法。你需要记住的心智模式要少得多。
// 👎 Class components are verbose |
编写一致的组件
为组件编写固定一种风格,将辅助(工具)函数放在同一个位置,使用一致的方式导出,并且遵循一致的命名原则
其中一种方法并不比另一种有真正的好处
无论你是在文件的底部导出还是直接在组件的定义中导出,选择一种方法并坚持使用它。
为组件命名,不要使用匿名组件
始终为组件命名,它可以帮助你读取错误堆栈跟踪并使用 React Dev Tools。
如果组件的名称在文件中,那么在开发时更容易找到文件所在的位置。
// 👎 Avoid this |
组织你的辅助(工具)函数,与组件分离
不需要在组件上持有闭包的辅助(工具)函数应该被移到外部。理想的位置是在组件定义之前,这样文件就可以从上到下读取。
这也减少了组件的冗余程度,只留下那些需要在那里。
// 👎 Avoid nesting functions which don't need to hold a closure. |
您希望在定义中保留最少的辅助(工具)函数。将尽可能多的变量移到外部,并将 state 的值作为参数传递。用仅依赖输入的纯函数组成逻辑可以更容易地跟踪 bug 和扩展。
// 👎 Helper functions shouldn't read from the component's state |
不要编写硬编码
不要为导航、过滤器或列表硬编码标记。使用一个配置对象并循环遍历这些项。这样做在未来更改时只需要在单个位置更改标记和项。
// 👎 Hardcoded markup is harder to manage. |
组件长度
React 组件只是一个获取 props 并返回标记语言的函数。它们遵循相同的软件设计原则。
如果一个函数做了太多的事情,提取一些逻辑并包装为一个新的函数。
组件也是一样,如果你有太多的功能,把它分解成更小的组件,然后调用它们。
如果标记语言的一部分是复杂的,需要循环和条件渲染。那么就可以去抽象为一个新的组件
依靠 props 和回调进行通信和数据。代码行数不是一个客观的度量。考虑一下职责和抽象。
在 jsx 中编写注释
当某些东西需要更明确的时候,打开一个代码块并提供额外的信息。注释是逻辑的一部分,所以当您觉得某些东西需要更清晰时,就提供它。
function Component(props) { |
使用 <ErrorBoundary>
标签
一个组件中的错误不应该导致整个 UI 崩溃。在极少数情况下,如果发生了严重错误,我们希望删除整个页面或重定向。在大多数情况下,只要在屏幕上隐藏一个特定的元素就可以了。
在函数中你可能会有一些关于页面需要展示数据的处理,不要只在顶层设置错误边界。包装 在页面上可以单独存在的元素以避免级联失败。
function Component() { |
解构 props
大多数 React 组件只是功能性的组件。他们得到 props,然后返还 渲染到页面上的标记语言。
在普通函数中,您使用直接传递的参数,因此在这里应用相同的原则是有意义的。不需要到处重复使用 props。
有一种不解构 props 的情况是为了区分什么是外部状态,什么是内部状态。
但在常规函数中,参数和变量之间没有区别。不要创造不必要的模式
// 👎 Don't repeat props everywhere in your component |
props 的数量
一个组件应该接收多少 props 是一个主观的问题。一个组件拥有的 props 数量与它做了多少事情相关。你给予它的支持越多,它的责任也就越大
大量的 props 是一个组件做太多的信号。
如果我超过 5 个 props,我考虑这个组件是否应该被拆分。
在某些情况下,它可能只需要大量数据。例如,一个输入字段可能有很多 props。但是在另一些情况下,它是需要被封装抽象的标志。
注意:使用的 props 数量越多,越可能被重复渲染
使用对象代替原始值
限制 props 数量的一种方法是传递一个对象而不是原始值。比如下面,比起将用户名、电子邮件和设置一个一个地传下去不如使用一个对象。这也减少了用户获得额外字段时需要做的更改。
使用 TypeScript 进行类型注解会让这变得更容易
// 👎 Don't pass values on by one if they're related |
条件渲染
在某些情况下,为条件呈现使用短路操作符(&&)可能会适得其反,您可能最终在 UI 中得到一个不想要的 0。
为了避免这种情况,默认使用三元运算符。唯一的警告是,它们更加冗长。
短路操作符减少了代码的数量,这总是很好。三进制更冗长,但不可能出错。另外,添加替代条件的改变更小。
// 👎 Try to avoid short-circuit operators |
避免嵌套的三元运算符
三值运算符在第一级之后变得难以阅读。即使它们在当时看起来节省了空间,但最好是能够一眼看出代码块的意图
// 👎 Nested ternaries are hard to read in JSX |
将递归渲染的 DOM 封装为单独的组件
循环遍历项列表是一种常见的操作,通常使用 map 函数完成。然而,在一个有很多标记的组件中,额外的缩进和 map 语法对可读性没有帮助。
当您需要映射元素时,在它们自己的列表组件中提取它们,哪怕标记并不多。父组件不需要知道详细信息,只需要知道它正在显示一个列表。
只有当组件的主要职责是显示它时,才在标记中保持一个循环。尽量为每个组件保留一个映射,但如果标记很长或很复杂,可以用任何一种方法提取列表。
// 👎 Don't write loops together with the rest of the markup |
解构时为 props 设置默认值
指定默认 props 值的一种方法是将 defaultProps 属性附加到组件上。这意味着组件函数及其参数的值不会放在一起。
最好在重新构造 props 时直接分配默认值。它使从上到下阅读代码变得更容易,而无需跳转,并将定义和值保持在一起。
// 👎 Don't define the default props outside of the function |
避免嵌套的组件
当您需要从组件或逻辑中提取标记时,不要将其放在位于同一组件中的函数中。组件只是一个函数。这样定义它就是嵌套在其父类中。
这意味着它可以访问其父节点的所有状态和数据。它使代码更加不具有可读性——这个函数在所有组件之间的作用是什么?
将其移动到自己的组件中,命名并依赖于 props 而不是 closure。
// 👎 Don't write nested render functions |
状态管理
使用 Reducers
有时您需要一种更强大的方式来传递和管理状态更改。在使用外部库之前先使用 useReducer。这是一个很好的机制来进行复杂的状态管理,它不需要第三方依赖。
结合 React s Context 和 TypeScript, useReducer 可以变得非常强大。不幸的是,它并没有被广泛使用。人们仍然使用第三方库。
如果您需要多个状态块,则将它们移动到 Reducers。
// 👎 Don't use too many separate pieces of state |
优先选择 hooks 而不是 HOC 或者 render props
在某些情况下,我们需要增强组件或允许它访问外部状态。通常有三种方法可以做到这一点-高阶组件(HOCs),props 和 hooks。
hooks 已经被证明是实现这种组合的最有效的方法。从哲学的角度来看,组件是使用其他功能的功能。hooks 允许您访问外部功能的多个来源,而不会相互冲突。不管有多少个 hooks,您都知道每个值来自哪里。
使用第三方数据获取插件
我们想要管理状态的数据通常是从 API 中检索的。我们需要将数据保存在内存中,更新它,并在多个地方访问它。像 React Query 这样的现代数据获取库提供了足够的机制来管理外部数据。我们可以缓存它,使它失效,并重新获取它。它们还可以用于发送数据,触发另一条数据的刷新。如果使用像 Apollo 这样的 GraphQL 客户端,则更容易。它内置了客户端状态的概念。
状态管理插件
在大多数情况下,您不需要状态管理库。它们应该用于需要管理复杂状态的大型应用程序。关于这个主题有很多指南,所以我将只提到在这种情况下我将探索的两个库——Recoil 和 Redux。
组件心智模式
Container & Presentational (容器和展示组件)
主要的思路是将组件分成两组:表示组件和容器组件。也被称为聪明和愚蠢。其思想是,有些组件没有任何功能和状态。它们只是被带有一些道具的父组件调用。容器组件包含业务逻辑,执行数据获取和管理状态。这种心理模型就是 MVC 结构对于后端应用程序的作用。它足够通用,可以在任何地方工作,你不会错的。但是,在现代 UI 应用程序中,这种模式还不够。把所有的逻辑都拉进来。
无状态和有状态组件
可以将组件分为有状态和无状态两种。上面提到的心智模型意味着一些组件应该管理大量的复杂性数据。相反,它应该在整个应用程序内使用。
数据应该位于使用数据的地方附近。当您使用 GraphQL 客户端时,您将在显示数据的组件中获取数据。即使它不是顶级的。不要想容器,想想组件的职责。考虑保存状态块的最符合逻辑的组件是什么。
例如,一个 <Form />
组件应该有属于自己的数据,一个 <Input />
在输入值改变时应该接收值并进行回调。当一个<Button />
组件被点击时应该通知表单并让表单知道发生了什么。
谁在表单中进行验证?输入字段负责吗?这意味着该组件将意识到应用程序的业务逻辑。它如何通知表单有错误?如何刷新错误状态?表格会知道吗?如果有一个错误,但你试图提交,会发生什么?
当面对这样的问题时,你应该意识到组件的职责正在被混淆。在这种情况下,最好让输入保持无状态,并从表单接收错误消息。
应用架构
通过路由或者模块分组
按容器和组件分组使应用程序难以导航。要理解组件属于哪里,您需要非常熟悉整个应用。
并不是所有的组件都是相同的——有些是全局使用的,有些是为应用程序的特定部分制作的。这种结构对于最小的项目很有效。但是,对于大型项目来说很难管理。
// 👎 Don't group by technical details |
开始使用 路由或者模块来进行目录的组织,这是一个支持变化和增长的结构。关键是不要让您的应用程序快速超出体系结构。如果你的项目是基于组件和容器的,那将会很快就会变得无序和难于查找。
基于组件和容器的结构没有错,但太一般化了。除了它使用 React 之外,它没有告诉读者任何关于这个项目的信息。
创建公共的模块
像按钮、输入和卡片这样的组件随处可见。即使你不打算使用基于模块的结构,提取这些也是很好的。即使你不使用 Storybook,你也可以看到你所拥有的公共组件。它有助于避免重复。你不希望团队中的每个人都制作自己版本的按钮。不幸的是,由于结构糟糕的项目,这种情况经常发生。
使用绝对路径
使事情更容易更改是项目结构的基础。绝对路径意味着如果你需要移动一个组件,你需要改变的就少了。同时,它也让我们更容易找到所有东西的来源。
// 👎 Don't use relative paths |
我使用@前缀表示它是一个内部模块,但我也看到过使用~完成它
对外部组件进行包装
尽量不要直接导入太多第三方组件。通过围绕它们创建适配器,我们可以在必要时修改 API。此外,我们可以在一个地方改变第三方库。
这同样适用于语义 UI 和实用程序组件等组件库。您可以做的最简单的事情是从公共模块重新导出它们,以便从相同的位置提取它们。
组件不需要知道我们使用什么库作为日期选择器——只需要知道它存在即可。
// 👎 Don't import directly |
为组件建立单独的文件夹
我为 React 应用程序中的每个模块创建了一个组件文件夹。每当我需要创建一个组件时,我都会先在这里创建它。如果它需要额外的文件,如样式或测试,我创建自己的文件夹并将它们放在那里。
作为一种常规做法,最好有一个 index.js
文件来导出 React
组件,这样你就不必更改导入路径或重复导入路径, 比如像import Form from 'components/UserForm/UserForm'
。不过,要保留组件文件的名称,这样当打开多个组件时就不会混淆了。
// 👎 Don't keep all component files together |
性能
不要过早的开始性能优化
在进行任何类型的优化之前,请确保这些优化是有原因的。盲目地遵循最佳实践是浪费精力,除非它在某种程度上影响了您的应用程序。
是的,知道某些事情是很重要的,但是在性能之前先构建可读和可维护的组件。编写好的代码更容易改进。
当您注意到应用程序中的性能问题时——测量并确定问题的原因。如果你项目打包之后非常大,那么就没有必要减少渲染数量
一旦您知道了性能问题来自哪里,就按照其影响的重要性修复它们。
观察引入包的大小
发送到浏览器的 JavaScript 数量是影响应用程序性能的最重要因素。你的应用程序可以非常快,但如果他们需要加载 4MB 的 JS 才能加载它,可能没有人会发现这一点。
不要发布单一的 JS 包。在路由级别上拆分应用程序,甚至更进一步。确保你发送的 JS 数量尽可能少。
在后台加载或者当用户显示他们需要另一个 bundle 的意图时。如果按下一个按钮会触发 PDF 下载,您可以延迟 PDF 库的下载,直到按钮悬停。
减少重复渲染 - 回调,数组和对象
试着减少应用中不必要的渲染量是很好的。请记住这一点,但也要注意,不必要的渲染很少会对你的应用产生最大的影响。
最常见的建议是避免传递回调函数作为 props。使用一个表示每次都会创建一个新函数,从而触发一个渲染器。我没有遇到任何回调相关的性能问题,事实上,这是我的首选方法。
如果遇到性能问题,而闭包是罪魁祸首的话,则删除它们。但是不要使你的代码可读性降低,也不要使它变得不必要的冗长。
直接向下传递数组或对象属于同一类问题。它们没有通过引用检查,因此将触发一个渲染器。如果需要传递固定数组,则在组件定义之前将其作为常量提取,以确保每次都传递相同的实例。
测试
不要依赖快照测试
自从 2016 年我开始使用 React 以来,我只遇到过一次快照测试在我的组件中发现问题的情况。没有参数的 new Date()调用已经溜走,它总是默认为当前日期。
除此之外,快照只是在组件更改时导致构建失败的原因。通常的工作流程是对组件进行更改,查看快照是否失败,更新它们,然后继续。
不要误解我的意思,它们是一种很好的完整性检查,但它们不能替代好的组件级测试。我甚至不再使用它们。
测试正确展示
测试应该验证的主要内容是组件是否按预期工作。确保它使用 default props
和传递给它的 props 正确地呈现。验证对于给定的输入(props)函数是否返回正确的结果(JSX)。确认您需要的所有内容都展示在屏幕上。
验证状态和事件
有状态组件很可能会随着事件的响应而更改。模拟事件,并确保组件正确响应这些事件。验证是否调用了处理函数并传递了正确的参数。检查内部状态是否设置正确。
测试边界情况
当你有了基本的测试,确保你添加一些处理边缘情况。这意味着传递一个空数组,以确保您不会在没有检查的情况下访问索引。在 API 调用中抛出一个错误以确保组件处理它。
编写集成测试
集成测试是为了验证整个页面或更大的组件。它测试它作为一个抽象是否有效。它们使我们对应用程序按预期工作最有信心。组件本身可以工作得很好,并且它们的单元测试可以通过。不过,它们之间的整合可能会出现问题。
样式
使用 css-in-js
这是一个很有争议的观点,很多人都不同意。我宁愿使用像样式组件或 Emotion 这样的库,因为它允许我用 JavaScript 来表达我的组件的所有内容。少维护一个文件。没有需要考虑的 CSS 约定。React 中的逻辑单元是组件,所以从关注点分离的角度来看,它应该拥有与它相关的所有东西。注意:当谈到样式时没有错误的选择- SCSS, CSS 模块,像 Tailwind 这样的库。CSS-in-JS 就是我推荐的方法。
保持统一的组件书写格式
当涉及到 CSS-in-JS 组件时,在同一个文件中有多个是正常的。理想情况下,我们希望将它们保存在与使用它们的常规组件相同的文件中。但是,如果样式变得太长,可以将它们提取到使用它们的组件旁边的自己的文件中。我曾在 Spectrum 等开源项目中看到过这种模式。
数据获取
使用一个获取数据的第三方包
React 不提供从 API 获取或更新数据的武断方式。每个团队创建自己的实现,通常涉及一个包含与 API 通信的异步函数的服务。这意味着我们需要自己管理加载状态和处理 http 错误。这将导致使用大量样板文件的冗长代码。
相反,我们应该使用类似 React Query 或 SWR 这样的库。它们以一种惯用的方式——钩子——使与服务器的通信成为组件生命周期的自然组成部分。它们内置了缓存,为我们管理加载和错误状态。我们只需要处理他们。此外,它们还消除了使用状态管理库来处理数据的需要。