前言
尽管我们现在已经有了各种各样的代码检测工具,例如 eslint,tslint,sonar等。但这些工具目前还只能检测一些比较简单的代码问题,对于一些复杂的模块和代码可读性问题,还是需要通过人去解决,只有大家达成统一的规范,有一定的共识,才能在一定程度上解决此类问题。
基于此,引出今天的主题:React最佳实践
变量篇
1.变量数量的定义
在变量定义的数量上,我们基本上很少对其做严格的限制。但就是基于此,经常会出现滥用变量的情况,这对后期的维护非常具有阻碍性。
// 👎 滥用变量
export const kpi = 4; // 定义好了之后再也没用过
function example() {
//一顿操作....
return;
}
// 👍 数据若不使用就删除掉,不然过三个月自己都不敢删,怕是不是哪里用到了
2.变量的命名
程序开发过程中变量命名不仅是一个头疼问题,同时也是一个对开发者综合素质的检验,它会直接影响到代码的最终交付质量、代码Review人员心智承受力。
在变量的命名中应尽量减少缩写的情况发生,做到见名知意。
// 👎 自我感觉良好的缩写:
let fName = 'jackie'; // 看起来命名挺规范,缩写,驼峰法都用上,ESlint各种检测规范的工具都通过,But,fName是啥?这时候,你是不是想说What are you 弄啥呢?
let rContent = 'willen';
// 👍 无需对每个变量都写注释,从名字上就看懂
let firstName = 'jackie';
let rightContent = 'willen';
3.特定的变量
对业务方面的变量,及时做好注释,减少无说明的变量,这对业务理解真的很重要!
// 👎 无说明的参数: 为什么要小于8,8表示啥?长度,还是位移,还是高度?
if (value.length < 8) {
....
}
// 👍 添加注释
const MAX_INPUT_LENGTH = 8;
if (value.length < MAX_INPUT_LENGTH) { // 一目了然,不能超过最大输入长度
....
}
4.使用说明性的变量
在函数的传参中,应避免使用使用长代码的方式,使用说明性的变量来代替可大大增加代码的可读性和可维护性。
// 👎 长代码不知道啥意思
const address = 'One Infinite Loop, Cupertino 95014';
const cityZipCodeRegex = /^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/;
saveCityZipCode(
address.match(cityZipCodeRegex)[1], // 这个公式到底要干嘛,对不起,原作者已经离职了。自己看代码
address.match(cityZipCodeRegex)[2], // 这个公式到底要干嘛,对不起,原作者已经离职了。自己看代码
);
// 👍 用变量名来解释长代码的含义
const address = 'One Infinite Loop, Cupertino 95014';
const cityZipCodeRegex = /^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/;
const [, city, zipCode] = address.match(cityZipCodeRegex) || [];
saveCityZipCode(city, zipCode);
5.变量的赋值
对变量赋值进行有效的兜底,也称为对异常情况的处理。在编写的过程中需要考虑对空、非正常情况下希望的赋值以及返回值,这是程序正常运转的基础条件。
// 👎 对于求值获取的变量,没有兜底
const MIN_NAME_LENGTH = 8;
let lastName = fullName[1];
if(lastName.length > MIN_NAME_LENGTH) { // 这样你就给你的代码成功的埋了一个坑,有考虑过这样的情况吗?这样程序一跑起来就爆炸...
fullName = ['jackie']
....
}
// 👍 对于求值变量,做好兜底
const MIN_NAME_LENGTH = 8;
let lastName = fullName[1] || ''; // 做好兜底,fullName[1]中取不到的时候,不至于赋值个undefined,至少还有个空字符,从根本上讲,lastName的变量类型还是String,String原型链上的特性都能使用,不会报错。不会变成undefined。
if(lastName.length > MIN_NAME_LENGTH) {
....
}
函数
1.函数命名
Quora 和 Ubuntu Forums thread 上的 4500 个程序员曾对程序编码中所遇到的问题进行投票。49%的程序员认为给函数,变量等命名是最难的任务。
个人认为如果通过看函数名字能明确的辨别函数意图,并返回什么数据,那就是合适的命名。
// 👎 从命名无法知道返回值类型
function showFriendsList() {....} // 现在问,你知道这个返回的是一个数组,还是一个对象,还是true or false。
function emlU(user) { .... } // 无法辨别函数意图
// 👍 明确函数意图,对于返回true or false的函数,最好以should/is/can/has开头
function shouldShowFriendsList() {...}
function isEmpty() {...}
function canCreateDocuments() {...}
function hasLicense() {...}
function sendEmailToUser(user) {.... } //动词开头,函数意图就很明显
2.功能函数最好为纯函数
纯函数是函数式编程中非常重要的一个概念,简单来说,就是一个函数的返回结果只依赖于它的参数,并且在执行过程中没有副作用,我们就把这个函数叫做纯函数。
并非所有的函数都需要纯函数. 例如: 操作DOM的事件处理函数就不适合使用纯函数. 不过, 这种事件处理函数, 可以调用其他纯函数来处理, 以此减少项目中不纯函数的数量.
使用纯函数的主要原因是可测试性和重构。
// 👎 不要让功能函数的输出变化无常
function plusAbc(a, b, c) { // 这个函数的输出将变化无常,因为api返回的值一旦改变,同样输入函数的a,b,c的值,但函数返回的结果却不一定相同。
var c = fetch('../api');
return a+b+c;
}
// 👍 功能函数使用纯函数,输入一致,输出结果永远唯一
function plusAbc(a, b, c) { // 同样输入函数的a,b,c的值,但函数返回的结果永远相同。
return a+b+c;
}
3.函数传参
对于函数的传参,应明确所调用函数的参数意义,可以大大减少传参位置错误导致的问题。
// 👎 传参无说明
page.getSVG(api, true, false); // true和false啥意思,一目不了然
// 👍 传参有说明
page.getSVG({
imageApi: api,
includePageBackground: true, // 一目了然,知道这些true和false是啥意思
compress: false,
})
4.函数拆解
一个函数完成一个独立的功能,不要一个函数混杂多个功能这是软件工程中最重要的一条规则。当函数需要做更多的事情时,它们将会更难进行编写、测试、理解和组合。
当你能将一个函数抽离出只完成一个动作,他们可以很容易的进行重构并且代码更容易阅读。如果能严格遵守本条规则,将会领先于许多开发者。
// 👎 函数功能混乱,一个函数包含多个功能
function sendEmailToClients(clients) {
clients.forEach(client => {
const clientRecord = database.lookup(client)
if (clientRecord.isActive()) {
email(client)
}
})
}
// 👍 功能拆解
function sendEmailToActiveClients(clients) { //各个击破,易于维护和复用
clients.filter(isActiveClient).forEach(email)
}
function isActiveClient(client) {
const clientRecord = database.lookup(client)
return clientRecord.isActive()
}
5.优先使用函数式编程
使用函数式编程时经常谈到的一个问题:性能问题。要不要为了一点点性能而牺牲可读性?
我的观念是:过早优化是万恶之源,你应该把可读性,可维护性,可测试性放到首位,而不是通过各种奇技淫巧来提升影响并不大的性能。
// 👎 使用for循环编程
for(i = 1; i <= 10; i++) {
a[i] = a[i] +1;
}
// 👍 使用函数式编程
let b = a.map(item => ++item)
6.函数中过多的采用if else
一部分的新手开发者在遇到逻辑判断时,习惯性的去用if else,这在逻辑简单时确实是可行的。
而当后期业务变更过程中,业务再次遇到逻辑判断时,我相信大部分人会保持之前的代码风格,在if中嵌套if else,这为后期维护埋下了坑点。
// 👎 if else过多
if (a === 1) {
...
} else if (a ===2) {
...
} else if (a === 3) {
...
} else {
...
}
// 👍 可以使用switch替代
switch(a) {
case 1:
....
case 2:
....
case 3:
....
default:
....
}
// 👍 或用对象替代
let handler = {
1: () => {....},
2: () => {....}.
3: () => {....},
default: () => {....}
}
handler[a]() || handler['default']()
组件
1.偏爱函数式组件
函数式组件 -- 它们有着更简单的语法,没有生命周期、构造函数等,可以用更简洁的代码来表达相同的逻辑,同时拥有更好的可读性。
除非需要用到错误边界,否则函数式组件应该是首选方法。
// 👎 Class组件实在太冗余
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>
)
}
}
// 👍 函数式组件代码简洁,可读性更好
function Counter() {
const [counter, setCounter] = useState(0)
handleClick = () => setCounter(counter + 1)
return (
<div>
<p>counter: {counter}</p>
<button onClick={handleClick}>Increment</button>
</div>
)
}
2.命名组件
始终对组件进行命名,即使采用默认导出的方式。
当阅读错误堆栈跟踪并使用React开发工具时,这种方式能帮我们快速的定位到错误。
如果组件的名称在文件中,在开发时也更容易找到组件所在的位置。
// 👎 无效命名
export default () => <form>...</form>
// 👍 对组件进行命名
export default function Form() {
return <form>...</form>
}
3.组织辅助函数
与该组件无关的辅助函数的实现应移到组件外部。理想位置是在组件定义之前,这样代码可以从上到下是可读的。
这大大减少了组件中的干扰性,只留下那些需要关注的东西,用仅依赖输入的纯函数组合逻辑可以更轻松地跟踪错误和扩展。
// 👎 避免嵌套应当移到组件外部的函数
function Component({ date }) {
function parseDate(rawDate) {
...
}
return <div>Date is {parseDate(date)}</div>
}
// 👍 对函数进行抽离
function parseDate(date) {
...
}
function Component({ date }) {
return <div>Date is {parseDate(date)}</div>
}
4.组件复杂度
如果一个函数做的事情太多,应适当提取一些逻辑并调用另一个函数。组件也是如此,如果该组件拥有太多功能,也应将其拆分为更小的组件。
如果提取的函数很复杂,则需要依照一定的规则和条件一一提取它。
代码行数并不是一个客观的衡量标准,更多是需要考虑责任划分和抽象。
5.代码注释
代码注释一直是一个令不少开发者头疼的问题,相信一段复杂的逻辑有注释和没有注释会让人产生不同的阅读体验(不要再抱怨他人的代码没有注释,从自己做起!)。
function Component(props) {
return (
<>
{/* 如果用户没有订阅则不展示广告 */}
{user.subscribed ? null : <SubscriptionPlans />}
</>
)
}
6.使用错误边界
一个组件中的错误不应导致整个 UI 崩溃。在极少数情况下,如果发生严重错误,我们希望删除整个页面或重定向。大多数情况下,如果我们只需要在页面上隐藏特定元素并展示降级UI就可以了。
在处理数据的函数中,可能有多个 try/catch 语句,亦或者没有及时对数据进行兜底,这个时候我们可以在数据处理的组件外使用错误边界,及时的提升用户体验。
function Component() {
return (
<Layout>
<ErrorBoundary>
<CardWidget />
</ErrorBoundary>
<ErrorBoundary>
<FiltersWidget />
</ErrorBoundary>
<div>
<ErrorBoundary>
<ProductList />
</ErrorBoundary>
</div>
</Layout>
)
}
7.解构Props
在函数式组件中,我们通常使用props来传递参数。但是当组件中全是重复的props传递时,此时它失去的它本身的意义。
不解构 props 的一个重要场景是需要区分什么是外部状态和什么是内部状态。但是在常规组件中,参数和变量之间没有区别。
// 👎 不要在组件中到处重复传递props
function Input(props) {
return <input value={props.value} onChange={props.onChange} {...props}/>
}
// 👍 对props解构并使用这些值
function Component({ value, onChange }) {
const [state, setState] = useState('')
return <div>...</div>
}
8.props传参数量
一个组件应该接收多少个 props 是一个主观的问题。组件拥有的 props 数量与它的工作量呈正相关,组件接收的props越多,它的责任就越大,大量的 props 是一个组件做得太多的信号。
如果超过 5 个props,就该考虑是否拆分该组件。在某些情况下,这是需要对组件进行重构的标志。
注意:组件使用的props越多,重新渲染的理由就越多。
9.传递对象而不是基础数据
限制props数量的一种方法是传递一个对象而不是基础数据。可以将它们组合在一起,而不是作为基础数据一一传递。
如果在后期 user 获得额外的字段,这也减少了需要更改的地方。
// 👎 如果值都是相关的,不用一个一个传递
<UserProfile
bio={user.bio}
name={user.name}
email={user.email}
subscription={user.subscription}
/>
// 👍 使用对象进行传递,在后期增改字段也更方便
<UserProfile user={user} />
10.避免嵌套三元运算符
三元运算符在第一级之后变得难以阅读,虽然看起来节省了代码空间,但最好在代码中明确意图,保持良好的阅读性。
// 👎 不够清晰,要是再嵌套一层两层呢
isSubscribed ? (
<ArticleRecommendations />
) : isRegistered ? (
<SubscribeCallToAction />
) : (
<RegisterCallToAction />
)
// 👍 将判断逻辑进行拆分
function CallToActionWidget({ subscribed, registered }) {
if (subscribed) {
return <ArticleRecommendations />
}
if (registered) {
return <SubscribeCallToAction />
}
return <RegisterCallToAction />
}
function Component() {
return (
<CallToActionWidget
subscribed={subscribed}
registered={registered}
/>
)
}
11.将列表组件封装成独立组件
遍历列表是非常常见的一种情况,通常用map函数完成。然而,在一个有很多参数的组件中,过分的细化反而会降低组件的可读性。
当需要对数据进行map时,可以将它们封装到自己的独立组件中,父组件不需要关心细节,只需要知道这是一个列表即可。
// 👎 列表渲染和其他逻辑杂糅在一起
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>
)
}
// 👍 将列表组件提取出来,一目了然
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>
)
}
12.对组件props赋予默认值
指定默认 prop 值的一种方法是将defaultProps属性附加到组件,这意味着组件函数及其参数的值不会放在一起。
当解构props时,我更喜欢直接赋予默认值,这样可以更轻松地从上到下阅读代码而无需任何跳转,并将定义和值保持在一起。
// 👎 将默认值定义在defaultProps中
function Component({ title, tags, subscribed }) {
return <div>...</div>
}
Component.defaultProps = {
title: '',
tags: [],
subscribed: false,
}
// 👍 直接在函数中赋予默认值
function Component({ title = '', tags = [], subscribed = false }) {
return <div>...</div>
}
13.避免嵌套渲染函数
当需要从组件或逻辑中提取组件时,不要将其放在同一个组件中的函数中。组件应该只有属于它本身的功能,以这种方式定义它是嵌套在其父级中。
这意味着该嵌套组件可以访问其父级的所有状态和数据。它使代码更难读(这个函数在所在组件之间做了什么?)
将它移动到它自己的组件中,命名它并传递props,而不是以闭包的形式存在。
// 👎 不要将其定义在渲染函数组件中
function Component() {
function renderHeader() {
return <header>...</header>
}
return <div>{renderHeader()}</div>
}
// 👍 将其抽离到独立的组件中去
import Header from '@modules/common/components/Header'
function Component() {
return (
<div>
<Header />
</div>
)
}
14.单标签闭合
在react中,若没有子节点,可以采用两种方式来闭合标签。但这难免不会让人产生歧义:该组件能接受children吗?若不能接受,请尽量使用单标签闭合的方式。
// 👎 不闭合标签:该组件难道支持children?
function Component() {
return <Card text="hello"></Card>
}
// 👍 单标签闭合
function Component() {
return <Card text="hello"/>
}
总结
本文主要从最常用的变量,函数,组件三个维度出发,逐层深入,属于该系列的第一篇文章。若大家有疑问或者觉得有异议的地方,欢迎评论留言。
参考资料: github.com/jackiewille…