大家好,我是若川。持续组织了6个月源码共读活动,感兴趣的可以点此加我微信 ruochuan12 参与,每周大家一起学习200行左右的源码,共同进步。同时极力推荐订阅我写的《学习源码整体架构系列》 包含20余篇源码文章。历史面试系列
翻译自:https://github.com/mithi/react-philosophies[1] 2.5k star
原文作者:mithi[2]
已获作者授权
概要
介绍
最低要求
面向幸福设计
性能优化技巧
测试原则
🧘 0. 介绍
《React 开发思想纲领》
是:
我开发
React
时的一些思考每当我 review 他人或自己的代码时自然而然会思考的东西
仅仅作为参考和建议,并非严格的要求
会随着我的经验不断更新
大多数技术点是基础的
重构方法论
,SOLID 原则
以及极限编程
等思想的变体,仅仅是在React
中的实践而已 🙂
你可能会觉得我写的这些非常基础。但以下示例都来自一些复杂大型项目的线上代码。
《React 开发思想纲领》
的灵感来源于我实际开发中遇到的各种场景。
🧘 1. 最低要求
1.1 计算机比你更「智能」
使用
ESLint
来静态分析你的代码,开启rule-of-hooks
和exhaustive-deps
这两个规则来捕获React
错误。开启 JS
严格模式
吧,都 2202 年了。直面依赖
,解决在useMemo
,useCallback
和useEffect
上exhaustive-deps
规则提示的 warning 或 error 问题。可以将最新的值挂在 ref 上来保证这些 hook 在回调中拿到的都是最新的值,同时避免不必要的重新渲染。使用 map 批量渲染组件时,
都加上 key
。只在最顶层使用 hook
,不要在循环、条件或嵌套语句中使用 hook。理解
不能对已经卸载的组件执行状态更新
的控制台警告。给不同层级的组件都添加
错误边界(Error Boundary)
来防止白屏,还可以用它来向错误监控平台(比如Sentry
)上报错误,并设置报警。不要忽略了控制台中打印的错误和警告。
记得要
tree-shaking
!使用
Prettier
来保证代码的格式化一致性!使用
Typescript
和NextJS
这样的框架来提升开发体验。强烈推荐
Code Climate
(或其他类似的)开源库。这类工具会自动检测代码异味(Code Smell,代码中的任何可能导致深层次问题的症状),它可以促使我去处理项目里留下的技术债。
1.2 Code is just a necessary evil
译者注:程序员的目标是解决客户的问题,代码只是副产品
1.2.1 先思考,再加依赖
依赖加的越多,提供给浏览器的代码就越多。扪心问问自己,你是否真的使用了某个库的 feature?
🙈 你真的需要它吗? 看看这些你可能不需要的依赖
你是否真的需要
Redux
?有可能需要,但其实 React 本身也是一个状态管理库
。你是否真的需要
Apollo client
?Apollo client
有许多很强大的功能,比如数据规范化。但使用的同时也会显著提高包体积。如果你的项目使用的并非是Apollo client
特有的 feature,可以考虑使用一些轻量的库来替代,比如react-query
或SWR
(或者根本不用)。Axios
呢?Axios 是一个很棒的库,它的一些特性不容易通过原生的fetch
API 来复刻。但是如果使用Axios
只是因为它有更好的 API,完全可以考虑在fetch
上做一层封装(比如redaxios
或自己实现)。取决于你的 App 是否真正地使用了Axios
的核心 feature。Decimal.js
呢?或许Big.js
或者其他轻量的库就足够了。Lodash
/underscoreJS
呢?推荐你看看【你不需要系列之“你不需要 Lodash/Underscore”】[3]。MomentJS
呢?【你不需要系列之“你不需要 Momentjs”】[4]。你不需要为了主题(
浅色
/深色
模式)而使用Context
,考虑下用css 变量
代替。你甚至不需要
Javascript
,CSS 也足够强大。【你不需要系列之“你不需要 JavaScript”】[5]
1.2.2 不要自作聪明,提前设计
"我们的软件在未来会如何迭代?可能会这样或者那样,如果在当下就开始往这些方向进行代码设计,这就叫 future-proof(防过时,面向未來编程)。"
不要这样搞! 应该在面临需求的时候再去实现相应功能,而不是在你预见到可能需要的时候。代码应该越少越好!
1.3 发现了就优化它
1.3.1 检测代码异味(Code Smell),并在必要时对其进行处理。
当你意识到某个地方出现了问题,那就马上处理掉。但如果当前不容易修复,或者没有时间,那请至少添加一条注释(FIXME
或者 TODO
),附上对该问题的简要描述。来让项目里的每个人都知道这里有问题,让他们意识到当他们遇到这样的情况时也该这样做。
🙈 来看看这些容易发现的代码异味
❌ 定义了很多参数的函数或方法
❌ 难以理解的,返回 Boolean 值的逻辑
❌ 单个文件中代码行数太多
❌ 在语法上可能相同(但格式化可能不同)的重复代码
❌ 可能难以理解的函数或方法
❌ 定义了大量函数或方法的类/组件
❌ 单个函数或方法中的代码行数太多
❌ 具有大量返回语句的函数或方法
❌ 不完全相同但代码结构类似的重复代码(比如变量名可能不同)
切记,代码异味并不一定意味着代码需要修改,它只是告诉你,你应该可以想出更好的方式来实现相同的功能。
1.3.2 无情的重构。简单比复杂好。
💁♀️ 小技巧: 简化复杂的条件语句
,最好能提前 return。
🙈 提前 return 的示例
# ❌ 不太好if (loading) {return <LoadingScreen />
} else if (error) {return <ErrorScreen />
} else if (data) {return <DataScreen />
} else {throw new Error('This should be impossible')
}# ✅ 推荐if (loading) {return <LoadingScreen />
}if (error) {return <ErrorScreen />
}if (data) {return <DataScreen />
}throw new Error('This should be impossible')
💁♀️ 小技巧: 比起传统的循环语句,链式的高阶函数更优雅
如果没有明显的性能差异,尽量使用链式的高阶函数(map
, filter
, find
, findIndex
, some
等) 来代替传统的循环语句。
1.4 你可以做的更好
💁♀️ 小技巧: 可以在 setState
时传入回调函数,所以没必要把 state
作为一个依赖项
你不用把 setState
和 dispatch
放在 useEffect
和 useCallback
这些 hook 的依赖数组中。ESLint 也不会给你提示,因为 React 已经确保了它们不会出错。
# ❌ 不太好
const decrement = useCallback(() => setCount(count - 1), [setCount, count])
const decrement = useCallback(() => setCount(count - 1), [count])# ✅ 推荐
const decrement = useCallback(() => setCount(count => (count - 1)), [])
💁♀️ 小技巧: 如果你的 useMemo
或 useCallback
没有任何依赖,那你可能用错了
# ❌ 不太好
const MyComponent = () => {const functionToCall = useCallback(x: string => `Hello ${x}!`,[])const iAmAConstant = useMemo(() => { return {x: 5, y: 2} }, [])/* 接下来可能会用到 functionToCall 和 iAmAConstant */
}# ✅ 推荐
const I_AM_A_CONSTANT = { x: 5, y: 2 }
const functionToCall = (x: string) => `Hello ${x}!`
const MyComponent = () => {/* 接下来可能会用到 functionToCall 和 I_AM_A_CONSTANT */
}
💁♀️ 小技巧: 巧用 hook 封装自定义的 context,会提升 API 可读性
它不仅看起来更清晰,而且你只需要 import 一次,而不是两次。
❌ 不太好
// 你每次需要 import 两个变量
import { useContext } from 'react';
import { SomethingContext } from 'some-context-package';function App() {const something = useContext(SomethingContext); // 看起来 ok,但可以更好// ...
}
✅ 推荐
// 在另一个文件中,定义这个 hook
function useSomething() {const context = useContext(SomethingContext);if (context === undefined) {throw new Error('useSomething must be used within a SomethingProvider');}return context;
}// 你只需要 import 一次
import { useSomething } from 'some-context-package';function App() {const something = useSomething(); // 看起来会更清晰// ...
}
💁♀️ 小技巧: 在写组件之前,先思考该怎么用它
设计 API 很难,README 驱动开发(RDD)
是个很有用的办法,可以帮助你设计出更好的 API。并不是说应该无脑使用 RDD,但它背后的思想是很值得学习的。我自己发现,在设计实现组件 API 之前,使用 RDD 通常比不用时设计地更好。
🧘 2. 面向幸福设计
太长不看版
💖 通过删除冗余的状态来减少状态管理的复杂性。
💖 “传递香蕉,而不是拿着香蕉的大猩猩和整个丛林“(意思是组件要什么传什么,不要传大对象)。
💖 让你的组件小而简单 —— 单一职责原则。
💖 复制比错误的抽象要“便宜”的多(避免提早/不恰当的设计)。
避免 prop 层层传递(又叫 prop 钻取,prop drilling)。
Context
不是解决状态共享问题的银弹。将巨大的
useEffect
拆分成独立的小useEffect
。将逻辑提取出来都放到 hook 和工具函数中。
useCallback
,useMemo
和useEffect
依赖数组中的依赖项最好都是基本类型。不要在
useCallback
,useMemo
和useEffect
中放入太多的依赖项。为了简单起见,如果你的状态依赖其他状态和上次的值,考虑使用
useReducer
,而不是使用很多个useState
。Context
不一定要放在整个 app 的全局。把Context
放在组件树中尽可能低的位置。同样的道理,你的变量,注释和状态(和普通代码)也应该放在靠近他们被使用的地方。
💖 2.1 删除冗余的状态来减少状态管理的复杂性
冗余的状态指可以通过其他状态经过推导得到的状态,不需要单独维护(类似 Vue computed),当你有冗余的状态时,一些状态可能会丢失同步性,在面对复杂交互的场景时,你可能会忘记更新它们。
删除这些冗余的状态,除了避免同步错误外,这样的代码也更容易维护和推理,而且代码更少。
💖 2.2 “传递香蕉,而不是拿着香蕉的大猩猩和整个丛林“
为了避免掉入这种坑,最好将基本类型(boolean
, string
, number
等)作为 props 传递。(传递基本类型也能更好的让你使用 React.memo
进行优化)
组件应该仅仅只了解和它运作相关的内容就足够了。应该尽可能地与其他组件产生协作,而不需要知道它们是什么或做什么。
这样做的好处是,组件间的耦合会更松散,依赖程度会更低。低耦合更利于组件修改,替换和移除,而不会影响其他组件。
💖 2.3 让你的组件小而简单
什么是「单一职责原则」?
一个组件应该有且只有一个职责。应该尽可能的简单且实用,只有完成其职责的责任。
具有各种职责的组件很难被复用。几乎不可能只复用它的部分能力,很容易与其他代码耦合在一起。那些抽离了逻辑的组件,改起来负担不大而且复用性更强。
如何判断一个组件是否符合单一职责?
可以试着用一句话来描述这个组件。如果它只负责一个职责,描述起来会很简单。如果描述中出现了“和“或“或”,那么这个组件很大概率不是单一职责的。
检查组件的 state,props 和 hooks,以及组件内部声明的变量和方法(不应该太多)。问问自己:是否这些内容必须组合到一起这个歌组件才能工作?如果有些不需要,可以考虑把它们抽离到其他地方,或者把这个大组件拆解成小组件。
🧘 3. 性能优化技巧
如果你觉得应用速度慢,就应该做一次基准测试(benchmark)来证明。 "面对模凌两可的情况,拒绝猜测。" 多使用
Chrome 插件 - React 开发者工具
的 profiler!useMemo
主要用在大开销的计算上。如果你打算使用
React.memo
,useMemo
, 和useCallback
来减少重新渲染,它们不该有过多的依赖项,且这些依赖项最好都是基本类型。确保你清楚代码里
React.memo
,useCallback
或useMemo
它们都是为了什么而使用的(是否真的能防止重新渲染?是否能证明在这些场景中真的可以显著提高性能? Memoization 有时会起到反作用,所以需要关注!)优先修复慢渲染,再修复重新渲染。
把状态尽可能地放在它被使用的地方,一方面让代码读起来更顺,另一方面,能让你的 app 更快(state colocation(状态托管))
Context
应该按逻辑分开,不要在一个 provider 中管理多个 value。如果其中某个值变化了,所有使用该 context 的组件(即便没有用到这个值),都会重新渲染。可以通过拆分
state
和dispatch
来优化context
。了解下
lazy loading(懒加载)
和bundle/code splitting(代码分割)
。长列表请使用
tannerlinsley/react-virtual
或其它类似的库。包体积越小,app 越快。你可以使用
source-map-explorer
或者@next/bundle-analyzer
(用于 NextJS) 来进行包体积分析。关于表单的库,推荐使用
react-hook-forms
,它在性能和开发体验各方面都做的比较好。
🧘 4. 测试原则
测试应该始终与软件的使用方式相似。
确保不是在测试一些边界细节(用户不会使用,看不到甚至感知不到的内容)。
如果你的测试不能让你对自己的代码产生信任,那测试就是无意义的。
如果你正在重构某个代码,且最后实现的功能都是完全一致的,其实几乎不需要修改测试,而且可以通过测试结果来判定你正确的重构了。
对于前端来说,不需要 100% 的测试覆盖率,70% 就足够了。测试应该提升你的开发效率,虽然维护测试会暂时地阻塞你目前的开发,但当你不断地增加测试,会在不同阶段得到不同的回报。
我个人喜欢使用
Jest
,React testing library
,Cypress
,和Mock service worker
。
End
翻译的不好,请大家见谅。如有任何想法,欢迎评论交流
参考资料
[1]
https://github.com/mithi/react-philosophies: https://github.com/mithi/react-philosophies
[2]mithi: https://github.com/mithi
[3]【你不需要系列之“你不需要 Lodash/Underscore”】: https://github.com/you-dont-need/You-Dont-Need-Lodash-Underscore
[4]【你不需要系列之“你不需要 Momentjs”】: https://github.com/you-dont-need/You-Dont-Need-Momentjs
[5]【你不需要系列之“你不需要 JavaScript”】: https://github.com/you-dont-need/You-Dont-Need-JavaScript
················· 若川简介 ·················
你好,我是若川,毕业于江西高校。现在是一名前端开发“工程师”。写有《学习源码整体架构系列》20余篇,在知乎、掘金收获超百万阅读。
从2014年起,每年都会写一篇年度总结,已经坚持写了8年,点击查看年度总结。
同时,最近组织了源码共读活动,帮助3000+前端人学会看源码。公众号愿景:帮助5年内前端人走向前列。
扫码加我微信 ruochuan02、拉你进源码共读群
今日话题
略。分享、收藏、点赞、在看我的文章就是对我最大的支持~