Vue3组件二次封装的小技巧

3,772 阅读3分钟

刚才在 b 站学了两个二次封装组件的小技巧 (b 站果然是个学习平台)

  1. 双向数据绑定 2. 向子组件传递插槽 3. 获取子组件的 ref

我们使用 vite 初始化一个 vue、ts 的项目,然后安装下 element-plus

下边开始展示 猛击访问 github 仓库

双向数据绑定

我们以 input 组件作为例子

双向数据绑定的原理及实现想必大家已经烂熟于心了直接看官网吧!

image.png

子组件接受一个 modelValue 的 prop, 通过 emit 触发 update:modelValue 事件完成数据的更新

父组件直接 v-model="xxxx"

嫌麻烦官方还提供了 defineModel() 用于简化上边的步骤

image.png

向子组件传递插槽

我们以 input 组件作为例子,创建一个 WrapInput.vue 组件

image.png

未学习之前

WrapInput.vue 常规的做法,遍历 $slots 来实现

<script setup lang="ts">
const model = defineModel()

</script>

<template>
  <el-input v-model="model" placeholder="Please input" >
    <template v-for="(_, slot) in $slots" :key="solt" v-slot:[slot]="slotProps">
      <slot :name="slot" v-bind="slotProps"></slot>
    </template>
  </el-input>
</template>

<style lang='scss' scoped></style>

app.vue 中引入并传递 prepend、append 插槽

<script setup lang="ts">
import { ref } from "vue";
import WrapInput from "./components/WrapInput.vue";

const inputText = ref('')
</script>

<template>
  <WrapInput v-model="inputText">
    <template #prepend>Http://</template>
    <template #append>.com</template>
  </WrapInput>

  <div>
    {{inputText}}
  </div>
</template>

<style scoped>
</style>

正确渲染了插槽

image.png

学习之后

让我们来修改下 WrapInput.vue

<script setup lang="ts">
import { h } from "vue";
import { ElInput } from "element-plus";
const model = defineModel()
</script>

<template>
<component :is="h(ElInput, $attrs, $slots)" v-model="model"></component>
</template>

<style lang='scss' scoped></style>

app.vue 的代码不做任何修改

image.png

插槽正常传递、数据更新正常,看到这种写法的时候有点震惊的

component 组件为什么可以传入 h 函数

看下 h 函数的文档, h(ElInput, $attrs, $slots) 是创建了一个虚拟 dom 节点

image.png

component 组件的 is 属性则可以接收

  1. 被注册的组件名
  2. 导入的组件对象
  3. 一个返回上述值之一的函数

image.png

component 组件的 is 属性接收到一个函数时,Vue 会调用这个函数并使用其返回值作为要渲染的组件。

在这种情况下,h(ElInput, $attrs, $slots) 会立即执行并返回一个 VNode,这个 VNode 描述了如何渲染 ElInput 组件。

获取子组件的 ref

未学习之前

之前的自己的写法有点蠢的具体的做法是在子组件创建一个 getRef 的函数把 ref 暴露出去,父组件调用 getRef 方法后在执行子组件方法的调用,大概是下边这样

WrapInput1.vue

<script setup lang="ts">
import { h, ref} from "vue";
import { ElInput } from "element-plus";
const model = defineModel()

const inputRef = ref()

function getRef () {
  return inputRef.value
}

defineExpose({
  getRef
})
</script>

<template>
  <component ref="inputRef" :is="h(ElInput, $attrs, $slots)" v-model="model"></component>
</template>

<style lang='scss' scoped></style>

学习之后

WrapInput.vue

<script setup lang="ts">
import { h, ref } from "vue";
import { ElInput } from "element-plus";
const model = defineModel()

const inputRef = ref()

defineExpose(new Proxy({}, {
  get(_target, prop)  {
    return inputRef.value?.[prop]
  },
  has (_target, prop) {
    return prop in inputRef.value
  }
}))

</script>

<template>
  <component :is="h(ElInput, $attrs, $slots)" v-model="model" ref="inputRef"></component>
</template>

<style lang='scss' scoped></style>

使用 Proxy 代理暴露出去的方法,是有点震惊的,还能这么写

App.vue

<script setup lang="ts">
import { ref } from "vue";
import WrapInput from "./components/WrapInput.vue";

const inputText = ref('')

const prependSlotText =  ref('Http://')
const appendSlotText =  ref('.com')

function updateSlotInfo (){
  prependSlotText.value = 'https://'
  appendSlotText.value = `${new Date().getTime()}`
}

const wrapInputRef = ref()
function setWrapInputFocus () {
  wrapInputRef.value?.focus()
}
</script>

<template>
  <WrapInput v-model="inputText" ref="wrapInputRef">
    <template #prepend>{{ prependSlotText }}</template>
    <template #append>{{ appendSlotText }}</template>
  </WrapInput>

  <div style="margin: 20px 0;">
    {{inputText}}
  </div>

  <el-button type="primary" @click="updateSlotInfo">更新插槽内容</el-button>
  <el-button type="primary" @click="setWrapInputFocus">set input focus</el-button>
  
</template>

<style scoped>
</style>

调用组件的 focus 方法让 WrapInput.vue 组件获取焦点

image.png

使用 useTemplateRef 会有 ts 类型错误,可尝试按照如下方式修改

const SimpleUserList = defineAsyncComponent(() => import('./SimpleUserList.vue'))

// 获取组件类型
type SimpleUserListInstance = InstanceType<typeof SimpleUserList>

const simpleUserListRef = useTemplateRef('simpleUserListRef')

defineExpose(new Proxy({} as SimpleUserListInstance, {
  get(_target, prop) {
    return simpleUserListRef.value?.[prop as keyof SimpleUserListInstance]
  },
  has(_target, prop) {
    return prop in (simpleUserListRef.value ?? {})
  },
}))

监听子组件的生命周期事件

image.png

3.4 版本之前是 @vnode:XXX

image.png

总结

本文实践了在 vue3 中在二次封装组件时如何实现 v-model、插槽传递、子组件 ref 获取

插槽传递通过向 component 组件的 is 属性传递 h 函数创建虚拟 dom 来实现

获取子组件的 ref 则是使用 new Proxy 的方式来实现

猛击直达大佬 b 站原视频

往期文章