React-组件解耦之道(译)

To be or not to be, that is a question.

React组件非常灵活可扩展,不过随着业务复杂度的增加和许多外部工具库的引入,组件会随着时间的推移而更加庞大,最终往往会变得浮肿。

如同任何其他种类的编程语言,遵守单一功能原则不仅使您的组件更易于维护,而且还可以实现更多的复用。然而,了解怎样分离一个庞大React组件的功能并不总是那么容易。下面有三种React组件解耦的方法,从最简单的到最高阶:

拆分 render() 方法

这是最显而易见的应用方法:当一个组件渲染太多的元素时,简化此类组件的一个简单方法是将这些元素分解为成逻辑子组件。

拆分rende()方法最常见和最快速的方法 是在同一个类中创建子渲染方法:

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

  renderBody() {
    // ...
  }

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

尽管这个方法有一定的作用,但是并没有分解组件本身。组件内所有的stateporps和类的方法仍然是共享的,因此很难确定他们是被哪个渲染方法使用。

要正真的减少组件的复杂度,就应该创建全新的组件来替代。为了简化子组件,函数式组件可以用来保持模板最小化。

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>
    );
  }
}

通过这种方法实现拆分组件由一个微妙但是非常重要的差别。通过将直接的方法调用替换为为间接的组件声明,我们产生了更小的单元来影响React。这是因为Panelrender()方法的返回值是一个元素索引树,它仅仅只执行到 PanelHeaderPanelBody,而不是它下面所有的元素。

这些对测试也有实际的意义:一个浅层渲染可以被用来轻松隔离这些单元,进行独立测试。当新的React
算法体系发布时,较小的单元将允许更高效的执行增量渲染

通过porps传递React元素来模板化组件

当一个组件因为多个变量或者配置变得更复杂时,就该考虑将这个组件转换成一个简单的拥有一个或者更多开放的接口的“模板”组件。这将使父组件的功能集中在配置这一块。

例如:一个Comment组件可能拥有不同的行为和显示不同的元数据,这取决于你是否是作者,评论是否成功保存,或者你拥有哪些权限。与其将Comment组件的结构(如何和在哪里呈现组件的内容)和处理所有可能变量的逻辑混合,不如考虑独立实现这两个问题。利用React的props传递元素的功能,而不仅仅是传递数据来创建一个灵活的模板组件。

组件模板

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>
    );
  }
}

接着,另外一个组件可以单独负责计算出填充元数据行为接口的内容

逻辑组件

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属性来传递。当该属性使用正确的时候,表现的尤为明显。为了符合语言习惯,它应该被保留用作内容的主要区域。在Comment这个例子当中,这个值应该为评论文本本身。

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

将公共内容提取到高阶组件中

组件经常会被与其主要目的不直接相关的交叉问题污染。

假设你想在Document组件中的任意超链接触发时发送分析数据,进一步复杂假设,发送的数据需要包含Document的一些信息,如它的ID。最显而易见的方法可能是在文档组件的生命周期方法componentDidMountcomponentWillUnmount中添加代码,如下:

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. 重新构建组件变得更加困难,因为你必须解决分析代码

拆分这样的内容可以通过高阶函数(HOCs)实现。 简而言之,这些方法可以应用于任何React组件,如果用所需的行为包裹该组件。

高阶函数

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);

请注意一个特殊的细节,发送什么样的数据(documentId)可以被HOC提取为配置。该数据使文件的作用域信息,Document组件和HOCwithLinkAnalytics点击事件的通用功能保持一致。

高阶组件展现了React组件的强大的组件化特性。这个简单的例子展示了如何将看起来紧耦合的代码如何被解耦成拥有单一功能的模块。

HOC经常被用在React库中,如:react-reduxstyled-componentsreact-intl。毕竟,这些库都是关于解决React应用通用方面的问题。另一个库,recompose,更进一步为和组件状态和生命周期方法所有方法使用HOC

###总结###

React组件被设计为高组合。通过易于解耦和组装他们来使用盖特点变为你的优点。不要因为创建小而功能专一的组件而害羞。可能在最开始写代码的时候感觉笨拙,但是写出来的代码将更强大,更易于复用。


原文地址Techniques for decomposing React components

Share Comments