高阶组件
学习目标
- 掌握 高阶组件功能说明-如何编写高阶组件?
- 掌握 1 强化props
- 掌握 2 控制渲染
高阶组件功能说明-如何编写高阶组件?
1 强化props
强化 props 就是在原始组件的 props 基础上,加入一些其他的 props ,强化原始组件功能。举个例子,为了让组件也可以获取到路由对象,进行路由跳转等操作,所以 React Router 提供了类似 withRouter 的 HOC 。
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 对象。
const HOC = (WrapComponent) =>
class Index extends WrapComponent {
render() {
if (this.props.visible) {
return super.render()
} else {
return <div>暂无数据</div>
}
}
}
修改渲染树
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 效果,接下来看看大致的实现思路。
编写
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 />
}
}
}使用
const Index = AsyncRouter(()=>import('../pages/index'))实现思路:
- Index 组件中,在 componentDidMount 生命周期动态加载上述的路由组件Component,如果在切换路由或者没有加载完毕时,显示的是 Loading 效果。
3 组件赋能
ref获取实例
对于属性代理虽然不能直接获取组件内的状态,但是可以通过 ref 获取组件实例,获取到组件实例,就可以获取组件的一些状态,或是手动触发一些事件,进一步强化组件,但是注意的是:类组件才存在实例,函数组件不存在实例。
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 ,只对组件内的点击事件做一个监听效果。
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 谨慎修改原型链
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
类组件中🙅错误写法:
class Index extends React.Component{
render(){
const WrapHome = HOC(Home)
return <WrapHome />
}
}函数组件中🙅错误写法:
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 声明的类组件,可以用装饰器模式,对类组件进行包装:
@HOC1(styles)
@HOC2
@HOC3
class Index extends React.Componen{
/* ... */
}对于函数组件:
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 组件上来,但前提是必须准确知道应该拷贝哪些方法。
function HOC(Component) {
class WrappedComponent extends React.Component {
/*...*/
}
// 必须准确知道应该拷贝哪些方法
WrappedComponent.staticMethod = Component.staticMethod
return WrappedComponent
}引入第三方库
每个静态属性方法都手动绑定会很累,尤其对于开源的 HOC ,对原生组件的静态方法是未知 ,为了解决这个问题可以使用 hoist-non-react-statics 自动拷贝所有的静态方法:
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 获取权限列表,并且和签名做匹配,如果有权限,就展示,如果没有权限,展示默认没有权限组件。
第一步,在根部注入权限。
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
/* 没有权限 */
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 接收权限列表,做权限匹配。组件有权限展示,没有权限展示无权限组件。
第三部:绑定权限
@PermissionHoc('writeDoc') // 绑定文档录入页面
export default class Index extends React.Component{}export default PermissionHoc('writeTag')(index) //绑定标签录入页面export default PermissionHoc('tagList')(index) //绑定标签列表页面export default PermissionHoc('docList')(Index) // 绑定文档列表页面- 对于业务组件进行权限 HOC 的包裹。
因为上述模拟数据返回的是[ 'docList' , 'tagList' ],所以最终只能看到 标签列表 和 文档列表 页面。
第四部:验证效果
完美实现效果,正常开发可能考虑的因素要比 demo 中的多,demo 实践给大家提供一个思路,具体实现还要看具体的业务需求。
##总结
这节主要学习了:
- 1 HOC 解决什么问题,诞生的初衷,两种不同的 HOC 。
- 2 如何编写 HOC 。
- 3 编写 HOC 的注意事项。
- 4 HOC 实现权限隔离的实践。
下一节,将一起进入React优化篇.