前端React最佳实践

2,162 阅读12分钟

前言

尽管我们现在已经有了各种各样的代码检测工具,例如 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, truefalse); // true和false啥意思,一目不了然

// 👍 传参有说明
page.getSVG({
 imageApi: api,
 includePageBackgroundtrue// 一目了然,知道这些true和false是啥意思
 compressfalse,
})

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…