为何会提到一个抽象组件的概念,其实咱们称其为高复用组件更好,由于其实在业务开发中不少时候会有这样的场景,咱们的某部分功能是能够共用给其余部分的,但这部分又不太可能脱离组件或者某个基准数据存在。因而,咱们须要将这部分代码进行必定的抽象或者说设计。javascript
混入在其余编程语言中很是常见,在es6的语法中已经提到了装饰器的语法,其实装饰器就是混入的基本实现。下面咱们实现下js版本的mixin。vue
function mixins(obj,mixins){
let newObj = obj;
newObj.prototype = Object.create(obj.prototype);
for(let p in mixins){
if(mixins.hasOwnProperty(p)){
newObj[p] = mixins[p];
}
}
return newObj;
}
复制代码
看完以后,发现其实现其实和lodash的assign以及underscore的extend方法很是相似。那么结合react,以前的方式是咱们在react的中能够定义一个mixins数组共享一些方法。在vue中也有相似的方式。不过因为这种方式会致使不灵活的使用,已经被高级组件所代替。java
class App extends React.creatClass({
mixins:[fn1],
render(){
}
})
复制代码
点击跳转查看个人另外一篇文章: 连接react
属性代理是咱们最多见的使用方式,它能够将指定的属性传入,并返回带有这些属性的任意组件。点击查看个人codesanbox地址:连接es6
// 包装组件的容器
import React from "react";
export const MyContainer = WrappedComponent =>
class extends React.PureComponent {
componentWillMount() {
this.setState({ type: 1 });
}
render() {
return <WrappedComponent type={this.state.type} />; } }; // 具体使用 直接用函数封装传递 import React from "react"; import { MyContainer } from "./MyContainer"; class Hoc extends React.PureComponent { constructor(props) { super(props); this.state = {}; } render() { let { type } = this.props; return <div>高阶组件{type}</div>; } } export default MyContainer(Hoc); 复制代码
不管咱们删除仍是编辑属性的部分,咱们都应该尽量最高阶组件的props作新的命名来防止混淆。例如咱们须要添加一个新的prop.因而咱们须要保留原有的属性,这是必要的。这样使用高级组件就可使用新的属性,而原有组件不使用的时候仍然是无损的。(其中对象的拓展符是很方便的,在不肯定有哪些属性或者属性很是多的时候,很是建议使用这个语法特性)。编程
// 包装组件的容器
import React from "react";
export const MyContainer = WrappedComponent =>
class extends React.PureComponent {
componentWillMount() {
}
render() {
const newProps = {
text:1
}
return <WrappedComponent {...this.props} {...newProps} />; } }; 复制代码
在高阶组件中,咱们能够经过refs来使用WrappedComponent的引用。看上去与上面的控制属性么有什么差异,实际上,每当子组件执行的时候,refs的回调函数就会执行,它能够方便的调用或者读取实例的props.换一句说法,这里能够实现调用子组件的方法,除了实现部分组件钩子,还能够根据需求灵活的进行一些方法调用。数组
以为很没有想法,找不到什么场景下会有这种需求,给你们举个例子,好比容器组件想主动调用子组件的某个方法或者读取其某个值的状态,在我作业务开发的时候,就有一种场景,用户在容器组件的某个操做,须要主动刷新子组件的一些数据,还有执行子组件的一些事件,按照常规方式,是没有主动触发这一条的。由于咱们的通常的通信是经过子组件使用父组件的回调函数来实现的。那么假如是这种场景,咱们直接封装一个这种需求的高级组件即可,而后在根据不断变动的需求,去维护固定的一个或者多个高阶组件。bash
// 包装组件的容器
import React from "react";
export const MyContainer = WrappedComponent =>
class extends React.PureComponent {
proc(WrappedComponentInstance) {
WrappedComponentInstance && WrappedComponentInstance.method();
}
render() {
const props = Object.assign({},this.props,{ref:this.proc.bind(this)});
return <WrappedComponent {...props} />; } }; 复制代码
这一层设计的缘由是咱们在考虑设计我函数组件仍是状态组件时常常考量的一点,在react的组件设计思想中,咱们判断的核心标准是组件自己是否有状态,是否须要根据数据的状态灵活的变化,也就是是否对setState的更新视图操做有强依赖,是不是屡次渲染,若是有,那么是建议的使用带状态组件,不然建议你使用无状态组件,也就是函数组件。架构
可是咱们在开发某些业务时,发现耦合了太多交互逻辑以及状态逻辑在组件中,而这些代码设计是可重用的。好比咱们都是展现用户信息,都是点击某个位置,更新用户信息,只是展现的位置以及渲染有差别。那么咱们该如何作?那就是抽象出这部分state,原来的组件变为函数组件。(若是你只有一个组件中这样,能够没必要提取,若是出现多个,建议这样使用高阶组件抽象一次)。app
// 包装组件的容器
import React from "react";
export const MyContainer = WrappedComponent =>
// 在这个组件中完成全部的数据变动 和 交互逻辑,完成后属性传递给渲染组件便可
class extends React.PureComponent {
constructor(props){
super(props);
this.state ={
//xxx
}
}
method1(){
}
method2(){
}
render() {
const newProps = {};
return <WrappedComponent {...this.props} {...newProps} />; } }; 复制代码
实际上咱们除了上面的用途,还能够根据本身的须要去灵活的对组件的样式,外层空间,等任意的自定义。好比咱们常常须要对一些组件规定它的大小位置,或者就是指定一些有规律的className.剩下的空间自行发挥,这里只是提醒你们高阶组件有如此的一个使用场景。
// 封装函数里的返回class render函数里
render(){
return <div className="side-bar">
<WrappedComponent className="side-bar-content" {...this.props} {...newProps} />
</div>;
}
复制代码
说的简单一点就是在封装高级组件的时候对包装组件使用继承。其基本的写法以下:
const MyContainer = WrapperComp =>(
class extends WrapperComp{
render(){
return super.render()
}
}
)
复制代码
这种方法与属性代理不一样,它能够经过super方法来获取组件的属性以及方法。下面会详细说明其带来的两个特色:渲染劫持以及state控制。在了解这个以前,咱们有必要了解下其生命周期以及其会带来的影响。
备注:同名的方法以及生命周期,若是你再次申明会被覆盖。你能够经过我写的hocSuper的例子查看这个问题。
说的直白一点就是控制如何渲染原来已经肯定好的输出渲染的某部分,咱们在许多业务中其实已经加入了相似的代码,好比 hasRight && .只不过如今咱们的场景是把这部分的代码用在反向继承中的组件上。它的代码多是下面这样的。
render(){
return(
<div> {hasRight ? return super.render(): <span>无权限提示文本</span>} </div>
)
}
复制代码
固然上面的控制看起来很是简单,没有什么华丽的技巧,咱们更须要的多是下面这样的渲染劫持。拿到渲染的树以后,咱们改变其某些节点的状态。
render(){
const elementsTree = super.render();
let newProps = {};
const props = Object.assign({},elementsTree.props,newProps);
const newElementsTree= React.cloneElement(elementsTree,props,props.children);
return newElementsTree
}
复制代码
咱们可在高阶组件中删除或者修改组件的state,但为了不一些低级的问题,**咱们不建议直接修改甚至删除其原具备的state,更建议的方式是新建以及重命名。**若是你不确认原来的组件具备哪些属性以及方法,能够尝试着用JSON.stringify来序列化展现,固然更好的方式你能够经过开发工具好比devTool去查看这些。
当咱们用高级组件时,咱们失去了原来组件的名字,咱们能够经过简单的命名规则为HOC${getDisplayName(WrappedComponent)}
来实现,其中getDisplayName函数写法能够参考下面的方式:或可使用 recompose 库,它已经帮咱们实现了相应的方法。
function getDisplayName(WrappedComponent){
return WrappedComponent.dispalyName || WrappedComponent.name || 'Component';
}
// recompose 方法
// Any Recompose module can be imported individually
import getDisplayName from 'recompose/getDisplayName'
ConnectedComponent.displayName = `connect(${getDisplayName(BaseComponent)})`
// Or, even better:
import wrapDisplayName from 'recompose/wrapDisplayName'
ConnectedComponent.displayName = wrapDisplayName(BaseComponent, 'connect')
复制代码
有不少时候,咱们给高级组件添加一些灵活的参数,而不只仅是使用组件做为参数,那么咱们多一层嵌套便可实现。
import React, { Component } from 'React';
function HOCFactoryFactory(...params) {
// 能够作一些改变 params 的事
return function HOCFactory(WrappedComponent) {
return class HOC extends Component { render() {
return <WrappedComponent {...this.props} />; } } } //当你使用的时候,能够这么写: HOCFactoryFactory(params)(WrappedComponent) // 或者 @HOCFatoryFactory(params) class WrappedComponent extends React.Component{} 复制代码
高阶组件属于函数式编程(functional programming)思想,对于被包裹的组件时不会感知到高阶组件的存在,而高阶组件返回的组件会在原来的组件之上具备功能加强的效果。
虽然咱们知道mixin被慢慢的废弃,可是咱们仍是有必要了解下用这个的问题是那些?而显然新的高级组件是能解决这些问题的。这也将有益于咱们理解一些高级组件设计的优点。
它指的是当咱们进行一些高阶组件的开发的时候,发现不少时候不断的去调整属性,同时为了减小对已经在使用的部分,通常是高级组件的属性都是增长,累加下去会致使配置了不少可能无用的属性。
也就是将组件进一步细分,每个组件均可以尽量的原子化,而后稍高阶的组件经过组装完成咱们所看到的一个基本组件。好比下图理解下:
实际上这种思想,咱们也偶尔会使用,只不过没有造成一些固定的思惟设计思路。实际上,无论咱们是设计的可重用的组件,仍是说就是写业务组件,页面组件,咱们都应该考虑组件的拆分。让每一个组件内部尽量的细化,拆分红若干具备单独解耦的独立渲染的逻辑或者子组件。
咱们在ant的input组件中,能够看到其组件目录每个文档上基本的组件都是有入口文件,若干的小组件拼装而成。
咱们在写组件的时候也要有这样的思惟模式,好比一个帐单的显示,本来是这样的:
// old way
render (){
return (
<div>
<h2>标题</h2>
// 列表数据的渲染
{list.map(item)=>(<div className="m-docItem">
<img src={item.headimg}/>
<span>{item.docName}</span>
<p>{item.resume}</p>
</div>)}
</div>
)
}
//new way as a class fun
renderDocItem(list){
return ({
list.map(item)=>(<div className="m-docItem">
<img src={item.headimg}/>
<span>{item.docName}</span>
<p>{item.resume}</p>
</div>)
})
}
// new way as a single fun comp
export const RenderDocItem(props){
const {list}= props;
return ({
list.map(item)=>(<div className="m-docItem">
<img src={item.headimg}/>
<span>{item.docName}</span>
<p>{item.resume}</p>
</div>)
})
}
render(){
return(
<div>
<h2>标题</h2>
// 方法的方式
{this.renderDocItem(list)}
// 函数组件的方式
<RenderDocItem list={list}/>
</div>
)
}
复制代码
咱们在库中也常常看到这样的代码维护方式:养成这样的编码习惯,会让你的代码可维护性大大的加强。
好比咱们针对输入框的值进行监听以后执行某个特定的事件,而这个事件自己发现可重用的位置不少,和输入框自己是没有重度关联的,那么针对这个场景,若是你有强迫症,能够抽象一波。
// 完成 SearchInput 与 List 的交互
const searchDecorator = WrappedComponent => {
class SearchDecorator extends Component {
constructor(props) {
super(props);
this.handleSearch = this.handleSearch.bind(this);
}
handleSearch(keyword) {
this.setState({
data: this.props.data,
keyword, });
this.props.onSearch(keyword);
}
render() {
const { data, keyword } = this.state;
return (
<WrappedComponent {...this.props} data={data} keyword={keyword} onSearch={this.handleSearch} /> ); } } return SearchDecorator; } 复制代码
图解组合组件开发的架构
经过本文但愿咱们能了解到高阶组件的一些基本设计思路,能解决的组件组合的痛点。使用react越久咱们越会发现,对于一个比较复杂的系统,若是你有特别思考过组件的可重用这个问题,而不只仅是一个页面一个组件,附加基本的ui框架,设计好这些组件的组合方式,如何抽离等都是一个很考验你能力的部分。