React 开发思想纲领

概要

  1. 介绍
  2. 最低要求
  3. 面向幸福设计
  4. 性能优化技巧
  5. 测试原则
  6. 英文版阅读

0 介绍

《React 开发思想纲领》是:

  • 我开发 React 时的一些思考
  • 每当我 review 他人或自己的代码时自然而然会思考的东西
  • 仅仅作为参考和建议,并非严格的要求
  • 会随着我的经验不断更新
  • 大多数技术点是基础的重构方法论SOLID 原则以及极限编程等思想的变体,仅仅是在 React 中的实践而已

你可能会觉得我写的这些非常基础。但以下示例都来自一些复杂大型项目的线上代码。

《React 开发思想纲领》的灵感来源于我实际开发中遇到的各种场景。

1 最低要求

1.1 计算机比你更「智能」

  1. 使用 ESLint 来静态分析你的代码,开启 rule-of-hooksexhaustive-deps 这两个规则来捕获 React 错误。
  2. 开启 JS 严格模式吧,都 2202 年了。
  3. 直面依赖,解决在useMemouseCallbackuseEffectexhaustive-deps 规则提示的 warning 或 error 问题。可以将最新的值挂在 ref 上来保证这些 hook 在回调中拿到的都是最新的值,同时避免不必要的重新渲染。
  4. 使用 map 批量渲染组件时,都加上 key
  5. 只在最顶层使用 hook,不要在循环、条件或嵌套语句中使用 hook。
  6. 理解不能对已经卸载的组件执行状态更新的控制台警告。
  7. 给不同层级的组件都添加错误边界(Error Boundary)来防止白屏,还可以用它来向错误监控平台(比如 Sentry)上报错误,并设置报警。
  8. 不要忽略了控制台中打印的错误和警告。
  9. 记得要 tree-shaking!
  10. 使用 Prettier 来保证代码的格式化一致性!
  11. 使用 TypescriptNextJS这样的框架来提升开发体验。
  12. 强烈推荐 Code Climate(或其他类似的)开源库。这类工具会自动检测代码异味(Code Smell,代码中的任何可能导致深层次问题的症状),它可以促使我去处理项目里留下的技术债。

1.2 Code is just a necessary evil

译者注:程序员的目标是解决客户的问题,代码只是副产品

1.2.1 先思考,再加依赖

依赖加的越多,提供给浏览器的代码就越多。扪心问问自己,你是否真的使用了某个库的 feature?

你真的需要它吗? 看看这些你可能不需要的依赖

  1. 你是否真的需要 Redux?有可能需要,但其实 React 本身也是一个状态管理库
  2. 你是否真的需要 Apollo clientApollo client 有许多很强大的功能,比如数据规范化。但使用的同时也会显著提高包体积。如果你的项目使用的并非是 Apollo client 特有的 feature,可以考虑使用一些轻量的库来替代,比如 react-querySWR(或者根本不用)。
  3. Axios 呢?Axios 是一个很棒的库,它的一些特性不容易通过原生的 fetch API 来复刻。但是如果使用 Axios 只是因为它有更好的 API,完全可以考虑在 fetch 上做一层封装(比如 redaxios 或自己实现)。取决于你的 App 是否真正地使用了 Axios 的核心 feature。
  4. Decimal.js 呢?或许 Big.js 或者其他轻量的库就足够了。
  5. Lodash/underscoreJS呢?推荐你看看【你不需要系列之“你不需要 Lodash/Underscore”】[3]。
  6. MomentJS呢?【你不需要系列之“你不需要 Momentjs”】[4]。
  7. 你不需要为了主题(浅色/深色模式)而使用 Context,考虑下用 css 变量 代替。
  8. 你甚至不需要 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 作为一个依赖项

你不用把 setStatedispatch 放在 useEffectuseCallback 这些 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)), [])

小技巧: 如果你的 useMemouseCallback 没有任何依赖,那你可能用错了

# ❌ 不太好  
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 面向幸福设计

太长不看版

  1. 通过删除冗余的状态来减少状态管理的复杂性。
  2. “传递香蕉,而不是拿着香蕉的大猩猩和整个丛林“(意思是组件要什么传什么,不要传大对象)。
  3. 让你的组件小而简单 —— 单一职责原则。
  4. 复制比错误的抽象要“便宜”的多(避免提早/不恰当的设计)。
  5. 避免 prop 层层传递(又叫 prop 钻取,prop drilling)。Context 不是解决状态共享问题的银弹。
  6. 将巨大的 useEffect 拆分成独立的小 useEffect
  7. 将逻辑提取出来都放到 hook 和工具函数中。
  8. useCallback, useMemouseEffect 依赖数组中的依赖项最好都是基本类型。
  9. 不要在 useCallback, useMemouseEffect 中放入太多的依赖项。
  10. 为了简单起见,如果你的状态依赖其他状态和上次的值,考虑使用 useReducer,而不是使用很多个 useState
  11. Context 不一定要放在整个 app 的全局。把 Context 放在组件树中尽可能低的位置。同样的道理,你的变量,注释和状态(和普通代码)也应该放在靠近他们被使用的地方。

2.1 删除冗余的状态来减少状态管理的复杂性

冗余的状态指可以通过其他状态经过推导得到的状态,不需要单独维护(类似 Vue computed),当你有冗余的状态时,一些状态可能会丢失同步性,在面对复杂交互的场景时,你可能会忘记更新它们。

删除这些冗余的状态,除了避免同步错误外,这样的代码也更容易维护和推理,而且代码更少。

2.2 “传递香蕉,而不是拿着香蕉的大猩猩和整个丛林“

为了避免掉入这种坑,最好将基本类型(boolean, string, number 等)作为 props 传递。(传递基本类型也能更好的让你使用 React.memo 进行优化)

组件应该仅仅只了解和它运作相关的内容就足够了。应该尽可能地与其他组件产生协作,而不需要知道它们是什么或做什么。

这样做的好处是,组件间的耦合会更松散,依赖程度会更低。低耦合更利于组件修改,替换和移除,而不会影响其他组件。

2.3 让你的组件小而简单

什么是「单一职责原则」?

一个组件应该有且只有一个职责。应该尽可能的简单且实用,只有完成其职责的责任。

具有各种职责的组件很难被复用。几乎不可能只复用它的部分能力,很容易与其他代码耦合在一起。那些抽离了逻辑的组件,改起来负担不大而且复用性更强。

如何判断一个组件是否符合单一职责?

可以试着用一句话来描述这个组件。如果它只负责一个职责,描述起来会很简单。如果描述中出现了“和“或“或”,那么这个组件很大概率不是单一职责的。

检查组件的 state,props 和 hooks,以及组件内部声明的变量和方法(不应该太多)。问问自己:是否这些内容必须组合到一起这个组件才能工作?如果有些不需要,可以考虑把它们抽离到其他地方,或者把这个大组件拆解成小组件。

3 性能优化技巧

  1. 如果你觉得应用速度慢,就应该做一次基准测试(benchmark)来证明。 "面对模凌两可的情况,拒绝猜测。" 多使用 Chrome 插件 - React 开发者工具的 profiler!
  2. useMemo 主要用在大开销的计算上。
  3. 如果你打算使用 React.memo, useMemo, 和 useCallback 来减少重新渲染,它们不该有过多的依赖项,且这些依赖项最好都是基本类型。
  4. 确保你清楚代码里 React.memo, useCallbackuseMemo 它们都是为了什么而使用的(是否真的能防止重新渲染?是否能证明在这些场景中真的可以显著提高性能? Memoization 有时会起到反作用,所以需要关注!)
  5. 优先修复慢渲染,再修复重新渲染。
  6. 把状态尽可能地放在它被使用的地方,一方面让代码读起来更顺,另一方面,能让你的 app 更快(state colocation(状态托管))
  7. Context 应该按逻辑分开,不要在一个 provider 中管理多个 value。如果其中某个值变化了,所有使用该 context 的组件(即便没有用到这个值),都会重新渲染。
  8. 可以通过拆分 statedispatch 来优化 context
  9. 了解下 lazy loading(懒加载)bundle/code splitting(代码分割)
  10. 长列表请使用 tannerlinsley/react-virtual 或其它类似的库。
  11. 包体积越小,app 越快。你可以使用 source-map-explorer 或者 @next/bundle-analyzer(用于 NextJS) 来进行包体积分析。
  12. 关于表单的库,推荐使用 react-hook-forms,它在性能和开发体验各方面都做的比较好。

4 测试原则

  1. 测试应该始终与软件的使用方式相似。
  2. 确保不是在测试一些边界细节(用户不会使用,看不到甚至感知不到的内容)。
  3. 如果你的测试不能让你对自己的代码产生信任,那测试就是无意义的。
  4. 如果你正在重构某个代码,且最后实现的功能都是完全一致的,其实几乎不需要修改测试,而且可以通过测试结果来判定你正确的重构了。
  5. 对于前端来说,不需要 100% 的测试覆盖率,70% 就足够了。测试应该提升你的开发效率,虽然维护测试会暂时地阻塞你目前的开发,但当你不断地增加测试,会在不同阶段得到不同的回报。
  6. 我个人喜欢使用 JestReact testing libraryCypress,和 Mock service worker

英文版阅读

英文版地址:https://md.tuboshu.mobi/note/1509740278

翻译自:https://github.com/mithi/react-philosophies 原文作者:mithi