UI组件二次封装的正确打开思路

背景

在日常开发过程中,产品经理经常会提出一些看似简单但实现起来颇具挑战的需求。为了更高效地满足这些业务场景,我们通常需要对现有的 UI 组件进行二次封装。例如,基于 ElementUI 或 ElementPlus 提供的组件进行功能扩展与定制,使其更贴合具体业务需求。

本文将以对 ElementPlus 的 <el-input> 组件进行二次封装为例,创建一个自定义组件 <my-input>,并通过该示例展示如何进行合理、规范的组件封装。

解决思路

本次封装的目标是确保自定义组件具备高度的兼容性和可复用性,具体实现思路如下:

  • 属性透传 :保留并支持 <el-input> 的所有原生attributes,确保原有配置能力不受影响。
  • 插槽透传 :支持 <el-input> 的所有插槽slots,保持原有结构扩展能力。
  • 事件透传 :支持 <el-input> 所有对外抛出的事件,确保行为一致性与交互完整性。

属性透传

使用$attrs可以实现左右属性透传,特定组件自定义的参数依旧可以使用,毫不冲突

vue 复制代码
// my-input.vue
<template>
  <section class="my-input-wrap">
    <section class="input__label">
      {{ label }}
    </section>
    <el-input v-bind="$attrs" />
    <section class="input__desc">
      {{ desc }}
    </section>
  </section>
</template>

<script setup lang="ts">
defineProps({
  label: {
    type: String,
    default: '',
  },
  desc: {
    type: String,
    default: '',
  },
});
</script>
<style lang="scss" scoped>
.my-input-wrap {
  padding: 16px;
  border: 1px solid #ececec;
  border-radius: 8px;

  .input__label {
    position: relative;
    display: flex;
    align-items: center;
    height: 36px;
    padding-left: 8px;
    font-size: 14px;
    font-weight: 600;
    color: #111;

    &::before {
      position: absolute;
      top: 50%;
      left: 0;
      width: 2px;
      height: 10px;
      margin-top: -5px;
      content: '';
      background-color: #007af5;
      border-radius: 4px;
    }
  }

  .input__desc {
    margin-top: 8px;
    color: #999;
  }
}
</style>

在父组件中使用

vue 复制代码
<MyInput
    v-model="inputValue"
    label="输入框"
    placeholder="请输入内容"
    desc="我这里传入的是描述信息"
/>

效果展示

插槽透传

<el-input>组件可以支持很多插槽,我们二次封装之后,应该如何处理插槽呢?聪明的你应该想到使用 slots,然后我们动态便利父组件传入的slots来实现保持<el-input>组件原有结构扩展能力。具体代码实现如下:

vue 复制代码
// my-input.vue 核心代码

<template>
  <section class="my-input-wrap">
    <section class="input__label">
      {{ label }}
    </section>
    <el-input v-bind="$attrs">
      <template
        v-for="(_, name) in slots"
        :key="name"
        #[name]="slotData"
      >
        <slot
          :name="name"
          v-bind="slotData || {}"
        />
      </template>
    </el-input>
    <section class="input__desc">
      {{ desc }}
    </section>
  </section>
</template>

<script setup lang="ts">
import { useSlots } from 'vue';
defineProps({
  label: {
    type: String,
    default: '',
  },
  desc: {
    type: String,
    default: '',
  },
});

const slots = useSlots();
</script>

在父组件中使用

vue 复制代码
<MyInput
  v-model="inputValue"
  label="输入框"
  placeholder="请输入内容"
  desc="我这里传入的是描述信息"
>
  <template #prepend>
    <el-select
      placeholder="Select"
      style="width: 115px;"
    >
      <el-option
        label="Restaurant"
        value="1"
      />
      <el-option
        label="Order No."
        value="2"
      />
      <el-option
        label="Tel"
        value="3"
      />
    </el-select>
  </template>
  <template #append>
    .com
  </template>
</MyInput>

效果展示

事件透传

在 Vue 中,我们都知道可以通过 ref 在父组件中访问子组件通过 expose 暴露出来的方法和属性。然而,直接将 ref 透传到更深层的子组件是无法直接实现的。不过我们可以这样思考:既然在中间子组件中通过 ref 能够获取到其内部子组件暴露的方法和属性,那么我们将这些内容再次通过 expose 暴露出去,不就实现了 ref 的透传了吗?

vue 复制代码
// my-input.vue核心代码
<template>
  <section class="my-input-wrap">
    <section class="input__label">
      {{ label }}
    </section>
    <el-input
      v-bind="$attrs"
      ref="myCustomInputRef"
    >
      <template
        v-for="(_, name) in slots"
        :key="name"
        #[name]="slotData"
      >
        <slot
          :name="name"
          v-bind="slotData || {}"
        />
      </template>
    </el-input>
    <section class="input__desc">
      {{ desc }}
    </section>
  </section>
</template>

<script setup lang="ts">
import { onMounted, useSlots, ref } from 'vue';
defineProps({
  label: {
    type: String,
    default: '',
  },
  desc: {
    type: String,
    default: '',
  },
});

const slots = useSlots();

const myCustomInputRef = ref();
const exposedInfo = {} as Record<string, any>;

const getExposedInfo = () => {
  const myInstance = myCustomInputRef.value;
  if (myInstance) {
    // 显式列出需要暴露的方法
    const methodNames: string[] = [
      'blur',
      'clear',
      'focus',
      'input',
      'ref',
      'resizeTextarea',
      'select',
      'textarea',
      'textareaStyle',
      'isComposing',
    ];

    methodNames.forEach((key) => {
      const method = myInstance[key];
      if (typeof method === 'function') {
        exposedInfo[key] = (...args: any[]) =>
          method?.apply(myInstance, args);
      } else {
        exposedInfo[key] = method;
      }
    });
  }
};

onMounted(() => {
  getExposedInfo();
});

defineExpose(exposedInfo);
</script>

父组件中使用

ts 复制代码
...

<MyInput
  ref="myInputRef"
  v-model="inputValue"
  label="输入框"
  placeholder="请输入内容"
  desc="我这里传入的是描述信息"
/>

...

const myInputRef = ref();

onMounted(() => {
  myInputRef.value?.focus();
});

...

效果展示

总结

主要是为了展示如何实现下面三点的内容,如果需要实现其他需求,大侠你自己举一反三吧🤗🤗!哈哈...

  • 属性透传 :保留并支持 <el-input> 的所有原生attributes
  • 插槽透传 :支持 <el-input> 的所有插槽slots
  • 事件透传 :支持 <el-input> 所有对外抛出的事件。
相关推荐
layman05281 小时前
webpack5 css-loader:从基础到原理
前端·css·webpack
半桔1 小时前
【前端小站】CSS 样式美学:从基础语法到界面精筑的实战宝典
前端·css·html
AI老李1 小时前
PostCSS完全指南:功能/配置/插件/SourceMap/AST/插件开发/自定义语法
前端·javascript·postcss
_OP_CHEN1 小时前
【前端开发之CSS】(一)初识 CSS:网页化妆术的终极指南,新手也能轻松拿捏页面美化!
前端·css·html·网页开发·样式表·界面美化
啊哈一半醒1 小时前
CSS 主流布局
前端·css·css布局·标准流 浮动 定位·flex grid 响应式布局
PHP武器库1 小时前
ULUI:不止于按钮和菜单,一个专注于“业务组件”的纯 CSS 框架
前端·css
电商API_180079052471 小时前
第三方淘宝商品详情 API 全维度调用指南:从技术对接到生产落地
java·大数据·前端·数据库·人工智能·网络爬虫
晓晓莺歌1 小时前
vue3某一个路由切换,导致所有路由页面均变成空白页
前端·vue.js
Up九五小庞2 小时前
开源埋点分析平台 ClkLog 本地部署 + Web JS 埋点测试实战--九五小庞
前端·javascript·开源
qq_177767373 小时前
React Native鸿蒙跨平台数据使用监控应用技术,通过setInterval每5秒更新一次数据使用情况和套餐使用情况,模拟了真实应用中的数据监控场景
开发语言·前端·javascript·react native·react.js·ecmascript·harmonyos