前言

最近在开发一个较为复杂的表单页面时,我遇到一个场景:页面中包含了一些自定义封装的组件,需要让这些组件的 size 属性(用于控制尺寸)能够自动继承自父级 el-form 组件的设置。

我们知道,在 Element Plus 中,只要为 <el-form> 组件设置了 size 属性(如 largesmall),其内部的 el-inputel-button 等官方组件都能自动继承这个尺寸。 但对于我们自己开发的组件,例如一个自定义的 <Icon> 组件,它默认是无法感知到 el-formsize 设置的。

那么,如何才能让我们的自定义组件也具备这种“继承”能力,与整个表单风格保持统一呢?

实现原理:探索 Element Plus 的源码

通过阅读 Element Plus 的源码,我发现其内部是借助 Vue 3 的 provideinject API 来实现跨组件通信和属性继承的。 这种机制允许祖先组件向其所有后代组件注入依赖,无论层级有多深。

1. el-form 的数据提供 (Provide)

el-form 组件作为“提供者”,会将其自身的上下文(包含 props、事件发射器和各种内部方法)通过 provide 函数向下传递。这个上下文被一个名为 formContextKey 的唯一 InjectionKey 标记。

// Element Plus 源码片段:el-form 提供上下文
import { formContextKey } from './constants'

// ...
provide(
  formContextKey,
  reactive({
    ...toRefs(props), // 包含了 size, disabled 等属性
    emit,

    // ... 其他方法
    resetFields,
    clearValidate,
    validateField,
  })
)

2. 子组件的数据注入 (Inject)

Element Plus 的表单子组件(如 el-input)则通过 inject 来获取这个上下文。官方为了逻辑复用,将其封装成了一个 useFormSize Hook。

这个 Hook 的逻辑非常严谨,它会按照 自身 props.size > el-form-itemsize > el-formsize > 全局配置的 size 的优先级来确定最终的组件尺寸。

// Element Plus 源码片段:useFormSize Hook
import { formContextKey, formItemContextKey } from './constants'
import { useGlobalSize } from '@element-plus/hooks'

export const useFormSize = (...) => {
  const size = useProp<ComponentSize>('size')
  const globalConfig = useGlobalSize()
  const form = inject(formContextKey, undefined)
  const formItem = inject(formItemContextKey, undefined)

  return computed(
    (): ComponentSize =>
      // 优先级从高到低
      size.value ||          // 1. 组件自身的 size 属性
      formItem?.size ||      // 2. form-item 的 size 属性
      form?.size ||          // 3. form 的 size 属性
      globalConfig.value ||  // 4. 全局配置
      ''
  )
}

实践教程:在自定义组件中实现继承

理解了原理之后,我们就可以在自己的组件中轻松实现相同的效果。我们不需要像官方 useFormSize Hook 那样处理复杂的优先级,通常只需要获取 el-form 的上下文即可。

下面是一个简单的实现步骤:

1. 导入 formContextKey

首先,从 element-plus 中导入 formContextKey。这是一个 InjectionKey,可以确保类型安全。

2. 定义组件 props

在自定义组件中,我们仍然需要定义自己的 size prop,以允许用户进行强制覆盖。

3. 注入并计算最终尺寸

使用 inject 获取 el-form 的上下文,并创建一个计算属性 componentSize。这个计算属性的逻辑是:

  • 如果用户为当前组件明确传入了 size prop,则优先使用该 prop。
  • 否则,尝试从注入的 formContext 中获取 size
  • 如果两者都不存在,则回退到一个默认值(如 'default')。

完整代码示例 (MyIcon.vue)

<script setup lang="ts">
import { computed, inject } from 'vue';
import { formContextKey } from 'element-plus';
import type { ComponentSize } from 'element-plus';

// 1. 定义 props,允许用户覆盖
const props = defineProps<{
  size?: ComponentSize;
}>();

// 2. 注入来自 el-form 的上下文
const formContext = inject(formContextKey, undefined);

// 3. 计算最终生效的 size
// 优先级:props.size > formContext.size > 'default'
const componentSize = computed<ComponentSize>(
  () => props.size ?? formContext?.size ?? 'default'
);

// 你可以在模板中根据 componentSize 的值来应用不同的样式
// 例如::style="{ fontSize: componentSize === 'large' ? '24px' : '16px' }"
</script>

<template>
  <i :class="`my-icon--${componentSize}`">
    <!-- 图标内容 -->
    ...
  </i>
</template>

<style>
.my-icon--large {
  font-size: 20px;
}
.my-icon--default {
  font-size: 16px;
}
.my-icon--small {
  font-size: 12px;
}
</style>

通过以上方式,我们的自定义组件就“学会”了如何继承 el-formsize 属性,实现了与 Element Plus 生态更深度、更一致的集成。

总结

通过 provideinject,我们可以安全地访问 Element Plus 内部组件的上下文,实现自定义组件与官方组件的无缝协作。这种方法不仅限于 size 属性,同样适用于 disabled 等其他可以在 el-form 上统一设置的属性,极大地提高了自定义表单组件的可复用性和一致性。

分类: vue 项目实战 标签: 继承element-plussizeel-form

评论

暂无评论数据

暂无评论数据

目录