一、前言
文章包含我对封装的一些理解,并提供几种技巧和封装实例,包括弹窗(抽屉)的封装套路、Tooltip根据父元素宽度自动显示、按钮组的封装。
二、技巧
封装在我看来有两种:
- 页面上重复了,我封装方便一改多,这种一般和业务强耦合
- 对原有组件的增强。比如给组件库的某一组件,再包裹封装一层,以统一样式风格、补充功能
对象prop处理:
- 对象内部的属性声明必不可少,不然别人无法理解要传什么
- 使用watch或watchEffect监听对象prop的变化,更新到组件内部对象中
- 由于对象是引用类型,所以赋值时需要拷贝,否则会改变原始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}`);
}
六、最后
封装是件耗时的事情,往往比你想的要复杂,所以如果时间不够,就不封装直接复制!
如果帮到你了,可以点个赞,欢迎交流沟通。