React组件设计之高阶函数和插件机制

时间:2022-04-25
本文章向大家介绍React组件设计之高阶函数和插件机制,主要内容包括HOC的简单定义、结语、基本概念、基础应用、原理机制和需要注意的事项等,并结合实例形式分析了其使用技巧,希望通过本文能帮助到大家理解应用这部分内容。

作者简介:slashhuang 研究型程序员 现就职于爱屋吉屋

React技术栈已成为大部分互联网公司的标配。关于React组件设计,大家经常谈的是高阶组件、props等等,市面上关于组件设计的文章也相对较少。本文笔者将从高阶组件和插件设计的角度,阐述在React项目中个人的一些组件设计心得。

一个基本的React组件

我们从简单的代码着手,进行React组件的讨论。

import { Component,PropTypes },React from 'react';import {render} from 'react-dom';class FirstComponent extends Component{    constructor(){        super();        this.state={ text: 'hello world' }    }    //描述数据类型     static propTypes={        velocity: PropTypes.number,//滚动速度    }    //描述默认的props    static defaultProps={        velocity:500    }    clickFunc(){        this.setState({text:'I am clicked'})    }    render(){        return <div onClick={::this.clickFunc}>                 {this.state.text}               </div>    } };

一个信息完备的React组件,一般都具备上述这种结构,这样的结构具备以下几个功能点。

1.采用propTypes来描述组件props的数据类型和含义。2.采用this.state来描述组件内部的数据结构。

这样的一个组件已经能够覆盖业务层面的大部分功能。

它的不足之处在于太不灵活。别的开发者必须通过修改源码的形式增加组件功能。

如果这个组件被多处复用,那么修改源码将会是一件危险的事情。

那么问题来了,怎么在不修改源码的基础上为组件增加功能呢?

下面我们从高阶组件和插件机制来增加组件的灵活性。

高阶组件HOC丰富组件功能

HOC的简单定义

高阶组件的概念来自于高阶函数,一般指的是将ReactComponent 作为参数,同时,函数的return值也为ReactComponent的转换模式。

一个基本的高阶组件写法如下

const HOC = (嵌入逻辑)=>(目标组件)=>{    return 增加功能后的新组件}

HOC的第一个参数是我们要嵌入的逻辑,目标组件则是我们要改造的组件,最后这个HOC返回出来一个增加功能后的新组件,这个新组件就是在目标组件的基础上修改过功能的组件。

接下来,我们采用如上HOC的逻辑来动态修改React组件的内部方法、props和state。

引入HOC来修改React组件内部方法

为了表达更加直观,我们来实现一个具体的业务场景。

我们定义如下高阶函数fn,使得InnerComponent目标组件在每次click后都能在控制台打印日志。

HOC = hookFn=>InnerComponent=>newComponent 为了侵入InnerComponent的逻辑,我们需要在原来InnerComponent.prototype的基础上,嵌入hookFn的逻辑。

const highOrderFunc=hookFn=>InnerComponent=>{       //引用目标组件原型        let ref = InnerComponent.prototype;        let cache = ref['clickFunc'];        //修改原型,hook我们自定义的功能        ref['clickFunc']=function(...args){            cache.apply(this,args)            hookFn()        }        return InnerComponent}

如上,我们就在保持原来 InnerComponent.prototype['clickFunc']方法逻辑的基础上,增加了hookFn的逻辑。比如我定义hookFn = console.log('clicked'),就可以实时记录用户的点击事件。

下面我们将这个简单逻辑完整组装起来。

@highOrderFunc(()=>console.log('hook click called'))class FirstComponent extends Component{    constructor(){        super();        this.state={ text: 'hello world' }    }    clickFunc(){        this.setState({text:'I am clicked'})    }    render(){        return <div onClick={::this.clickFunc}>                  {this.state.text}               </div>    } };render(<FirstComponent />,document.getElementById('root'))

当我们做了如上操作后,点击FirstComponent的时候,即可在控制台打印hook click called。

如果我们见微知著,将hookFn逻辑改成一段前端打点,即可实现产品经理经常要求的打点功能,并且对原来的FirstComponent逻辑没有任何侵入。

关于如上的代码需要说明的是,@符号是ES7的decorator语法,在高阶组件中使用会显得比较简洁,这里不多做介绍。

如上例子演示的是HOC通过修改组件的prototype,来实现对事件逻辑的侵入。

下面我们继续写代码,采用HOC来实现对组件props和state的侵入。

引入HOC修改state和props

同样,为了表达直观,我们来实现react-redux中,通过connect将action挂载在props上的逻辑。

我们定义如下高阶组件,使得newComponent的this.props能够访问actions。

HOC = actions=>InnerComponent=>newComponent 为了侵入InnerComponent的this.props,我们需要将InnerComponent包裹一层,以便在render的时候,this.props上能够拿到actions。

我们定义一个Wrapper组件来包裹InnerComponent,并且在Wrapper的componentDidMount时机,修改InnerComponent.prototype来完全覆盖InnerComponent原来的click逻辑。

    class Wrapper extends Component{        componentDidMount(){             let _ref = this.refs.InnerComponent;             //覆盖原来组件的click逻辑             _ref.__proto__.clickFunc = function(...args){                this.props.Update();                let { text } = this.state;                this.setState({text:`add Text ${text}`})             }        }        render(){            //侵入props数据            this.props = {                ...this.props,                ...actions            };            return <InnerComponent ref='InnerComponent'                                  {...this.props}/>        }    }    return Wrapper;}

如上的HOC返回的Wrapper组件在UI展示上和InnerComponent一模一样。同时,Wrapper在render的时候,this.props动态添加了actions传入InnerComponent。最后,InnerComponent的click逻辑clickFunc也被覆盖,因而在click的时候可以执行this.props.Update()逻辑。

@HOC({Update:()=>console.log('Update')})class InnerComponent extends Component{    constructor(){        super()        this.state={ text: 'hello world' }    }    clickFunc(){        this.setState({text:'I am clicked'})    }    render(){        return <div onClick={()=>this.clickFunc()}>                {this.state.text}</div>    } };

如上即为第二个例子的完整演示。我们通过高阶组件HOC实现了对InnerComponent的事件及props侵入。事实上,第二个例子的实现已经非常类似react-redux中的connect的功能了。

阶段性小结 HOC的核心思路是夺取目标组件的控制权,将逻辑、props、state修改交给HOC。 目标组件的控制权转移给HOC是它的核心。 讲完HOC,接下来我们从props设计的角度来审视React组件设计

由于在前端开发中,UI改版是一个经常碰到的需求。因此,React组件设计需要兼顾功能和UI侵入。我们通过定义Plugins接口,来定制可拔插的插件体系。

同样,为了表达直观,我们来实现一个Slider中底部文案的样式修改。

定义Plugins接口实现插件体系

class Slider extends Component{    renderPlugins(){        let { Plugins } = this.props;        let dataModel = {...this.props,...this.state};        return do{            if(typeof Plugins=='function'){;                <Plugins  dataModel={dataModel}/>            }else{                Plugins;            }        }    }    render(){        return <div>                hello world                {Plugins && this.renderPlugins() }            </div>    }};

如上,给Slider组件默认提供一个Plugins接口,这个Plugins的props是Slider的props和state数据集合。这样的一个模型即可完成当Slider的props更新、click事件发生的时候,Plugins能够拿到所有的数据,从而完成plugins层面的UI更新。

这种机制重要的一点是对Slider组件原来的逻辑无侵入。

当开发者需要修正UI样式的时候,直接定义Plugins即可完成这个工作。

关于如上的代码需要说明的是,代码中的do expression是babel-stage-0的语法,对于React组件中的条件分支处理非常直观。

结语

这篇文章洋洋洒洒都写了快200行了,感谢大家能够读到这里。关于React的组件设计,这边主要是采用高阶组件和Plugin机制来实现动态性。