React 之道:软件设计、架构和最佳实践
(给大前端技术之路加星标,提升前端技能)
我从 2016 年就开始使用 React,不过在应用架构和设计方面还没能总结出一个最佳实践。
虽然在低层面上有一些最佳实践,但在架构方面,大多数团队都会构建自己的“东西”。
当然是不存在所有业务领域都通用的最佳实践的。但确实有一些规则可以帮助你构建一个高效的基础代码。
软件架构的目的就是高效灵活。开发者可以高效开发,并能在不需要重写其核心的情况下进行修改。
这篇文章整合了一些对我和我合作过的团队行之有效的原则和规则。
我概述了有关组件、程序结构、测试、样式、状态管理和数据获取的优秀实践。有些例子可能过于简单化了,所以我们可以把重点放在原则上,而不是实现上。
把这里的一切都当作一种观点,而不是唯一的。构建软件的方法可不止一种。
优先函数组件
优先函数组件-他们语法更加简单。没有生命周期方法、构造函数或样板代码。你可以用更少的代码来表达相同的逻辑,而且不会失去可读性。
除非你要用错误边界,否则这是你最好的选择。这样你脑中需要保持的模型会变得小得多。
// 👎 Class components are verbose
class Counter extends React.Component {
state = {
counter: 0,
}
constructor(props) {
super(props)
this.handleClick = this.handleClick.bind(this)
}
handleClick() {
this.setState({ counter: this.state.counter + 1 })
}
render() {
return (
<div>
<p>counter: {this.state.counter}</p>
<button onClick={this.handleClick}>Increment</button>
</div>
)
}
}
// 👍 Functional components are easier to read and maintain
function Counter() {
const [counter, setCounter] = useState(0)
handleClick = () => setCounter(counter + 1)
return (
<div>
<p>counter: {counter}</p>
<button onClick={handleClick}>Increment</button>
</div>
)
}
编写风格一致的组件
对组件使用相同的风格。将helper函数放在相同的位置,以相同的方式导出,并遵循相同的命名模式。
没有真正一种方法要比另一种方法好。
无论是在文件的底部导出还是直接在组件的定义中导出,请选好一个并坚持它就行。
对组件命名
始终命名组件。这对读取错误堆栈信息以及使用React Dev工具都有所帮助。
而且在开发的时候也能更容易的在文件中找组件的位置。
// 👎 Avoid this
export default () => <form>...</form>
// 👍 Name your functions
export default function Form() {
return <form>...</form>
}
管理Helper函数
工具类方法不需要依赖于组件闭包信息的应该把其移到外面去。比较好的地方是组件定义之前,这样代码文件在阅读的时候就能上到下的读。
这样就减少了组件中的噪声,只留下那些必要的部分。
// 👎 Avoid nesting functions which don't need to hold a closure.
function Component({ date }) {
function parseDate(rawDate) {
...
}
return <div>Date is {parseDate(date)}</div>
}
// 👍 Place the helper functions before the component
function parseDate(date) {
...
}
function Component({ date }) {
return <div>Date is {parseDate(date)}</div>
}
你应该尽可能的减少组件中的工具方法。而尽量多的把这些方法移出去,把方法依赖的组件内部信息作为参数。
这样逻辑仅依赖于一些纯函数的组合,就容易地跟踪bug和扩展。
// 👎 Helper functions shouldn't read from the component's state
export default function Component() {
const [value, setValue] = useState('')
function isValid() {
// ...
}
return (
<>
<input
value={value}
onChange={e => setValue(e.target.value)}
onBlur={validateInput}
/>
<button
onClick={() => {
if (isValid) {
// ...
}
}}
>
Submit
</button>
</>
)
}
// 👍 Extract them and pass only the values they need
function isValid(value) {
// ...
}
export default function Component() {
const [value, setValue] = useState('')
return (
<>
<input
value={value}
onChange={e => setValue(e.target.value)}
onBlur={validateInput}
/>
<button
onClick={() => {
if (isValid(value)) {
// ...
}
}}
>
Submit
</button>
</>
)
}
不硬编码标签
不要为导航、过滤器或列表硬编码标签。使用一个配置对象并循环遍历这个配置。
这样你就只需要在第一个地方修改标签和项内容。
// 👎 Hardcoded markup is harder to manage.
function Filters({ onFilterClick }) {
return (
<>
<p>Book Genres</p>
<ul>
<li>
<div onClick={() => onFilterClick('fiction')}>Fiction</div>
</li>
<li>
<div onClick={() => onFilterClick('classics')}>
Classics
</div>
</li>
<li>
<div onClick={() => onFilterClick('fantasy')}>Fantasy</div>
</li>
<li>
<div onClick={() => onFilterClick('romance')}>Romance</div>
</li>
</ul>
</>
)
}
// 👍 Use loops and configuration objects
const GENRES = [
{
identifier: 'fiction',
name: Fiction,
},
{
identifier: 'classics',
name: Classics,
},
{
identifier: 'fantasy',
name: Fantasy,
},
{
identifier: 'romance',
name: Romance,
},
]
function Filters({ onFilterClick }) {
return (
<>
<p>Book Genres</p>
<ul>
{GENRES.map(genre => (
<li>
<div onClick={() => onFilterClick(genre.identifier)}>
{genre.name}
</div>
</li>
))}
</ul>
</>
)
}
组件长度
React组件其实就是一个输入props返回html标签的方法。他们遵循和普通发放一样的设计原则。
如果一个方法处理的事情太多了,就提取出一些逻辑到其他方法。组件也是一样,如果一个组件有太多功能项了,就拆分为更多小的组件。
如果一个部分结构很复杂,需要逻辑判断和循环,那就进行提取。
依赖props和回调进行通信和获得数据。代码行数不是一个客观的衡量标准。应该多想想责任和抽象。
在JSX中写注释
当需要更清晰的内容时,就该打开代码块并提供额外的信息(写注释)。html标签也是业务逻辑的一部分,所以你也应该积极的提供注释。
function Component(props) {
return (
<>
{/* If the user is subscribed we don't want to show them any ads */}
{user.subscribed ? null : <SubscriptionPlans />}
</>
)
}
使用Error Boundaries
一个组件中的错误不应该导致整个UI瘫痪。只有在很少的情况下,如果发生严重错误,我们才会删除整个页面或重定向。大多数情况下,如果我们只在屏幕上隐藏一个特定的元素就可以了。
一个处理数据的方法中可能会有多个的try/catch。所以error boundaries也不仅仅是组件最上层。多用error boundaries包装单个组件库,可以避免级联故障。
function Component() {
return (
<Layout>
<ErrorBoundary>
<CardWidget />
</ErrorBoundary>
<ErrorBoundary>
<FiltersWidget />
</ErrorBoundary>
<div>
<ErrorBoundary>
<ProductList />
</ErrorBoundary>
</div>
</Layout>
)
}
解构Props
大多数的React组件就是函数。他们输入props返回html标记。在一个正常的函数中你会直接使用传递进来的参数,所以这个原则也一样适用。不需要到处重复props
这个字段。
一个不解构props的理由是这样好区分props和内部state。但是在一个普通方法中参数和变量是没差别的。不要创造不必要的规则。
// 👎 Don't repeat props everywhere in your component
function Input(props) {
return <input value={props.value} onChange={props.onChange} />
}
// 👍 Destructure and use the values directly
function Component({ value, onChange }) {
const [state, setState] = useState('')
return <div>...</div>
}
Props的数量
一个组件应该接收多少props是一个主观的问题。一个组件拥有的props的数量与它所做的事情相关。你给它传递的props越多,它承担的功能就越多。
太多的props是一个组件做了太多事情的信号。
如果一个组件有超过5个的prop,那么就要考虑是否要拆分了。在某些情况下,它可能只是需要大量的数据。比如一个input输入框,就可能有很多个prop。在另外一些情况下,这是需要进行进一步提取的信号。
注意:组件使用的props越多,重新渲染的理由也就越多。
传递对象而不是原始类型
一种减少props数量的方法是传入对象而不是原始类型。一个一个地传递用户名、电子邮件和设置,还不如将它们组合在一起。这样即使用户新增一个字段,也不用做修改。
使用TypeScript 让其更加简单。
// 👎 Don't pass values on by one if they're related
<UserProfile
bio={user.bio}
name={user.name}
email={user.email}
subscription={user.subscription}
/>
// 👍 Use an object that holds all of them instead
<UserProfile user={user} />
条件渲染
在某些情况下,使用短路运算符进行条件渲染可能会适得其反,界面中可能会出现一个不需要的0。为了避免这种默认情况,请使用三元运算符。唯一要注意的是它们更冗长。
短路运算符减少了代码量,这很好。三元运算符虽然更长,但不会出错。而且,添加判断条件的也更容易。
// 👎 Try to avoid short-circuit operators
function Component() {
const count = 0
return <div>{count && <h1>Messages: {count}</h1>}</div>
}
// 👍 Use a ternary instead
function Component() {
const count = 0
return <div>{count ? <h1>Messages: {count}</h1> : null}</div>
}
避免嵌套的三元操作符
三元操作符超过一层后就会变的难以理解。虽然他们看上去更加简洁,但保证可读性更加重要。
// 👎 Nested ternaries are hard to read in JSX
isSubscribed ? (
<ArticleRecommendations />
) : isRegistered ? (
<SubscribeCallToAction />
) : (
<RegisterCallToAction />
)
// 👍 Place them inside a component on their own
function CallToActionWidget({ subscribed, registered }) {
if (subscribed) {
return <ArticleRecommendations />
}
if (registered) {
return <SubscribeCallToAction />
}
return <RegisterCallToAction />
}
function Component() {
return (
<CallToActionWidget
subscribed={subscribed}
registered={registered}
/>
)
}
抽取列表
使用map之类的方法遍历一个数组很常见。但在一个有很多html标签的组件里,这种额外的缩进和map语法降低了可读性。当你需要map一些element,把他们放到一个单独的组件中,即使其html标签很少。这样父组件就不需要关心细节,只需要展示列表。只有在这个组件的主要职责就是展现列表的时候才把loop操作保留在组件中。试着为每个组件只保留一个map,但是如果html标记很长或很复杂,则提取列表。
// 👎 Don't write loops together with the rest of the markup
function Component({ topic, page, articles, onNextPage }) {
return (
<div>
<h1>{topic}</h1>
{articles.map(article => (
<div>
<h3>{article.title}</h3>
<p>{article.teaser}</p>
<img src={article.image} />
</div>
))}
<div>You are on page {page}</div>
<button onClick={onNextPage}>Next</button>
</div>
)
}
// 👍 Extract the list in its own component
function Component({ topic, page, articles, onNextPage }) {
return (
<div>
<h1>{topic}</h1>
<ArticlesList articles={articles} />
<div>You are on page {page}</div>
<button onClick={onNextPage}>Next</button>
</div>
)
}
解构props时给与默认值
指定默认属性值的一种方法是给组件加一个defaultProps属性。这意味着组件及其参数的值不会放在一起。
最好在解构props时给与默认值。这样定义和值放在一起,从上到下阅读代码更容易,不用跳跃。
// 👎 Don't define the default props outside of the function
function Component({ title, tags, subscribed }) {
return <div>...</div>
}
Component.defaultProps = {
title: '',
tags: [],
subscribed: false,
}
// 👍 Place them in the arguments list
function Component({ title = '', tags = [], subscribed = false }) {
return <div>...</div>
}
避免嵌套的render方法
当你需要从组件或逻辑中提取html标记时,不要将其放在同一组件中的函数体中。组件只是一个函数。这样定义它就是嵌套在其父级中。
这意味着它将有权访问其父级的所有状态和数据。它使代码更不可读-这个函数在所有组件之间做了什么?
把它移动到自己的组件中,只依赖于自己的props而不是闭包。
// 👎 Don't write nested render functions
function Component() {
function renderHeader() {
return <header>...</header>
}
return <div>{renderHeader()}</div>
}
// 👍 Extract it in its own component
import Header from '@modules/common/components/Header'
function Component() {
return (
<div>
<Header />
</div>
)
}
状态管理
使用reducer
有时,你需要一种更强大的方式来管理状态。在准备使用外部库之前,先试试useReducer。这是一个用来进行复杂状态管理很好的机制,不需要依赖第三方库。
结合React的context和TypeScript,useReducer可以非常强大。不过,它还没有被广泛使用。人们仍然会去第三方库。
如果需要多个状态,请将它们移到一个reducer中。
// 👎 Don't use too many separate pieces of state
const TYPES = {
SMALL: 'small',
MEDIUM: 'medium',
LARGE: 'large'
}
function Component() {
const [isOpen, setIsOpen] = useState(false)
const [type, setType] = useState(TYPES.LARGE)
const [phone, setPhone] = useState('')
const [email, setEmail] = useState('')
const [error, setError] = useSatte(null)
return (
...
)
}
// 👍 Unify them in a reducer instead
const TYPES = {
SMALL: 'small',
MEDIUM: 'medium',
LARGE: 'large'
}
const initialState = {
isOpen: false,
type: TYPES.LARGE,
phone: '',
email: '',
error: null
}
const reducer = (state, action) => {
switch (action.type) {
...
default:
return state
}
}
function Component() {
const [state, dispatch] = useReducer(reducer, initialState)
return (
...
)
}
优先使用Hooks而不是HOC和Render Props
在某些情况下,我们需要增强组件或使其能够访问外部状态。通常有三种方法可以做到这一点---高阶组件(HOC),Render Props和Hooks。
Hooks已经被证明是实现这种效果的最有效的方法。形而上的看,组件是一个使用其他函数的函数。Hooks允许你访问多个外部源,而不会相互冲突。不管Hooks有多少,你都知道每个值来自哪里。
通过HOC,你可以通过props获得数据。这就不清楚它是来自父组件还是被包装的组件。此外,将多个props链接在一起会导致错误。
Render Props会导致高缩进和不好的可读性。在同一棵树中嵌套多个带有Render Props的组件会看起来更糟。而且它只在html标签中暴露值,因此你必须在其中写逻辑或将其传递下去。
使用Hooks,你可以使用简单的值,容易跟踪,并且不会干扰JSX。
// 👎 Avoid using render props
function Component() {
return (
<>
<Header />
<Form>
{({ values, setValue }) => (
<input
value={values.name}
onChange={e => setValue('name', e.target.value)}
/>
<input
value={values.password}
onChange={e => setValue('password', e.target.value)}
/>
)}
</Form>
<Footer />
</>
)
}
// 👍 Favor hooks for their simplicity and readability
function Component() {
const [values, setValue] = useForm()
return (
<>
<Header />
<input
value={values.name}
onChange={e => setValue('name', e.target.value)}
/>
<input
value={values.password}
onChange={e => setValue('password', e.target.value)}
/>
)}
<Footer />
</>
)
}
使用数据获取库
通常我们要在state中管理的数据是从API中获得的。我们需要将这些数据保存在内存中,去更新它并能在多个地方访问它。
现代的数据获取库像 React Query提供了足够的机制来管理外部数据。我们可以对其进行缓存,使其过期和重新获取。也可以用来发送数据,触发其进行刷新另一段数据。
如果你用到了GraphQL client 库比如Apollo,那就更容易了。它内置了 client state 的概念。
状态管理库
在大多数情况下,您不需要状态管理库。它们应该用于需要管理复杂状态的大型应用程序中。有很多关于这个主题的指南,所以我只想提下我会选择的2中库——Recoil和Redux。
组件构思模型
展示型和容器型
主要的思路是将组件分为两组-展示组件和容器组件。也被称为聪明和愚蠢。
其思想是有些组件没有任何功能和状态。它们只是由父组件通过一些props调用的。容器组件包含业务逻辑、数据获取和状态管理。
这个模型就是后端程序的MVC模。它的通用性足以在任何地方工作,用它也没错。
但是,在现代UI应用中,这种模式是不够的。把所有的逻辑放在一些组件中会导致膨胀。他们会最终承担了太多的责任,而变得难以管理。随着应用的发展,将复杂性集中在几个地方对可维护性来说是不好的。
无状态和有状态
将组件视为有状态的和无状态的。上面提到的构思模型意味着一部分组件应该管理着大多数复杂性。相反,它应该分散在整个应用程序中。
数据应该紧靠着它被使用到的地方。当使用 GraphQL 客户端时,应该在显示它的组件中获取数据。即使不是顶级的。不要考虑容器,要考虑责任。考虑保存一个状态的最合乎逻辑的组件是什么。
例如,一个<Form/>
组件应该拥有表单的数据。<Input/>
应该接收值并在发生更改时调用回调。<Button/>
应该通知form它被按下,并让form处理发生的事件。
谁在表单中进行验证?是input的职责吗?这意味着该组件将了解应用的业务逻辑。它将如何通知form有错误?这个错误将如何被刷新?form会知道吗?如果有一个错误存在,但你还是试图提交,会发生什么?
当你面对这样的问题时,你就应该意识到责任被混淆了。在这种情况下,最好让input保持无状态,并从表单接收错误消息。
应用结构
按Route/Module分组
按容器和组件分组会使应用程序难以查找。要了解什么组件属于哪,你需要对项目有非常高的熟悉度。
并不是所有的组件都是平等的——有些是全球通用的,有些是为应用程序的特定部分设计的。这种结构(容器和组件分组)只适用于很少的项目。组件稍微多点就会变得难以管理。
// 👎 Don't group by technical details
├── containers
| ├── Dashboard.jsx
| ├── Details.jsx
├── components
| ├── Table.jsx
| ├── Form.jsx
| ├── Button.jsx
| ├── Input.jsx
| ├── Sidebar.jsx
| ├── ItemCard.jsx
// 👍 Group by module/domain
├── modules
| ├── common
| | ├── components
| | | ├── Button.jsx
| | | ├── Input.jsx
| ├── dashboard
| | ├── components
| | | ├── Table.jsx
| | | ├── Sidebar.jsx
| ├── details
| | ├── components
| | | ├── Form.jsx
| | | ├── ItemCard.jsx
从一开始就用 route/module 的方式进行分组。这种结构支持变化和增长。关键是不要让应用的发展很快的让其架构失效了。如果它是基于组件和容器的分组,就会是这样。
基于模块的结构就易于扩展。你只需在上面添加模块,而不会增加复杂性。
容器/组件结构没有错,但太通用了。它不能告诉读者关于这个项目的任何信息,除了他用了react。
创建通用模块
像Button、输入框和选项卡这样的组件到处都在使用。即使你不打算使用基于模块的结构,也应该提取这些通用内容。
即使你没有用到Storybook,你也可以看到你有哪些组件。它有助于避免重复。你可不希望团队中的每个人都制作自己版本的Button。不幸的是,因为项目结构的糟糕,这种情况经常发生。
使用绝对路径
让事情可以更容易的修改是项目结构的根本目的。绝对路径意味着,如果需要移动组件,则必须进行较少的更改。此外,它还可以更容易地找出一切都是从哪里获得的。
// 👎 Don't use relative paths
import Input from '../../../modules/common/components/Input'
// 👍 Absolute ones don't change
import Input from '@modules/common/components/Input'
我使用@
前缀来表示它是一个内部模块,但我也看过用~
的。
包装外部组件
尽量不要直接导入太多第三方组件。通过创建适配器,这样我们可以在必要时修改API。而且,我们可以在一个地方更改第三方库。
这也适用于组件库,比如 Semantic UI和工具(utility)组件。最简单的就是从公共模块中重新导出它们,这样它们就可以从同一个地方被拉出来。
组件不需要知道我们使用了什么库作为日期选择器。
// 👎 Don't import directly
import { Button } from 'semantic-ui-react'
import DatePicker from 'react-datepicker'
// 👍 Export the component and use it referencing your internal module
import { Button, DatePicker } from '@modules/common/components'
把组件放到文件夹下
我为每个模块都创建了一个components目录。当我需要创建组件,我会先在那里创建。如果它需要样式或测试之类的额外文件,我会创建它自己的文件夹并将它们放在那里。
一个通用的实践:用一个index.js
文件导出React组件比较好。这样离就不需要像import Form from 'components/UserForm/UserForm'
这样需要重复的导入路径。尽管如此,还是要保留组件文件的名称,这样当打开多个组件文件时就不会混淆了。
// 👎 Don't keep all component files together
├── components
├── Header.jsx
├── Header.scss
├── Header.test.jsx
├── Footer.jsx
├── Footer.scss
├── Footer.test.jsx
// 👍 Move them in their own folder
├── components
├── Header
├── index.js
├── Header.jsx
├── Header.scss
├── Header.test.jsx
├── Footer
├── index.js
├── Footer.jsx
├── Footer.scss
├── Footer.test.jsx
性能
不要过早进行优化
在进行任何类型的优化之前,请确保它们是有原因的。盲目遵循最佳实践是浪费精力,除非它真的在某种程度上影响了应用。
是的,有优化意识是很重要的,但是在实现性能之前,优先构建可读和可维护的组件。写得好的代码更容易改进。
当发现性能问题时,先测定并确定问题的原因。如果bundle包很大,那么尝试减少rerender次数是没有意义的。
一旦你知道性能问题来自何处,请按影响的大小修复它们。
注意bundle大小
首屏运行所必需JavaScript代码数量是影响用程序性能的最重要因素。你的应用程序可能非常快,但如果需要加载4MB的JS来运行,那就可能没有人会发现这一点。
不要输出一整个bundle。在路由级别甚至更进一步地拆分应用。确保发送尽可能少的JS。
在后台加载或当用户需要的时候加载。如果按下按钮触发PDF下载,您等到这个按钮被hovered了,再开始PDF库的下载。
重新渲染-Callbacks, 数组和对象
尝试减少应用的中不必要的rerender是很好的。但也要注意,对你的应用程序产生最大的影响很少会是rerender次数。
最常见的建议是避免将callback作为props传递。这意味着每次都会创建一个新函数,从而触发一个rerender。我从来没有遇到过因为回调导致性能问题,事实上,我就是一直将callback作为props传递。
如果你真的遇到了闭包导致的性能问题,那就删掉他们。但是不要让你的代码变得不那么可读或者不必要的冗长。
直接传递数组或对象属于同一类问题。它们不能通过引用相等检查,因此将触发rerender。如果需要传递数组,请在组件定义之前将其提取为常量,以确保每次都传递相同的实例。
测试
不要依赖于快照测试
从我在2016年开始使用React以来,我只遇到过一种快照测试能帮我发现的问题。没有参数的new Date()
调用,它总是默认为当前日期。
除此之外,快照测试只会在组件更改时发生失败。通常的工作流程便是更改组件,查看快照是否失败,更新快照并继续。
别误会,他们是一个很好的健全检查,但他们不是一个好的组件级测试的替代品。我甚至不再创造它们快照测试用例。
测试渲染正确性
测试的主要功能内容应该是检查组件是否按预期工作。确保它使用默认的props和传递的props都能正确渲染。
验证对于给定的输入(props),函数是否返回正确的结果(JSX)。检测你所需要的一切都在屏幕上。
验证状态和事件
一个有状态的组件很可能对事件做出响应而更改。模拟事件并且确保组件做出了正确的响应。
验证事件处理函数被调用了而且传入的参数是正确的。验证内部状态也被正确的设置了。
边界条件测试
当测试用例已涵盖基本情况,确保你添加了一些处理边界条件的用例。
这意味着传递一个空数组,以确保没有在未检查的情况下访问索引。在API调用中抛出一个错误,以确保组件能够处理它。
编写集成测试
集成测试的意思是验证这个界面或者更大的组件。测试它们作为一部分的提取是否工作得很好。这最能给与我们对应用程序能按预期工作的信心。
因为组件本身可以很好地工作,它们的单元测试也可以通过。不过,它们之间的整合可能会有问题。
样式
使用CSS-in-JS
这是一个很有争议的观点,很多人会不同意。我更愿意使用像Styled Components或Emotion样的库,因为我能在JavaScript中表达关于组件的一切。又少了一个要维护的文件。也不需要考虑CSS的约定。
React中的逻辑单元是组件,因此从关注点分离的角度考虑,组件应该拥有与之相关的所有内容。
注意:在样式方面选择SCSS、CSS模块、库(比如Tailwind)并没有错。不过CSS-in-JS是我推荐的方法。
把styled组件放到一块
在同一个文件中有多个CSS-in-JS 组件是很正常的。理想情况下,我们希望将它们保存在与使用它们的普通组件相同的文件中。
但是,如果它们变得太长(样式一般都会这样),则将它们提取到单独文件中,放在其使用者的旁边。我在如Spectrum的开源项目中见过这种模式。
数据获取
使用数据获取库
React没有明确的一种从API获取或更新数据的方法。每个团队都会创建自己的实现,通常包括一个服务调用一些异步函数,这些函数与API通信。
这条路意味着我们要自己管理加载状态和http错误。这会导致冗长和样板化的代码。
相反的我们可以使用类似React Query 或 SWR这类的库。他们使用hooks和服务器进行通信,并且与组件生命周期自然的结合。
它们内置了缓存功能,并能管理加载和错误状态。我们只需要操作这些库。而且,它们消除了使用状态管理库来处理这些数据的需要。
- EOF -
觉得本文对你有帮助?请分享给更多人
关注「大前端技术之路」加星标,提升前端技能
点赞和在看就是最大的支持❤️