何为高阶组件
高阶组件在React官方文档已有明确定义。
高阶组件是参数为组件,返回值为新组件的函数
它本身并不是React特有的东西,而是一种基于 React 的组合特性而形成的设计模式。Vue也能做到这种模式,但是因为Vue的设计上和React有区别,所以要实现React这种函数式的高阶组件会更麻烦一点。
场景
我们先预设一种场景,在项目中所使用的是Element-plus的组件库,由于公司的一些规范要求,所有的输入框都要具有清空按钮(clearable
)和固定的占位文本符(placeholder
),且能够监控用户输入用于搜集数据。
代码实现
我们通过组件再封装的形式,写一个my-input
组件,它具有el-input
的所有功能,并且符合上面场景的要求。
template形式
属性和事件继承
如果my-input
组件的根节点就是el-input
,那么属性会自动通过vue的透传 Attributes的形式继承父组件传入进来的属性和事件。
<!-- MyInput.vue -->
<template>
<!-- 默认具有清空按钮和占位文本符 -->
<el-input clearable placeholder="请输入" @change="changeMyInput" />
</template>
<script setup lang="ts">
function changeMyInput(value: string) {
console.log("监控用户输入值", value);
}
</script>
如果组件的根节点不是el-input
,那么就需要通过v-bind="$attrs"
的方式来继承属性和事件了:
<!-- MyInput.vue -->
<template>
<div class="my-input">
<el-input
clearable
placeholder="请输入"
v-bind="$attrs"
@change="changeMyInput"
>
</el-input>
</div>
</template>
注意:根据Vue3特性,如果要让my-input
组件的属性能够覆盖底层写死的clearable
和 placeholder
属性,v-bind="$attrs"
要写在属性后面。
插槽的继承
在使用template的场景下,vue似乎没有提供简便的方式去继承插槽,那么这里通过v-for
的方式去循环插槽:
<template :key="name" v-for="(slot, name) in $slots" v-slot:[name]>
<slot :name="name"></slot>
</template>
完整代码
<!-- MyInput.vue -->
<template>
<div class="my-input">
<el-input
clearable
placeholder="请输入"
v-bind="$attrs"
@change="changeMyInput"
>
<template :key="name" v-for="(slot, name) in $slots" v-slot:[name]>
<slot :name="name"></slot>
</template>
</el-input>
</div>
</template>
<script setup lang="ts">
import { ElInput } from "element-plus";
import { Edit } from "@element-plus/icons-vue";
function changeMyInput(value: string) {
console.log("监控用户输入值", value);
}
</script>
<template>
<!-- 父节点调用 -->
<div>
自定义输入框:
<my-input
style="width: 300px"
v-model="inputValue"
@change="changeMyInput"
>
<template #prepend>Http://</template>
</my-input>
</div>
</template>
这种方式最多只能算一种组件再封装,并不能算是一种高阶组件,我们想要的是一个返回新组件的函数,参数是基础组件。
高阶组件形式
先把把上面的代码改成渲染函数的形式:
import { h, defineComponent, useAttrs } from "vue";
import { ElInput } from "element-plus";
export default defineComponent({
setup(props, context) {
const attr = useAttrs();
const slots = context.slots;
return () =>
h(
ElInput,
{
...attr,
clearable: true,
onChange(value) {
console.log("MyInput.ts ~ changeMyInput ~ value", value);
},
},
{
...slots,
}
);
},
});
在此基础上,我们进行改进:
// InputHoc.ts
import { h, defineComponent, useAttrs, DefineComponent } from "vue";
export default function (component: DefineComponent | String) {
return defineComponent({
setup(props, context) {
const attr = useAttrs();
const slots = context.slots;
return () =>
h(
component,
{
...attr,
clearable: true,
placeholder: "请输入",
onChange(value: any) {
console.log("MyInput.ts ~ changeMyInput ~ value", value);
},
},
{
...slots,
}
);
},
});
}
我们在调用的时候,传入对应的组件即可:
import inputHoc from "../components/InputHoc";
// 此处似乎ts有兼容性问题
const MyInput = inputHoc(ElInput as any);
可能有人会觉得这么做似乎让代码的实现和调用都变复杂了,不清楚这么做的意义何在。我们写的这个InputHoc.ts
高阶组件还可以给其他组件使用,比如el-select
,只要组件的属性都具有clearable
和placeholder
属性,以及change
事件,并且实际业务符合我们预设的场景:
const MySelect = inputHoc(ElSelect as any);
完整的调用代码如下:
<template>
<!-- 父节点调用 -->
<div>
自定义输入框:
<my-input style="width: 300px" v-model="inputValue" @change="changeMyInput">
<template #prepend>Http://</template>
</my-input>
<br />
自定义下拉框:
<my-select style="width: 300px" v-model="selectValue">
<el-option
v-for="item in options"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</my-select>
<!-- -->
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { ElInput, ElSelect, ElDatePicker } from "element-plus";
import inputHoc from "../components/InputHoc";
const options = [
{
value: "Option1",
label: "Option1",
},
{
value: "Option2",
label: "Option2",
},
];
// 此处似乎ts有兼容性问题
const MyInput = inputHoc(ElInput as any);
const MySelect = inputHoc(ElSelect as any);
let inputValue = ref("");
let selectValue = ref("");
function changeMyInput(value: string) {
console.log("Parent ~ changeMyInput ~ value", value);
}
</script>
总结
高阶组件的写法是一个灵活性较高、用于抽象提取公共逻辑的组件函数,它没有mixins
和extends
的命名空间冲突问题,相当实用。