前言
为什么会提到一个抽象组件的概念,其实我们称其为高复用组件更好,因为其实在业务开发中很多时候会有这样的场景,我们的某部分功能是可以共用给其他部分的,但这部分又不太可能脱离组件或者某个基准数据存在。于是,我们需要将这部分代码进行一定的抽象或者说设计。
mixin
混入在其他编程语言中非常常见,在es6的语法中已经提到了装饰器的语法,其实装饰器就是混入的基本实现。下面我们实现下js版本的mixin。
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中也有类似的方式。不过由于这种方式会导致不灵活的使用,已经被高级组件所代替。
class App extends React.creatClass({
mixins:[fn1],
render(){
}
})
高级组件
点击跳转查看我的另一篇文章: 链接
属性代理
属性代理是我们最常见的使用方式,它可以将指定的属性传入,并返回带有这些属性的任意组件。点击查看我的codesanbox地址:链接
// 包装组件的容器
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
无论我们删除还是编辑属性的部分,我们都应该尽可能最高阶组件的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使用引用
在高阶组件中,我们可以通过refs来使用WrappedComponent的引用。看上去与上面的控制属性么有什么差别,实际上,每当子组件执行的时候,refs的回调函数就会执行,它可以方便的调用或者读取实例的props.换一句说法,这里可以实现调用子组件的方法,除了实现部分组件钩子,还可以根据需求灵活的进行一些方法调用。
觉得很没有想法,找不到什么场景下会有这种需求,给大家举个例子,比如容器组件想主动调用子组件的某个方法或者读取其某个值的状态,在我做业务开发的时候,就有一种场景,用户在容器组件的某个操作,需要主动刷新子组件的一些数据,还有执行子组件的一些事件,按照常规方式,是没有主动触发这一条的。因为我们的一般的通讯是通过子组件使用父组件的回调函数来实现的。那么假如是这种场景,我们直接封装一个这种需求的高级组件便可,然后在根据不断变更的需求,去维护固定的一个或者多个高阶组件。
// 包装组件的容器
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} />;
}
};
抽象state
这一层设计的原因是我们在考虑设计我函数组件还是状态组件时经常考量的一点,在react的组件设计思想中,我们判断的核心标准是组件本身是否有状态,是否需要根据数据的状态灵活的变化,也就是是否对setState的更新视图操作有强依赖,是否是多次渲染,如果有,那么是建议的使用带状态组件,否则建议你使用无状态组件,也就是函数组件。
但是我们在开发某些业务时,发现耦合了太多交互逻辑以及状态逻辑在组件中,而这些代码设计是可重用的。比如我们都是展示用户信息,都是点击某个位置,更新用户信息,只是展示的位置以及渲染有差异。那么我们该如何做?那就是抽象出这部分state,原来的组件变为函数组件。(如果你只有一个组件中这样,可以不必提取,如果出现多个,建议这样使用高阶组件抽象一次)。
// 包装组件的容器
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,但为了避免一些低级的问题,**我们不建议直接修改甚至删除其原具有的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 退出的原因
虽然我们知道mixin被慢慢的废弃,但是我们还是有必要了解下用这个的问题是那些?而显然新的高级组件是能解决这些问题的。这也将有益于我们理解一些高级组件设计的优势。
- 破坏了原有组件的封装,可能会增加其他的状态侵入以及可能的混入依赖关系
- 不同的mixins存在的命名冲突
- 增加了组件的复杂性:混入了各种方法以及为生命周期可以添加了不同方法
组合组件开发
它指的是当我们进行一些高阶组件的开发的时候,发现很多时候不断的去调整属性,同时为了减少对已经在使用的部分,一般是高级组件的属性都是增加,累加下去会导致配置了很多可能无用的属性。
组件再分离
也就是将组件进一步细分,每一个组件都可以尽可能的原子化,然后稍高阶的组件通过组装完成我们所看到的一个基本组件。比如下图理解下:
实际上这种思想,我们也偶尔会使用,只不过没有形成一些固定的思维设计思路。实际上,不管我们是设计的可重用的组件,还是说就是写业务组件,页面组件,我们都应该考虑组件的拆分。让每个组件内部尽可能的细化,拆分成若干具有单独解耦的独立渲染的逻辑或者子组件。
我们在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框架,设计好这些组件的组合方式,如何抽离等都是一个很考验你能力的部分。