[vue3] 组件封装技巧

2,102 阅读2分钟

一、前言

文章包含我对封装的一些理解,并提供几种技巧和封装实例,包括弹窗(抽屉)的封装套路、Tooltip根据父元素宽度自动显示、按钮组的封装。

二、技巧

封装在我看来有两种:

  1. 页面上重复了,我封装方便一改多,这种一般和业务强耦合
  2. 对原有组件的增强。比如给组件库的某一组件,再包裹封装一层,以统一样式风格、补充功能

对象prop处理:

  1. 对象内部的属性声明必不可少,不然别人无法理解要传什么
  2. 使用watch或watchEffect监听对象prop的变化,更新到组件内部对象中
  3. 由于对象是引用类型,所以赋值时需要拷贝,否则会改变原始prop,不符合单向数据流的思想

v-bind="$attrs" 直接绑定所有透传属性,不用在一个个声明

弹窗或抽屉组件的显示隐藏通过方法来控制而不是prop,这样更好维护(调用组件不用管显示状态)

按钮组的封装:优先插槽,因为扩展性最强,其次通过传递按钮列表控制

怎么调用内部组件的方法?

<el-table ref="tableRef">

// 向外暴露el-table实例,以调用它的内部方法
defineExpose({
    getTableInstance: () => tableRef.value,
})

三、表单弹窗

通过这个例子演示弹窗(抽屉)的封装套路,思路都是一样的。

(1)封装

<template>
    <a-modal
        title="表单弹窗"
        v-model:open="formModal.open"
        @ok="formModal.handleConfirm"
        @cancel="formModal.closeModal"
    >
        <a-form ref="formRef" :model="formModal.form" :rules="formModal.rules">
            <a-form-item label="名称" name="name">
                <a-input v-model:value="formModal.form.name" />
            </a-form-item>
            <a-form-item label="描述" name="desc">
                <a-textarea v-model:value="formModal.form.desc" />
            </a-form-item>
        </a-form>
    </a-modal>
</template>

<script lang="ts" setup>
const props = defineProps(['form']);
const emit = defineEmits(['confirm']);

const formRef = ref();
const formModal = reactive({
    open: false,
    form: {
        name: '',
        desc: '',
    },
    rules: {
        name: [{ required: true, message: '名称不能为空', trigger: 'change' }],
        desc: [{ required: true, message: '请输入描述', trigger: 'blur' }],
    },
    showModal: () => {
        formModal.open = true;
    },
    closeModal: () => {
        formModal.open = false;
        /**
         * resetFields 无法更新初始值,只能重置为第一次传入的 form
         * 解决方法:<a-form v-if="formModal.open" ...
         */
        formRef.value.resetFields();
    },
    handleConfirm: () => {
        formRef.value
            .validate()
            .then(() => {
                emit('confirm', formModal.form);
            })
            .catch((error) => {
                console.log('error', error);
            });
    },
});

/** 跟踪 props.form 的变化,更新 formModal.form */
watchEffect(() => {
    /**
     * Object.assign 是浅拷贝,只复制第一层,引用类型还是复制原来的引用
     * JSON.parse(JSON.stringify()) 是深拷贝,但注意:undefined 和 function 会被过滤、Date 会转字符串
     */
    Object.assign(formModal.form, props.form);
});

defineExpose({
    showModal: formModal.showModal,
    closeModal: formModal.closeModal,
});
</script>

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

<style lang="scss"></style>

(2)使用

<template>
    <div>
        <a-button type="primary" @click="formModalRef?.showModal">打开弹窗</a-button>
        <FormModal ref="formModalRef" :form="formModal.form" @confirm="formModal.handleConfirm" />
    </div>
</template>

<script lang="ts" setup>
import FormModal from './FormModal.vue';

/** 通过调用方法来控制弹窗的显示隐藏 */
const formModalRef = ref<{ showModal: () => void; closeModal: () => void }>();
const formModal = reactive({
    /** 表单的初始数据 */
    form: {
        name: '123',
        desc: '',
    },
    /** 提交表单 */
    handleConfirm: (newForm) => {
        console.log(newForm);
        // formModalRef.value.closeModal();
    },
});
</script>

四、Tooltip增强:根据父元素宽度自动显示

用于表格的单元格中,可以仅在内容溢出时显示Tooltip。

(1)封装

<template>
    <a-tooltip v-bind="$attrs" v-if="showTooltip || !auto" color="rgba(9, 30, 66, 0.7)" overlayClassName="yc-tooltip">
        <template #title>
            <slot name="title"></slot>
        </template>

        <div class="single-line-ellipsis">
            <span>
                <slot></slot>
            </span>
        </div>
    </a-tooltip>

    <div v-else class="single-line-ellipsis" @mouseenter="handleMouseenter">
        <span ref="contentRef">
            <slot></slot>
        </span>
    </div>
</template>

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

const props = withDefaults(
    defineProps<{
        auto?: boolean;
    }>(),
    {
        auto: false, // 是否根据父元素宽度自动显示,常用于单元格内
    },
);

const showTooltip = ref(false);
const contentRef = ref<HTMLElement>();

// 悬浮处理:判断内容宽度是否超过父元素宽度,如果超过则显示Tooltip
const handleMouseenter = () => {
    if (!contentRef.value || !contentRef.value.parentNode || !props.auto) return;

    const contentWidth = contentRef.value.offsetWidth;
    const parentWidth = (contentRef.value.parentNode as HTMLElement).offsetWidth;

    showTooltip.value = contentWidth > parentWidth;
};
</script>

<style lang="scss">
.yc-tooltip {
    font-size: 14px;
    max-width: 300px;

    .ant-tooltip-inner {
        max-height: 98px; // 正好四行,超过可以滚动
        overflow: auto;
    }
}

.single-line-ellipsis {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    word-break: break-all;
}
</style>

(2)使用

<YcTooltip auto>
    <template #title>测试</template>
    <span>123</span>
</XTooltip>

五、按钮组:传递按钮列表

// 伪代码,参考思路即可 ==================================================
<Card :btnList="btnList" />

btnList = [{
    type: '',
    label: '查看',
    event: 'handleView'
    },{
    type: 'danger',
    label: '删除',
    event: 'handleDelete'
    },{
    type: 'primary',
    label: '下载',
    event: 'handleDownload'
}]

<el-button v-for="item in btnList" :key="item.label" :type="item.type" @click="callback(item.event)">
    {{ item.label }}
</el-button>

const emit = defineEmits(['handleExamine', 'handleDelete', 'handleDownload','handleAdd'])
const callback = (event) => {
    emit(`${event}`);
}

六、最后

封装是件耗时的事情,往往比你想的要复杂,所以如果时间不够,就不封装直接复制!

如果帮到你了,可以点个赞,欢迎交流沟通。