拆分你的React组件

原文: Techniques for decomposing React components

拆分你的React组件

React的组件具有很强大的能力同时又具有足够的灵活性。当使用很多工具开发后,我们的React组件会就很容易变复杂,一方面,代码结构变得膨胀臃肿,另一方面,在功能上组件也做了太多的事情。
就像任何其他类变成一样,在开发时,我们要秉承着单一责任原则,这样不仅会使你的组件易于维护,还可以提高组件的可复用性。然而,面对一个大型的组件,如何去分割组件的功能不是很容易的。那这里介绍了三个从基础到高阶的方案让你开始去分解组件。

分割Render函数

这是一个很基础的分解组件功能的方法: 当一个组件渲染了太多的节点,分割这些节点到子组件中,这就是一种简单的简化方式。

class Panel extends React.Component {
  renderHeading() {
    // ...
  }

  renderBody() {
    // ...
  }

  render() {
    return (
      <div>
        {this.renderHeading()}
        {this.renderBody()}
      </div>
    );
  }
}

在分解之后,各部分内容有了自己的render函数,但它没有对一个组件进行真正的拆解。组件中的State,Props,Class Methods仍然是共享的,这样就很难去辨识组件中有哪些属性方法状态被哪个子组件使用。
为了真正的减少复杂度,我们应该创造一个全新的组件。对于更简单的子组件,我们可以通过函数式组件使组件的规模达到最小。

const PanelHeader = (props) => (
  // ...
);

const PanelBody = (props) => (
  // ...
);

class Panel extends React.Component {
  render() {
    return (
      <div>
        // Nice and explicit about which props are used
        <PanelHeader title={this.props.title}/>
        <PanelBody content={this.props.content}/>
      </div>
    );
  }
}

这是另外一种写法,代码变化不多,但这之间却有很重要的区别。我们使用隐晦的组件声明去替代直接的函数调用,这样我们产出了可以使用的更小的组件单元。这是因为 Panel的render函数返回了一个简单的子树,它的运行速度和PanelHeaderPanelBody一样快,比直接返回所有的element快的多。// ???没有具体测试过

并且,这么做也为测试提供了便利:可以使用shallow render为组件单元做独立的扩展测试。作为一个惊喜,React提出了Fiber结构,更小的单元将会使它渲染的更有效率。

通过属性传递组件的模版组件

如果因为太多的变量和配置数据使组件变得很复杂,那我们可以考虑通过设置一个或者多个slots槽,把一个组件改变成模版组件。这样,作为专用的父组件只会关注组件的配置。

举个例子,一个Comment组件可以有不同的action和其余信息,比如,你是不是作者,评论是不是被成功保存,或者你有什么权限。为了不让控制渲染逻辑的代码混入织渲染内容的Comment结构中,可以根据这两个关注点把它们独立的分开。通常React的属性不仅可以传递数据,还可以传递组件,因此我们可以灵活的创建组件。

class CommentTemplate extends React.Component {
  static propTypes = {
    // Declare slots as type node
    metadata: PropTypes.node,
    actions: PropTypes.node,
  };

  render() {
    return (
      <div
        <CommentHeading>
          <Avatar user={...}/>

          // Slot for metadata
          <span>{this.props.metadata}</span>

        </CommentHeading>
        <CommentBody/>
        <CommentFooter>
          <Timestamp time={...}/>

          // Slot for actions
          <span>{this.props.actions}</span>

        </CommentFooter>
      </div>
    );
  }
}

另一个组件则独自承担起计算添加到metadataactions组件的责任。

class Comment extends React.Component {
  render() {
    const metadata = this.props.publishTime ?
      <PublishTime time={this.props.publishTime} /> :
      <span>Saving...</span>;

    const actions = [];
    if (this.props.isSignedIn) {
      actions.push(<LikeAction />);
      actions.push(<ReplyAction />);
    }
    if (this.props.isAuthor) {
      actions.push(<DeleteAction />);
    }

    return <CommentTemplate metadata={metadata} actions={actions} />;
  }
}

仔细思考一下JSX,在一个组件的闭合标签内的任何东西都会被传入组件的children属性。当我们合理使用这个属性时,我们的组件会表现的特别清晰。通常来讲,它应该被用于传递组件最主要的内容。在评论的例子中,评论的内容就可以这样来写

<CommentTemplate metadata={metadata} actions={actions}>
  {text}
</CommentTemplate>

提取公用的部分到更高阶的组件

组件经常会被一些交叉的关注点污染,通常这些关注点所需要的功能与组件的主要功能并没有很直接的关联。

假设这样一种情况,当Document组件被点击时,你想要去发送一份分析数据。为了进一步提高我们这个例子的组件复杂性,我们需要分析数据中包含关于文档的一些信息,例如它的ID。最简单的解决方案可能就是DocumentcomponentDidMountcomponentWillUnmount方法,例如

class Document extends React.Component {
  componentDidMount() {
    ReactDOM.findDOMNode(this).addEventListener('click', this.onClick);
  }

  componentWillUnmount() {
    ReactDOM.findDOMNode(this).removeEventListener('click', this.onClick);
  }

  onClick = (e) => {
    if (e.target.tagName === 'A') { // Naive check for <a> elements
      sendAnalytics('link clicked', {
        documentId: this.props.documentId // Specific information to be sent
      });
    }
  };

  render() {
    // ...
  }
}

在这个例子中,有一些问题:
1.这个组件现在有一个额外的关注点,这个会模糊其组件的主要目的: 展示一个文档数据。
2.如果组件在这些生命钩子中有额外的逻辑,那主要的代码渲染逻辑也会被模糊。
3.这个收集数据的代码是不可复用的。
4.重构组件会变的更艰难,因为你不得不去维护这个额外的代码。

在这个例子中,我们可以使用高阶组件(HOC)去解构它,简短来说,它们是一类函数,可以被用到任何一个React组件上,包裹那个组件可以Mix一个指定的功能。

function withLinkAnalytics(mapPropsToData, WrappedComponent) {
  class LinkAnalyticsWrapper extends React.Component {
    componentDidMount() {
      ReactDOM.findDOMNode(this).addEventListener('click', this.onClick);
    }

    componentWillUnmount() {
      ReactDOM.findDOMNode(this).removeEventListener('click', this.onClick);
    }

    onClick = (e) => {
      if (e.target.tagName === 'A') { // Naive check for <a> elements
        const data = mapPropsToData ? mapPropsToData(this.props) : {};
        sendAnalytics('link clicked', data);
      }
    };

    render() {
      // Simply render the WrappedComponent with all props
      return <WrappedComponent {...this.props} />;
    }
  }

  return LinkAnalyticsWrapper;
}

这是非常关键的一点,就是说,这个函数并没有修改组件去添加一些行为而是返回一个新包裹的组件。返回的包裹的组件用于替换原来的Document组件

class Document extends React.Component {
  render() {
    // ...
  }
}

export default withLinkAnalytics((props) => ({
  documentId: props.documentId
}), Document);

注意:这里有一个非常关键的细节,就是传递data数据(documentId)是可以被提取出来,作为高阶组件的一个配置项。这让Document组件保持着对文档的数据的绝对控制权,并且将通用的click事件的监听放在了withLinkAnalytics 高阶组件中。

高级组件展现了React强大的特性。这个简单的例子根据单一责任原则,使一个紧耦合的组件代码能够被拆分,模块化。

高阶组件通常被应用到React的一些库中,例如react-redux,style-components和react-intl等等。毕竟,这些库的主要功能就是解决一个React App的通用的部分。另一个库,recompose,通过使用高阶函数进一步去分离组件的生命钩子。

最后的思考

通过我们的设计,React组件是可以被高度组合的。使通过解构和重新组合组件会给你带来好处的。
不要害怕去创建一个功能很小的组件。可能刚开始会很糟糕,但是写出的组件是更强大的和可复用的到。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值