正文
当现有的组件无法满足需求时, 需对组件进行二次封装并添加自定义的属性及方法, 还需要保留该组件原有的属性和方法, 最好还保留类型声明. 根据上述需求, 我们在封装组件时, 可以选择从下面几个方面入手
- 接收父组件传递的属性 事件 插槽
- 继承并暴露原组件的属性及事件
- 暴露自定义的属性及事件
- 暴露原组件及自定义的类型声明
这里以二次封装 ElInput 组件为例
第一步: 用 h() 很轻松的渲染组件并拿到父组件传递的属性方法和插槽
1. setup 中没有 this, 使用 useAttrs() useSlots() 获取属性 方法 插槽
<component
:is="h(ElInput, { ...$attrs }, $slots)"
></component>
// 验证
const attrs = useAttrs();
console.log(attrs, "@attrs");
const slots = useSlots();
console.log(slots, "@slots");
第二步: 向父组件暴露 ElInput 组件原有的属性和方法. 在做 ElFrom 表单时, 我们知道需要使用 ref 来触发组件实例上的方法, 例如触发表单验证方法
const ruleFormRef = ref<FormInstance>()
await ruleFormRef.value.validate()
那么我们在进行二次封装时, 可以选择把 ElInput 组件的 ref 与自定义的属性方法进行合并后暴露出去; 这样就能在保留原组件的属性及事件的同时, 暴露自定义的属性及事件, 通过 ref.value.xxx 的方式访问自定义属性及事件
具体该怎么暴露呢? 这涉及到两个知识点: vnode 的 ref; defineExpose() 期望接收的参数
1. vnode 的 ref:
1. vue 在渲染阶段, 会检查 vnode 上是否有 ref 属性, 如果有且是字符串, 就会把该字符串写入到组件实例上, 类似于我们手动在组件上绑定 ref='xxx'
2. 如果 ref 传入的是一个函数, vue 会在组件挂载完成后调用该函数, 并把组件实例作为参数传入, 我们要拿到的就是这个
3. 注意: ref 的回调函数,接收的是 ElInput 组件实例, 而不是我们封装的组件实例, 所以需要手动合并
2. defineExpose():
1. 要求传入的参数是一个对象, 可以是 {} 或者 代理对象(Proxy(Object)), 但一定不能是 ref 包装的对象
所以我们可以在配置 props 时, 用函数的方式配置 ref, 然后在该函数中将自定义的内容合并, 也就是说直接用 ElInput 组件的实例去填充自定义的 exposed 对象,用这个对象拿到 ElInput 的方法 + 自定义方法, 然后向外暴露. 到这里组件基本封装完成了
<component
:is="h(ElInput, { ...$attrs, ref: changeRef }, $slots)"
></component>
// 自定义属性 方法
const exposed = reactive({
a: "wxm",
logA() {
console.log(this.a, "@a");
},
});
// 接收 ElInput 的属性和方法, 并于自定义的合并
function changeRef(instance) {
Object.assign(exposed, instance);
}
// 向父组件暴露
defineExpose(exposed);
第三步: ts 类型声明, 我们在只需要让定义的 exposed 继承 ElInput 类型, 以及自定义的类型即可.
-
需要注意的就是
Partial<T>, 它是 TypeScript 提供的一个内置工具类型,作用是将类型T中的所有属性变成可选(optional)属性 -
ComponentInstance: 是 vue 内置的工具类型, 用于提取组件实例
// 获取 ElInput 的类型 type ElInputInstance = ComponentInstance<typeof ElInput>; // 自定义类型, 继承 ElInput 的类型 interface Exposed extends ElInputInstance { a: string; logA: Function; } // 对 exposed 类型声明 const exposed = reactive<Partial<Exposed>>({ a: "wxm", logA() { console.log(this.a, "@a"); }, }); // 向外暴露 defineExpose(exposed);
完整代码及 Demo
// 二次封装的 Elinput 组件
<script setup lang="ts">
import { h, reactive, type ComponentInstance } from "vue";
import { ElInput } from "element-plus";
type ElInputInstance = ComponentInstance<typeof ElInput>;
interface Exposed extends ElInputInstance {
a: string;
logA: Function;
}
const exposed = reactive<Partial<Exposed>>({
a: "wxm",
logA() {
console.log(this.a, "@a");
},
});
function changeRef<T>(instance: T) {
if (instance) Object.assign(exposed, instance);
}
defineExpose(exposed);
</script>
<template>
<h2>二次封装 input 组件</h2>
<component
:is="h(ElInput, { ...$attrs, ref: changeRef }, $slots)"
></component>
</template>
父组件使用
import myInput from "@/components/MyComponents/Input.vue";
const modelValue = ref("hello word");
const inputRef = useTemplateRef<any>("inputRef");
const changA = () => {
console.log("!!!");
};
setTimeout(() => {
inputRef.value.clear();
inputRef.value.logA();
console.log(inputRef.value.a, "@inputRef.value.a");
}, 2000);
<el-divider>二次封装 Input 组件</el-divider>
<my-input
placeholder="传递给子组件的 placeholder"
v-model="modelValue"
ref="inputRef"
@changA="changA"
>
<template #append> 后置内容 </template>
<template #prefix> 前置内容 </template>
</my-input>
API
在这次的二级封装组件中, 用到了以下 API, 不熟悉的同学可以在这里查看
h() $attrs $slots defineExpose()
h() 函数
-
作用: 用于床架虚拟 DOM 节点(vnode)
-
参数: h(type: string | Component,props?: object | null,children?: Children | Slot | Slots): VNode
- 第一个参数: stirng 类型只能用于原生元素, 如 div 等; 也可以是一个 vue 组件定义
- 第二个参数:
- 为 null 时, 等同于 h(type: string | Component, children?: Children | Slot): VNode
- 类型为 object 时, 是要传递的 props; props 包含
- 所有原生属性: class, id, style
- vue 绑定的 props: 父组件传递给子组件的 props
- 事件监听器: 比如 onClick onInput
- 第三个参数 children?: Children | Slot | Slots
- 当使用 h() 创建组件时, 第三个参数必须使用插槽传递; 即第一个参数是 vue 组件, 第三个参数就要使用插槽传递
-
返回值: 返回一个虚拟 DOM 节点(vnode)
-
渲染方式:
-
可以用
component :is进行渲染const myVnode = h("div", { class: "bar", innerHTML: "hello" }); <component :is="myVnode"></component> -
函数式渲染
// MyVNodeRenderer.ts import { h, defineComponent } from 'vue' export default defineComponent({ name: 'MyVNodeRenderer', render() { const myVnode = h('div', { class: 'bar', innerHTML: 'hello2' }) return myVnode } }) import MyVNodeRenderer from './MyVNodeRenderer' <MyVNodeRenderer></MyVNodeRenderer>
-
$attrs
- 作用: 一个包含了组件所有透传 attributes 的对象
$slots
- 作用: 一个表示父组件所传入插槽的对象
defineExpose()
- 作用: 显式指定在
<script setup>组件中要暴露出去的属性 - 参数: 期望接受一个对象
- reactive() 定义的代理对象可以接收
- ref() 对定义的 RefImpl 对象不行 !
1487

被折叠的 条评论
为什么被折叠?



