作者简介: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丰富组件功能
高阶组件的概念来自于高阶函数,一般指的是将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机制来实现动态性。