Vue3高阶组件的实现方式

1,361 阅读2分钟

何为高阶组件

高阶组件在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,只要组件的属性都具有clearableplaceholder属性,以及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>

Pasted image 20221012164602.png

总结

高阶组件的写法是一个灵活性较高、用于抽象提取公共逻辑的组件函数,它没有mixinsextends的命名空间冲突问题,相当实用。