{T}

高阶组件

学习目标

  • 掌握 高阶组件功能说明-如何编写高阶组件?
  • 掌握 1 强化props
  • 掌握 2 控制渲染

高阶组件功能说明-如何编写高阶组件?

1 强化props

强化 props 就是在原始组件的 props 基础上,加入一些其他的 props ,强化原始组件功能。举个例子,为了让组件也可以获取到路由对象,进行路由跳转等操作,所以 React Router 提供了类似 withRouter 的 HOC 。

js
function withRouter(Component) {
  const displayName = `withRouter(${Component.displayName || Component.name})`;
  const C = props => {
      /*  获取 */
    const { wrappedComponentRef, ...remainingProps } = props;
    return (
      <RouterContext.Consumer>
        {context => {
          return (
            <Component
              {...remainingProps} // 组件原始的props 
              {...context}        // 存在路由对象的上下文,history  location 等 
              ref={wrappedComponentRef}
            />
          );
        }}
      </RouterContext.Consumer>
    );
  };

  C.displayName = displayName;
  C.WrappedComponent = Component;
  /* 继承静态属性 */
  return hoistStatics(C, Component);
}
export default withRouter

流程分析:

  • 分离出 props 中 wrappedComponentRef 和 remainingProps , remainingProps 是原始组件真正的 props, wrappedComponentRef 用于转发 ref。
  • 用 Context.Consumer 上下文模式获取保存的路由信息。( React Router 中路由状态是通过 context 上下文保存传递的)
  • 将路由对象和原始 props 传递给原始组件,所以可以在原始组件中获取 history ,location 等信息。

2 控制渲染

渲染劫持

HOC 反向继承模式,可以通过 super.render() 得到 render 之后的内容,利用这一点,可以做渲染劫持 ,更有甚者可以修改 render 之后的 React element 对象。

js
const HOC = (WrapComponent) =>
  class Index  extends WrapComponent {
    render() {
      if (this.props.visible) {
        return super.render()
      } else {
        return <div>暂无数据</div>
      }
    }
  }

修改渲染树

js
class Index extends React.Component{
  render(){
    return <div>
       <ul>
         <li>react</li>
         <li>vue</li>
         <li>Angular</li>
       </ul>
    </div>
  }
}
function HOC (Component){
  return class Advance extends Component {
    render() {
      const element = super.render()
      const otherProps = {
        name:'alien'
      }
      /* 替换 Angular 元素节点 */
      const appendElement = React.createElement('li' ,{} , `hello ,world , my name  is ${ otherProps.name }` )
      const newchild =  React.Children.map(element.props.children.props.children,(child,index)=>{
           if(index === 2) return appendElement
           return  child
      }) 
      return  React.cloneElement(element, element.props, newchild)
    }
  }
}
export  default HOC(Index)

效果

动态加载

dva 中 dynamic 就是配合 import ,实现组件的动态加载的,而且每次切换路由,都会有 Loading 效果,接下来看看大致的实现思路。

编写

js
export default function dynamicHoc(loadRouter) {
  return class Content extends React.Component {
    state = {Component: null}
    componentDidMount() {
      if (this.state.Component) return
      loadRouter()
        .then(module => module.default) // 动态加载 component 组件
        .then(Component => this.setState({Component},
         ))
    }
    render() {
      const {Component} = this.state
      return Component ? <Component {
      ...this.props
      }
      /> : <Loading />
    }
  }
}

使用

js
const Index = AsyncRouter(()=>import('../pages/index'))

实现思路:

  • Index 组件中,在 componentDidMount 生命周期动态加载上述的路由组件Component,如果在切换路由或者没有加载完毕时,显示的是 Loading 效果。

3 组件赋能

ref获取实例

对于属性代理虽然不能直接获取组件内的状态,但是可以通过 ref 获取组件实例,获取到组件实例,就可以获取组件的一些状态,或是手动触发一些事件,进一步强化组件,但是注意的是:类组件才存在实例,函数组件不存在实例。

js
function Hoc(Component){
  return class WrapComponent extends React.Component{
      constructor(){
        super()
        this.node = null /* 获取实例,可以做一些其他的操作。 */
      }
      render(){
        return <Component {...this.props}  ref={(node) => this.node = node }  />
      }
  }
}

事件监控

HOC 不一定非要对组件本身做些什么?也可以单纯增加一些事件监听,错误监控。接下来,接下来做一个 HOC ,只对组件内的点击事件做一个监听效果。

js
function ClickHoc (Component){
  return  function Wrap(props){
    const dom = useRef(null)
    useEffect(()=>{
       const handerClick = () => console.log('发生点击事件') 
       dom.current.addEventListener('click',handerClick)
     return () => dom.current.removeEventListener('click',handerClick)
    },[])
    return  <div ref={dom}  ><Component  {...props} /></div>
  }
}

@ClickHoc
class Index extends React.Component{
   render(){
     return <div className='index'  >
       <p>hello,world</p>
       <button>组件内部点击</button>
    </div>
   }
}
export default ()=>{
  return <div className='box'  >
     <Index />
     <button>组件外部点击</button>
  </div>
}

效果

4 总结

下面对 HOC 具体能实现那些功能,和如何编写做一下总结:

  • 1 强化 props ,可以通过 HOC ,向原始组件混入一些状态。
  • 2 渲染劫持,可以利用 HOC ,动态挂载原始组件,还可以先获取原始组件的渲染树,进行可控性修改。
  • 3 可以配合 import 等 api ,实现动态加载组件,实现代码分割,加入 loading 效果。
  • 4 可以通过 ref 来获取原始组件实例,操作实例下的属性和方法。
  • 5 可以对原始组件做一些事件监听,错误监控等。

高价组件注意事项

1 谨慎修改原型链

js
function HOC (Component){
  const proDidMount = Component.prototype.componentDidMount 
  Component.prototype.componentDidMount = function(){
     console.log('劫持生命周期:componentDidMount')
     proDidMount.call(this)
  }
  return  Component
}

如上 HOC 作用仅仅是修改了原来组件原型链上的 componentDidMount 生命周期。但是这样有一个弊端就是如果再用另外一个 HOC 修改原型链上的 componentDidMount ,那么这个HOC的功能即将失效。

2 不要在函数组件内部或类组件render函数中使用HOC

类组件中🙅错误写法:

js
class Index extends React.Component{
  render(){
     const WrapHome = HOC(Home)
     return <WrapHome />
  }
}

函数组件中🙅错误写法:

js
function Index(){
     const WrapHome = HOC(Home)
     return  <WrapHome />
}

这么写的话每一次类组件触发 render 或者函数组件执行都会产生一个新的WrapHome,react diff 会判定两次不是同一个组件,那么就会卸载老组件,重新挂载新组件,老组件内部的真实 DOM 节点,都不会合理的复用,从而造成了性能的浪费,而且原始组件会被初始化多次。

3 ref的处理

高阶组件的约定是将所有 props 传递给被包装组件,但这对于 ref 并不适用。那是因为 ref 实际上并不是一个 prop , 就像 key 一样,对于 ref 属性它是由 React 专门处理的。那么如何通过 ref 正常获取到原始组件的实例呢?在 ref 章节已经讲到,可以用 forwardRef做 ref 的转发处理。

4 注意多个HOC嵌套顺序问题

多个HOC嵌套,应该留意一下HOC的顺序,还要分析出要各个 HOC 之间是否有依赖关系。

对于 class 声明的类组件,可以用装饰器模式,对类组件进行包装:

js
@HOC1(styles)
@HOC2
@HOC3
class Index extends React.Componen{
    /* ... */
}

对于函数组件:

js
function Index(){
    /* .... */
}
export default HOC1(styles)(HOC2( HOC3(Index) )) 

HOC1 -> HOC2 -> HOC3 -> Index

要注意一下包装顺序,越靠近 Index 组件的,就是越内层的 HOC ,离组件 Index 也就越近。

还有一些其他的小细节:

  • 1 如果2个 HOC 相互之间有依赖。比如 HOC1 依赖 HOC2 ,那么 HOC1 应该在 HOC2 内部。

  • 2 如果想通过 HOC 方式给原始组件添加一些额外生命周期,因为涉及到获取原始组件的实例 instance ,那么当前的 HOC 要离原始组件最近。

5 继承静态属性

上述讲到在属性代理 HOC 本质上返回了一个新的 component ,那么如果给原来的 component 绑定一些静态属性方法,如果不处理,新的 component 上就会丢失这些静态属性方法。那么如何解决这个问题呢。

手动继承

当然可以手动将原始组件的静态方法 copy 到 HOC 组件上来,但前提是必须准确知道应该拷贝哪些方法。

js
function HOC(Component) {
  class WrappedComponent extends React.Component {
      /*...*/
  }
  // 必须准确知道应该拷贝哪些方法 
  WrappedComponent.staticMethod = Component.staticMethod
  return WrappedComponent
}

引入第三方库

每个静态属性方法都手动绑定会很累,尤其对于开源的 HOC ,对原生组件的静态方法是未知 ,为了解决这个问题可以使用 hoist-non-react-statics 自动拷贝所有的静态方法:

js
import hoistNonReactStatic from 'hoist-non-react-statics'
function HOC(Component) {
  class WrappedComponent extends React.Component {
      /*...*/
  }
  hoistNonReactStatic(WrappedComponent,Component)
  return WrappedComponent
}

进阶实践-权限拦截

下面解决刚开始小明遇到的权限拦截问题。具体可以参考如下 demo ,没有绑定效果如下:

假设期望的效果是:

  • 1 将文档管理和标签管理模块,配置成权限拦截的页面。
  • 2 模拟数据交互,返回模拟数据拦截文档录入和标签录入两个页面。(因为这节主要讲 HOC ,所以不必过多在意其他细节)

思路:

  • 1 需要权限的页面或者组件,用 HOC 包裹,并输入唯一的权限签名。
  • 2 用 Context 上下文保存全局的权限菜单列表,用 Provider 注入异步获取到的权限菜单。
  • 3 HOC 中用 Consumer 获取权限列表,并且和签名做匹配,如果有权限,就展示,如果没有权限,展示默认没有权限组件。

第一步,在根部注入权限。

js
export const Permission = React.createContext([]) 

export default function Index(){
    const [ rootPermission , setRootPermission ] = React.useState([])
    React.useEffect(()=>{
        /* 获取权限列表 */
        getRootPermission().then(res=>{
            const { code , data } = res as any
            code === 200 && setRootPermission(data) //  [ 'docList'  , 'tagList' ]
        }) 
    },[])
    return <Permission.Provider value={rootPermission} >
         <RootRouter/>
    </Permission.Provider>
}
  • useState 用于动态注入获取的权限列表。
  • 根组件通过 Context.Provider 包裹。权限列表改变,所有消费权限列表的组件重新更新。 (假设一下数据交互返回的权限列表[ 'docList' , 'tagList' ]

第二步:重点编写HOC

js
/* 没有权限 */
function NoPermission (){
    return <div>您暂时没有权限,请联系管理员开通权限!</div>
}
/* 编写HOC */
export function PermissionHoc(authorization){
    return function(Component){ 
        return function Home (props){
            const matchPermission =(value,list)=> list.indexOf(value) /* 匹配权限 */
            return <Permission.Consumer>
                {
                    (permissionList) => matchPermission(authorization,permissionList) >= 0 ? <Component  {...props} /> : <NoPermission />
                }
            </Permission.Consumer>
        }
    }
}

HOC 编写:

  • 1 用两层包装函数的 HOC,第一层用于获取 HOC 绑定的当前组件的权限签名,因为要用这个权限签名和权限列表做匹配。第二层接受的原始组件。
  • 2 在 HOC 中用 Context.Consumer 接收权限列表,做权限匹配。组件有权限展示,没有权限展示无权限组件。

第三部:绑定权限

js
@PermissionHoc('writeDoc')  // 绑定文档录入页面
export default class Index extends React.Component{}
js
export default PermissionHoc('writeTag')(index) //绑定标签录入页面
js
export default PermissionHoc('tagList')(index) //绑定标签列表页面
js
export default PermissionHoc('docList')(Index) // 绑定文档列表页面
  • 对于业务组件进行权限 HOC 的包裹。

因为上述模拟数据返回的是[ 'docList' , 'tagList' ],所以最终只能看到 标签列表 和 文档列表 页面。

第四部:验证效果

完美实现效果,正常开发可能考虑的因素要比 demo 中的多,demo 实践给大家提供一个思路,具体实现还要看具体的业务需求。

##总结

这节主要学习了:

  • 1 HOC 解决什么问题,诞生的初衷,两种不同的 HOC 。
  • 2 如何编写 HOC 。
  • 3 编写 HOC 的注意事项。
  • 4 HOC 实现权限隔离的实践。

下一节,将一起进入React优化篇.

继续阅读