视频1 视频21 视频41 视频61 视频文章1 视频文章21 视频文章41 视频文章61 推荐1 推荐3 推荐5 推荐7 推荐9 推荐11 推荐13 推荐15 推荐17 推荐19 推荐21 推荐23 推荐25 推荐27 推荐29 推荐31 推荐33 推荐35 推荐37 推荐39 推荐41 推荐43 推荐45 推荐47 推荐49 关键词1 关键词101 关键词201 关键词301 关键词401 关键词501 关键词601 关键词701 关键词801 关键词901 关键词1001 关键词1101 关键词1201 关键词1301 关键词1401 关键词1501 关键词1601 关键词1701 关键词1801 关键词1901 视频扩展1 视频扩展6 视频扩展11 视频扩展16 文章1 文章201 文章401 文章601 文章801 文章1001 资讯1 资讯501 资讯1001 资讯1501 标签1 标签501 标签1001 关键词1 关键词501 关键词1001 关键词1501 专题2001 知道1 知道21 知道41 知道61 知道81 知道101 知道121 知道141 知道161 知道181 知道201 知道221 知道241 知道261 知道281
问答文章1 问答文章501 问答文章1001 问答文章1501 问答文章2001 问答文章2501 问答文章3001 问答文章3501 问答文章4001 问答文章4501 问答文章5001 问答文章5501 问答文章6001 问答文章6501 问答文章7001 问答文章7501 问答文章8001 问答文章8501 问答文章9001 问答文章9501
分解React组件的几种进阶方法
2020-11-27 20:03:31 责编:小采
文档

React 组件魔力无穷,同时灵活性超强。我们可以在组件的设计上,玩转出很多花样。但是保证组件的Single responsibility principle: 单一原则非常重要,它可以使得我们的组件更简单、更方便维护,更重要的是使得组件更加具有复用性。本文主要和大家分享分解React 组件的几种进阶方法,希望能帮助到大家。

但是,如何对一个功能复杂且臃肿的 React 组件进行分解,也许并不是一件简单的事情。本文由浅入深,介绍三个分解 React 组件的方法。

方法一:切割 render() 方法

这是一个最容易想到的方法:当一个组件渲染了很多元素时,就需要尝试分离这些元素的渲染逻辑。最迅速的方式就是切割 render() 方法为多个 sub-render 方法。

看下面的例子会更加直观:

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

 renderBody() { // ...
 }

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

细心的读者很快就能发现,其实这并没有分解组件本身,该 Panel 组件仍然保持有原先的 state, props, 以及 class 方法。

如何真正地做到减少组件复杂度呢?我们需要创建一些子组件。此时,采用最新版 React 支持并推荐的函数式组件/无状态组件一定会是一个很好的尝试:

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

同之前的方式相比,这个微妙的改进是革命性的。

我们新建了两个单元组件:PanelHeader 和 PanelBody。这样带来了测试的便利,我们可以直接分离测试不同的组件。同时,借助于 React 新的算法引擎 React Fiber,两个单元组件在渲染的效率上,乐观地预计会有较大幅度的提升。

方法二:模版化组件

回到问题的起点,为什么一个组件会变的臃肿而复杂呢?其一是渲染元素较多且嵌套,另外就是组件内部变化较多,或者存在多种 configurations 的情况。

此时,我们便可以将组件改造为模版:父组件类似一个模版,只专注于各种 configurations。

还是要举例来说,这样理解起来更加清晰。

比如我们有一个 Comment 组件,这个组件存在多种行为或事件。

同时组件所展现的信息根据用户的身份不同而有所变化:

  • 用户是否是此 comment 的作者;

  • 此 comment 是否被正确保存;

  • 各种权限不同

  • 等等......

  • 都会引起这个组件的不同展示行为。

    这时候,与其把所有的逻辑混淆在一起,也许更好的做法是利用 React 可以传递 React element 的特性,我们将 React element 进行组件间传递,这样就更加像一个强大的模版:

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

    此时,我们真正的 Comment 组件组织为:

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

    metadata 和 actions 其实就是在特定情况下需要渲染的 React element。

    比如:

  • 如果 this.props.publishTime 存在,metadata 就是 <PublishTime time={this.props.publishTime} />;

  • 反之则为 <span>Saving...</span>。

  • 如果用户已经登陆,则需要渲染(即actions值为) <LikeAction /> 和 <ReplyAction />;

  • 如果是作者本身,需要渲染的内容就要加入 <DeleteAction />。

  • 方法三:高阶组件

    在实际开发当中,组件经常会被其他需求所污染。

    想象这样一个场景:我们想统计页面中所有链接的点击信息。在链接点击时,发送统计请求,同时这条请求需要包含此页面 document 的 id 值。

    常见的做法是在 Document 组件的生命周期函数 componentDidMount 和 componentWillUnmount 增加代码逻辑:

    class Document extends React.Component {
     componentDidMount() {
     ReactDOM.findDOMNode(this).addEventListener('click', this.onClick);
     }
    
     componentWillUnmount() {
     ReactDOM.findDOMNode(this).removeEventListener('click', this.onClick);
     }
    
     onClick = (e) => { // Naive check for <a> elements if (e.target.tagName === 'A') { 
     sendAnalytics('link clicked', { // Specific information to be sent
     documentId: this.props.documentId 
     });
     }
     };
    
     render() { // ...
     }
    }

    这么做的几个问题在于:

  • 相关组件 Document 除了自身的主要逻辑:显示主页面之外,多了其他统计逻辑;

  • 如果 Document 组件的生命周期函数中,还存在其他逻辑,那么这个组件就会变的更加含糊不合理;

  • 统计逻辑代码无法复用;

  • 组件重构、维护都会变的更加困难。

  • 为了解决这个问题,我们提出了高阶组件这个概念: higher-order components (HOCs)。不去晦涩地解释这个名词,我们来直接看看使用高阶组件如何来重构上面的代码:

    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) => { // Naive check for <a> elements if (e.target.tagName === 'A') { 
     const data = mapPropsToData ? mapPropsToData(this.props) : {};
     sendAnalytics('link clicked', data);
     }
     };
    
     render() { // Simply render the WrappedComponent with all props return <WrappedComponent {...this.props} />;
     }
     }
     ...
    }

    需要注意的是,withLinkAnalytics 函数并不会去改变 WrappedComponent 组件本身,更不会去改变 WrappedComponent 组件的行为。而是返回了一个被包裹的新组件。实际用法为:

    class Document extends React.Component {
     render() { // ...
     }
    }
    
    export default withLinkAnalytics((props) => ({
     documentId: props.documentId
    }), Document);

    这样一来,Document 组件仍然只需关心自己该关心的部分,而 withLinkAnalytics 赋予了复用统计逻辑的能力。

    高阶组件的存在,完美展示了 React 天生的复合(compositional)能力,在 React 社区当中,react-redux,styled-components,react-intl 等都普遍采用了这个方式。值得一提的是,recompose 类库又利用高阶组件,并发扬光大,做到了“脑洞大开”的事情。

    React 及其周边社区的崛起,让函数式编程风靡一时,受到追捧。其中关于 decomposing 和 composing 的思想,我认为非常值得学习。同时,对开发设计的一个建议是,一般情况下,不要犹豫将你的组件拆分的更小、更单一,因为这样能换来强健和复用。

    下载本文
    显示全文
    专题