自定义封装组件 向外暴露类型版

正文

当现有的组件无法满足需求时, 需对组件进行二次封装并添加自定义的属性及方法, 还需要保留该组件原有的属性和方法, 最好还保留类型声明. 根据上述需求, 我们在封装组件时, 可以选择从下面几个方面入手

  1. 接收父组件传递的属性 事件 插槽
  2. 继承并暴露原组件的属性及事件
  3. 暴露自定义的属性及事件
  4. 暴露原组件及自定义的类型声明

这里以二次封装 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 类型, 以及自定义的类型即可.

  1. 需要注意的就是 Partial<T>, 它是 TypeScript 提供的一个内置工具类型,作用是将类型 T 中的所有属性变成可选(optional)属性

  2. 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() 函数
  1. 作用: 用于床架虚拟 DOM 节点(vnode)

  2. 参数: h(type: string | Component,props?: object | null,children?: Children | Slot | Slots): VNode

    1. 第一个参数: stirng 类型只能用于原生元素, 如 div 等; 也可以是一个 vue 组件定义
    2. 第二个参数:
      1. 为 null 时, 等同于 h(type: string | Component, children?: Children | Slot): VNode
      2. 类型为 object 时, 是要传递的 props; props 包含
        1. 所有原生属性: class, id, style
        2. vue 绑定的 props: 父组件传递给子组件的 props
        3. 事件监听器: 比如 onClick onInput
    3. 第三个参数 children?: Children | Slot | Slots
      1. 当使用 h() 创建组件时, 第三个参数必须使用插槽传递; 即第一个参数是 vue 组件, 第三个参数就要使用插槽传递
  3. 返回值: 返回一个虚拟 DOM 节点(vnode)

  4. 渲染方式:

    1. 可以用 component :is 进行渲染

      const myVnode = h("div", { class: "bar", innerHTML: "hello" });
      
      <component :is="myVnode"></component>
      
    2. 函数式渲染

      // 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
  1. 作用: 一个包含了组件所有透传 attributes 的对象
$slots
  1. 作用: 一个表示父组件所传入插槽的对象
defineExpose()
  1. 作用: 显式指定在 <script setup> 组件中要暴露出去的属性
  2. 参数: 期望接受一个对象
    1. reactive() 定义的代理对象可以接收
    2. ref() 对定义的 RefImpl 对象不行 !
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值