Jiang's Tech Journal

Jiang's Tech Journal

首页
分类
关于
Login →
Jiang's Tech Journal

Jiang's Tech Journal

首页 分类 关于
Login
  1. Home
  2. 前端
  3. 【Vue】组件二次封装技巧

【Vue】组件二次封装技巧

0
  • 前端
  • Published at 2025-12-29
  • Read 36 times
Jiang
Jiang
Table of Contents
No Table of Contents

日常任务开发过程中避免不了进行 UI 组件库部分组件的二次封装使用,记录下长期开发过程中积累的一些经验和网络学习的新技巧,组件二次封装需要解决的无非就是属性 props 如何透传出去,如何复用原有组件的插槽,以及如何调用原组件暴露的方法,这里以比较热门的 UI 框架 Element Plus 为例。

属性 Props 透传

最常规的做法就是直接使用 v-bind="$attrs",直接可以将属性和事件透传出去,缺点在使用组件的时候编辑器就无法做到属性和事件的提示了。

<template>
  <div class="custom-input">
    <div>自定义封装的Input</div>
    <ElInput v-bind="$attrs" />
  </div>
</template>

<script lang="ts" setup></script>

TS 支持

此时我们可以定义一个 props,使用组件自带的 Props 类型。

<template>
  <div class="custom-input">
    <div>自定义封装的Input</div>
    <el-input v-bind="$attrs" />
  </div>
</template>

<script lang="ts" setup>
import type { InputProps } from 'element-plus'
import type { ExtractPropTypes } from 'vue'

// defineProps<Partial<InputProps>>()
defineProps<ExtractPropTypes<InputProps>>()
</script>

但是这样使用 Partial 会将所有属性都转换成非必填,在组件使用的时候如果该组件有些属性是必填的也不会有类型的报错提示了,这样就不太严谨了,我们可以参考 Element Plus 的代码使用 ExtractPropTypes,从组件的 props 配置中提取出对应的 TypeScript 类型。

最后我们抽取一个自定义的 props 类型,以便支持自定义属性和默认值。

::: warning 注意

  1. 需要添加 inheritAttrs: false,不然事件会被触发 2 次。
  2. 最后需要加 defineExpose<InputInstance>(),不然编辑器的相关提示出不来。 :::
<template>
  <div class="custom-input">
    <div>{{ title }}</div>
    <el-input v-bind="{ ...$attrs, ...props }" />
  </div>
</template>

<script lang="ts" setup>
import type { InputInstance, InputProps } from 'element-plus'
import type { ExtractPropTypes } from 'vue'

interface CustomInputProps extends ExtractPropTypes<InputProps> {
  // 自定义的一些参数
  title?: string
}

defineOptions({
  inheritAttrs: false
})

const props = withDefaults(defineProps<CustomInputProps>(), {
  title: '自定义封装的Input', // 自定义参数默认值
  clearable: true // el-input clearable 默认设置成 true
})

defineExpose<InputInstance>()
</script>

插槽透传

常规写法

常规做法一般都是需要哪些插槽透传就写哪些插槽透传,如果需要支持全部的插槽,大部分人的写法基本都是就直接循环 $slots,将所有插槽都透传出去。

#[name]="slotProps" 写法等同于 v-slot:[name]="slotProps"

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

h 函数写法

使用 h 函数进行 component 渲染,第一个参数渲染的元素,这里直接引入 ElInput,第二个参数是相关的属性,第三个参数就是插槽了。

::: warning 注意 如果组件没有样式了,请单独引入该组件的样式文件,Element Plus 可以参考文档中的手动导入。 :::

<template>
  <div class="custom-input">
    <div>{{ title }}</div>
    <component :is="h(ElInput, { ...$attrs, ...props }, $slots)" />
  </div>
</template>

<script lang="ts" setup>
import { h } from 'vue'
import { ElInput } from 'element-plus'
// ... 其他类型导入同上
</script>

Ref 调用

通常组件可以通过 Ref 的形式去调用内部 defineExpose 暴露的方法,二次封装的组件肯定也是需要支持的,最简单的做法就是将原组件暴露的方法再次使用 defineExpose 暴露出去。

简单版本

<script lang="ts" setup>
import type { InputInstance, InputProps } from 'element-plus'
import { getCurrentInstance, ref } from 'vue'

// ... 省略 props 定义

const vm = getCurrentInstance()

function changeRef(inputInstance: Record<string, any> | null) {
  if (vm) {
    vm.exposeProxy = vm.exposed = (inputInstance || {}) as InputInstance
  }
}

defineExpose((vm?.exposeProxy || {}) as InputInstance)
</script>

<template>
  <el-input :ref="changeRef" v-bind="{ ...$attrs, ...props }">
    <template v-for="(_, name) in $slots" :key="name" #[name]="slotProps">
      <slot :name="name" v-bind="slotProps" />
    </template>
  </el-input>
</template>

h 函数版

<template>
  <div class="custom-input">
    <div>{{ title }}</div>
    <component
      :is="h(ElInput, { ...$attrs, ...props, ref: changeRef }, $slots)"
    />
  </div>
</template>

<script lang="ts" setup>
import { getCurrentInstance, h } from 'vue'
import { ElInput } from 'element-plus'
import type { InputInstance } from 'element-plus'

const vm = getCurrentInstance()

function changeRef(inputInstance: Record<string, any> | null) {
  if (vm) {
    vm.exposeProxy = vm.exposed = (inputInstance || {}) as InputInstance
  }
}

defineExpose((vm?.exposeProxy || {}) as InputInstance)
</script>

最终版本

最终版本加入了自定义事件的暴露和自定义事件的派发。

基础版

<template>
  <div class="custom-input">
    <div @click="handleTitleClick">
      {{ title }}
    </div>
    <el-input :ref="changeRef" v-bind="{ ...$attrs, ...props }">
      <template v-for="(_, name) in $slots" :key="name" #[name]="slotProps">
        <slot :name="name" v-bind="slotProps" />
      </template>
    </el-input>
  </div>
</template>

<script lang="ts" setup>
import type { InputInstance, InputProps } from 'element-plus'
import { getCurrentInstance } from 'vue'
import type { ExtractPropTypes } from 'vue'

export interface CustomInputProps extends ExtractPropTypes<InputProps> {
  title?: string
}

export interface CustomInputInstance extends InputInstance {
  someClick: () => void
}

defineOptions({
  inheritAttrs: false
})

const props = withDefaults(defineProps<CustomInputProps>(), {
  title: '自定义封装的Input',
  clearable: true
})
const emit = defineEmits<{
  (e: 'titleClick'): void
}>()
const vm = getCurrentInstance()

function changeRef(inputInstance: Record<string, any> | null) {
  if (vm) {
    vm.exposeProxy = vm.exposed = Object.assign(inputInstance || {}, {
      someClick
    }) as CustomInputInstance
  }
}

function someClick() {
  console.log('someClick')
}

function handleTitleClick() {
  emit('titleClick')
}

defineExpose((vm?.exposeProxy || {}) as CustomInputInstance)
</script>

使用示例

<template>
  <div>
    <CustomInput v-model="inputValue" @input="handleInput" @titleClick="handleTitleClick">
      <template #append>
        <div>append</div>
      </template>
      <template #prepend>
        <div>prepend</div>
      </template>
    </CustomInput>
  </div>
</template>

<script lang="ts" setup>
import { ref } from 'vue'

const inputValue = ref('liubing.me')

function handleInput() {
  console.log('input')
}

function handleTitleClick() {
  console.log('title clicked')
}
</script>

结语

多看看大佬的视频果然是能够学习到新技巧和知识的。最后大家可以按需删减代码使用 Snippet Generator 工具生成编辑器的代码片段,以便快速输入。

Related Posts

OpenCode 接入 Figma MCP:无需安装 fork 的 OAuth 解决办法

在 OpenCode 中接入 Figma 的远程 MCP 服务时,如果 OAuth 授权流程无法正常完成,可以通过手动注册一个 Figma MCP OAuth 客户端来绕过问题。这个方法不需要安装第三方 fork,核心思路是: 向 Figma 的 MCP OAuth 注册接口申请客户端凭据。 把返回

【Vue】VueUse 中 createReusableTemplate 的妙用

在 Vue 3 开发中,虽然推荐使用模板语法,但在封装高阶组件或使用特定 UI 库(如 Naive UI、Element Plus、Ant Design Vue)的表格组件时,我们往往需要编写 Render 函数(h 函数)或使用 JSX/TSX 来处理复杂的自定义列渲染。 对于不熟悉渲染函数 AP

【Vue】 组件内模板复用技巧:createReusableTemplate

在 Vue 组件开发中,我们经常遇到部分模板内容需要在同一组件内多次复用的场景。传统的解决方案如提取子组件、v-for 循环或直接复制粘贴,在某些简单场景下可能显得过于繁琐或冗余。 本文介绍一种利用 Vue 3 组合式 API 和渲染函数特性实现的“局部模板复用”技巧,类似于模板引擎中的“宏(Mac

【Vue】组件二次封装技巧

日常任务开发过程中避免不了进行 UI 组件库部分组件的二次封装使用,记录下长期开发过程中积累的一些经验和网络学习的新技巧,组件二次封装需要解决的无非就是属性 props 如何透传出去,如何复用原有组件的插槽,以及如何调用原组件暴露的方法,这里以比较热门的 UI 框架 Element Plus 为例。

【vue-admin-kit】配置驱动的 Vue 3 后台管理组件工具套件

📖 在线文档:https://vue-admin-kit.jiang.in/ 前言 在企业级后台管理系统开发中,CRUD 页面占据了大量的开发工作。搜索表单、数据表格、新增/编辑弹窗、详情展示……这些重复性的工作不仅耗时,还容易产生不一致的代码风格。 vue-admin-kit 正是为解决这一痛点

[CSS] 移除元素焦点状态

padding: '0' - 移除内边距 border: 'none' - 移除边框 outline: 'none' - 移除焦点时的轮廓线(这是关键!) boxShadow: 'none' - 移除可能的阴影效果

Table of Contents
No Table of Contents
Copyright © 2024 your company All Rights Reserved. Powered by Halo.