如何写出优雅的 react 组件?

1,189 阅读5分钟

前言:写出一个能用的react组件很简单,但是如何写出一个优雅的组件呢?这就需要对组件有深入的理解。今天我们就来学习一下如何设计出一个好的react组件吧!

React 组件的分类, 社区中有非常经典的分类模式:

  • 把只作展示、独立运行、不额外增加功能的组件,称为哑组件,或无状态组件,还有一种叫法是展示组件
  • 把处理业务逻辑与数据状态的组件称为有状态组件,或灵巧组件,灵巧组件一定包含至少一个灵巧组件或者展示组件。

展示组件的复用性更强,灵巧组件则更专注于业务本身。

1.展示组件

展示组件内部是没有状态管理的,正如其名,就像一个个“装饰物”一样,完全受制于外部的 props 控制。展示组件具有极强的通用性,复用率也很高,往往与当前的前端工程关系相对薄弱,甚至可以做到跨项目级的复用。

代理组件

常用于封装常用属性,减少重复代码。是展示组件中最常用的代理组件,如:

const Button = props =>
  <button type="button" {...props}>

虽然进行封装感觉是多此一举,但切断了外部组件库的强依赖特性。在大厂中引入外部组件库需要考虑两点:

  • 如果当前组件库不能使用了,是否能实现业务上的无痛切换;
  • 如果需要批量修改基础组件的字段,如何解决?

代理组件的设计模式很好地解决了上面两个问题。从业务上看,代理组件隔绝了 Antd,仅仅是一个组件 Props API 层的交互。这一层如若未来需要替换,是可以保证兼容、快速替换的,而不需要在原有的代码库中查找修改。其次,如果要修改基础组件的颜色、大小、间距,代理组件也可以相对优雅地解决,使得这些修改都内聚在当前的 Button 组件中,而非散落在其他地方。

基于展示组件的思想,可以封装类似的其他组件,比如样式组件。

样式组件

样式组件也是一种代理组件,只是又细分了处理样式领域,将当前的关注点分离到当前组件内。

布局组件

布局组件的基本设计与样式组件完全一样,但它基于自身特性做了一个小小的优化。主要用于安放其他组件

<Layout
  Top={<NavigationBar />}
  Content={<Article />}
  Bottom={<BottomBar />}
/>

布局本身是确定的,不需要根据外部状态的变化去修改内部组件。所以这也是一个可以减少渲染的优化点。

由于布局组件无需更新,就可以通过写死shouldComponentUpdate 的返回值直接阻断渲染过程。对于大型前端工程,类似的小心思可以带来性能上的提升。当然,这也是基于代理组件更易于维护而带来的好处。

2.灵巧组件

由于灵巧组件面向业务,所以相对于展示组件来说,其功能更为丰富、复杂性更高,而复用度更低。展示组件专注于组件本身特性灵巧组件更专注于组合组件。那么最常见的案例则是容器组件。

容器组件

容器组件几乎没有复用性,它主要用在两个方面:拉取数据与组合组件

const CardList = ({ cards }) => (
  <div>
    {cards.map(card => (
      <CardLayout
        header={<Avatar url={card.avatarUrl} />}
        Content={<Card {...card} />}
      />
        {comment.body}-{comment.author}
    ))}
  </div>
);

class CardListContainer extends React.Component {
  state = { cards: [] }
  async componentDidMount() {
    const response = await fetch('/api/cards')
    this.setState({cards: response})
  }
  render() {
    return <CardList cards={this.state.cards} />
  }
}

像这样切分代码后,容器组件内非常干净,没有冗余的样式与逻辑处理,这也是采取了关注点分离的策略,

高阶组件

React 的官方文档将高阶组件称为 React 中复用组件逻辑的高级技术。高阶组件本身并不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。简而言之,高阶组件的参数是组件,返回值为新组件的函数。

这样听起来有一些高阶函数的味儿了。那什么是高阶函数呢?如果一个函数可以接收另一个函数作为参数,且在执行后返回一个函数,这种函数就称为高阶函数。在 React 的社区生态中,有很多基于高阶函数设计的库,比如 reselector 就是其中之一。

思想一脉相承,React 团队在组件方向也汲取了同样的设计模式。源自高阶函数的高阶组件,可以同样优雅地抽取公共逻辑。

抽取公共逻辑

const checkLogin = () => {
  return !!localStorage.getItem('token')
}

class CartPage extends React.Component {
   ...
}

class UserPage extends  React.Component {
  componentDidMount() {
    if(!checkLogin) {
      // 重定向跳转登录页面
    }
  }
  ...
}

class OrderPage extends  React.Component {
  componentDidMount() {
    if(!checkLogin) {
      // 重定向跳转登录页面
    }
  }
  ...
 }

虽然已经抽取了一个函数,但还是需要在对应的页面添加登录态的判断逻辑。然而如果有高阶组件的话,情况会完全不同。

const checkLogin = () => {
  return !!localStorage.getItem('token')
}

const checkLogin = (WrappedComponent) => {
          return (props) => {
              return checkLogin() ? <WrappedComponent {...props} /> : <LoginPage />;
          }

// 函数写法
class RawUserPage extends  React.Component {
  ...
}

const UserPage = checkLogin(RawUserPage)

// 装饰器写法
@checkLogin
class UserPage extends  React.Component {
 ...
}

@checkLogin
class OrderPage extends  React.Component {
  ...
}

链式调用

由于高阶组件返回的是一个新的组件,所以链式调用是默认支持的。基于 checkLogin 与 PV 两个例子,链式使用是这样的:

// 函数调用方式
class RawUserPage extends React.Component {
  ...
}

const UserPage = checkLogin(PV('用户页面')(RawUserPage))
// 装饰器调用方式
@checkLogin
@PV('用户页面')
class UserPage extends  React.Component {
  ...
}

在链式调用后,装饰器会按照从外向内、从上往下的顺序进行执行。 除了抽取公用逻辑以外,还有一种修改渲染结果的方式,被称为渲染劫持。

渲染劫持

渲染劫持可以通过控制 render 函数修改输出内容,常见的场景是显示加载元素,如下情况所示:

 function withLoading(WrappedComponent) {
    return class extends WrappedComponent {
        render() {
            if(this.props.isLoading) {
                return <Loading />;
            } else {
                return super.render();
            }
        }
    };
}

通过高阶函数中继承原组件的方式,劫持修改 render 函数,篡改返回修改,达到显示 Loading 的效果。

参考:

  1. 如何设计 React 组件
  2. react中文网