在vue项目中编写高阶组件

2,812 阅读5分钟

1、问题提出

假设我们的项目是用Element-UI构建

1、如何使得在修改一行代码的前提下,使得我们的ElDialog具备可以拖拽的功能?

2、如何使得在修改一行代码的前提下,我们的Selector可以实现汉语拼音的模糊匹配?

3、如何使得在修改一行代码的前提下,实现我们的Tree在只有点箭头图标的时候才会展开或者收缩节点,并且树节点内容可以展示小图标(AntD UI原生支持)呢?

带着这些问题,我们可以很容易的就联想到,我们需要对这些组件进行封装,如果对这些组件进行封装之后,我们的设计应该考虑到该如何提升组件的易用性,我的结论是:不改变组件本身的API,新增能满足以上特定功能的扩展API,为此我们引入一个概念,叫做高阶组件。

2、什么是高阶组件

高阶组件(HOC),这一概念来自于React,我们从React的官方文档摘录了下面这段话

高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。
HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。

具体而言,高阶组件是参数为组件,返回值为新组件的函数。因此,就我个人的理解,高阶组件的实质是AOP编程思想在前端框架的一种体现,所谓在不改变组件本身的功能或易用性的前提下,扩展某些新的功能,从而实现更好的组件封装、代码复用的一种手段。

3、如何在Vue组件中编写高阶组件

首先,我们先认识几个必备的API

3.1 $attrs

这是我们摘录自Vue官方文档的一段介绍:

包含了父作用域中不作为 prop 被识别 (且获取) 的 attribute 绑定 (class 和 style 除外)。
当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 (class 和 style 除外),并且可以通过 v-bind="$attrs" 传入内部组件——在创建高级别的组件时非常有用。

抓重点:父作用域中不作为 prop 被识别 (且获取) 的 attribute 绑定 (class 和 style 除外)

3.2 $listeners

这是我们摘自Vue官方文档的一段介绍

包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件——在创建更高层次的组件时非常有用。

抓重点:父作用域中的 (不含 .native 修饰器的) v-on 事件监听器 当这两个属性配合使用的时候,便可实现化腐朽为神奇的效果。 我们现在已经可以尝试编写出一个可以实现drag的给予ElDialog的组件了。 Dialog.vue组件代码如下:

<template>
	<el-dialog v-drag="draggable" v-bind="$attrs" v-on="$listeners"></el-dialog>
</template>
<script>
	// drag 代码可自行百度,本例不做展示
	import drag from '@/utils/drag';
	export default {
    	name: 'Dialog',
        directives: {
        	drag
        },
        props: {
        	draggable: {
            	type: Boolean,
                required: false,
                default: true
            }
        }
    }
</script>

这样的编码结果,我们便可以将外部的props或者事件侦听器,经过这一层次的代码的转交,这将不会影响ElDialog本身的功能,但是,此刻我们已经给我们的可拖拽功能已经实现了。🤣😎🥰

4、补充

虽然,$attrs和$listeners,实现了我们属性和事件侦听器的穿透,但是,目前我们还有两个问题是没有考虑到的,一个是组件的插槽,在上面的代码,还没有穿透,另一个是组件的refs获取的问题。 第一个问题很好解决,我们可以在上述代码添加对插槽的支持。 改写后的Dialog.vue代码如下:

<template>
	<el-dialog v-drag="draggable" v-bind="$attrs" v-on="$listeners">
    	<slot name='header' />
        <slot />
        <slot name='footer' />
    </el-dialog>
</template>
<script>
	// drag 代码可自行百度,本例不做展示
	import drag from '@/utils/drag';
	export default {
    	name: 'Dialog',
        directives: {
        	drag
        },
        props: {
        	draggable: {
            	type: Boolean,
                required: false,
                default: true
            }
        }
    }
</script>

第二个问题便显得尤为棘手了😫😫😫,场景如下:

<template>
	<!-- 假设刚才的Dialog组件已经注册名为DragDialog -->
	<drag-dialog ref='dialog' />
</template>

假设我们的ElDialog有方法(因为笔者举例的问题,ElDialog 官方API是没有对外保留可用方法),那么,此时我们这个ref获取到的是我们的DragDialog,而实际上,我们是想获取到真正的ElDialog,这便是refs转发的问题。就目前Vue2.x,笔者还没有找到简便的可以实现引用转发的API,需要我们额外的编写代码进行支持,代码如下:(若有大佬有别的办法,请联系我的邮箱,404189928@qq.com),希望帅气的尤老板考虑考虑小老弟的诉求,嘿嘿🤣🤣🤣

<template>
	<el-dialog v-drag="draggable" v-bind="$attrs" v-on="$listeners">
    	<slot name='header' />
        <slot />
        <slot name='footer' />
    </el-dialog>
</template>
<script>
	// drag 代码可自行百度,本例不做展示
	import drag from '@/utils/drag';
    import { ref } from '@vue/composition-api';
	export default {
    	name: 'Dialog',
        setup() {
        	const dialog =ref(null);
            return {
            	dialog
            };
        },
        directives: {
        	drag
        },
        props: {
        	draggable: {
            	type: Boolean,
                required: false,
                default: true
            }
        },
        methods: {
        	sayHello() {
            	// 假设sayHello 是ElDialog对外保留的一个有用的方法。
            	this.dialog && this.dialog.sayHello();
            }
        }
    }
</script>

而在React框架中,React是支持引用转发的: 我们还是先看一下React官方的释义:

Ref 转发是一项将 ref 自动地通过组件传递到其一子组件的技巧。对于大多数应用中的组件来说,这通常不是必需的。但其对某些组件,尤其是可重用的组件库是很有用的。

举个其官方的🌰:

const FancyButton = React.forwardRef((props, ref) => (
  <button ref={ref} className="FancyButton">
    {props.children}
  </button>
));

// 你可以直接获取 DOM button 的 ref:
const ref = React.createRef();
<FancyButton ref={ref}>Click me!</FancyButton>;

5、总结:高阶组件的优缺点

5.1 高阶组件的优点:

1、可以减少冗余的props或v-on/emit代码。

2、可以实现增加UI库本身的功能,而并不影响组件的调用,有利于团队的协作,也有利于后来的人员进行项目维护。

3、可以在项目中进行统一的管理,降低编码和管理成本,减少不必要的bug的产生。

5.2 高阶组件的缺点:

1、对开发人员的素质有一定的要求,如果不小心在这类组件中混入了业务代码,则将会为后期的开发埋下潜在的隐患。

2、由于高阶组件相比于原生组件包裹了一层,性能会比原生组件有所降低,因此我们需要我们在项目中合理的使用高阶组件。

由于笔者的水平有限,写作过程中难免出现错误,若有纰漏,请各位读者指正,你们的意见将会帮助我更好的进步。本文乃原创,若转载,请联系作者本人,邮箱404189928@qq.com🥰