部分组件的拆分以及优化-Dialog优化
近期在回顾公司项目的开发,发现了一些问题:组件的拆分以及业务组件的使用很容易混淆,其实这些也不算什么大问题。现在有很多重复性较高的代码,不论是组件的封装还是弹窗的重复使用等等。但我的想法是这些不是很重要,没必要重复调用使用,对我来说一次即可。
el-dialog
,vxe-dialog
组件弹窗的使用:现在很多业务组件的弹窗都是使用el-dialog
或者vxe-dialog
组件进行封装,现在每个业务组件的弹窗的代码重复度较高。 那么这些组件的封装,是否可以进行抽离呢?el-image
,img
组件的使用:对于列表图片,详情图片,以及预览图片还可以提升加载性能以及对其优化。el-button
操作组的使用:通常操作组带来的是多个可操作的按钮(按钮组), 当然每个按钮的形状。主题色等也是不同的,并且部分按钮还附带操作权限等等。
我对以上几个问题进行说明记录。
Dialog 的处理
先大概看一下之前的逻辑:
之前是点击一个按钮弹一个弹窗,但是这个弹窗的代码大致如下:
<!-- 创建订单的弹窗 -->
<template>
<el-dialog v-model="dialogVisible">
<!-- 内容 -->
</el-dialog>
</template>
<!-- 编辑订单的弹窗 -->
<template>
<el-dialog v-model="dialogVisible">
<!-- 内容 -->
</el-dialog>
</template>
<!-- ... -->
el-dialog
外层的属性都是大差不差的,props
都是内置的属性,但是只有内容区域是不同的。内容区域是不是可以是一个动态组件?
组件的封装
一个完善的中后台系统,它带有不同业务场景下的弹窗,大致可分为:
- 全局弹窗: 需要显示在
APP
层, 例如:消息公告,通知等。 - 局部弹窗: 需要显示在
页面
层,例如:修改订单,添加订单等。
局部弹窗
局部弹窗我采用的是函数式弹窗,也就是通过代码的形式去调用渲染el-dialog
, 当然也可使用组件的形式,大致逻辑如下:
代码如下:
<script setup lang="ts">
const onClickFunc = () => showDIalog();
</script>
<template>
<el-button type="primary" @click="onClickFunc">
【函数式调用】:点击按钮弹窗查看用户信息
</el-button>
</template>
// ==========================================
// 当前的业务组件
// ==========================================
const dialogProps = { visible: true, title: "查看用户信息" };
const dialogSlots: DialogOptions["dialogSlots"] = {
// dialogUserInfo 是你的vue组件
default: () => h(dialogUserInfo, {}),
footer: () => {
return h("div", { style: "text-align: center;" }, [
h(
ElButton,
{
type: "default",
onClick: closeDialog,
},
{ default: () => "关闭窗口" }
),
h(ElButton, { type: "primary" }, { default: () => "保存数据" }),
]);
},
header: () => null,
};
const { show: showDIalog, close: closeDialog } = setDialog(
dialogProps,
dialogSlots
);
export { showDIalog, closeDialog };
// 这是一个核心函数,用于创建一个dialog 并渲染
/**
* @description 创建一个dialog
* @param dialogProps 弹窗的props
* @param dialogSlots 弹窗的slots
* @returns
*/
function setDialog(
dialogProps: DialogOptions["dialogProps"],
dialogSlots: DialogOptions["dialogSlots"]
) {
let vNode: VNode | null;
const props = reactive({
...dialogProps,
"onUpdate:modelValue": (val: boolean) => {
if (!val) {
// 这里是关闭弹窗卸载dialog
render(null, document.body);
vNode = null;
}
},
});
const onRender = (show: boolean) => {
vNode = h(ElDialog, { ...props, modelValue: show }, dialogSlots);
render(vNode, document.body);
};
return {
show: () => onRender(true),
close: () => onRender(false),
};
}
// types
import { type Component } from "vue";
import { type DialogProps } from "element-plus";
type Slots = () => Component | null;
// dialog-function
export type DialogOptions = {
// dialog 的props
dialogProps: Partial<Omit<DialogProps, "modelValue">>;
// 渲染插槽
dialogSlots?: {
default?: Slots;
header?: Slots;
footer?: Slots;
};
};
全局弹窗
全局弹窗我使用了vue组件 + 动态组件
实现:
大致代码如下:
<script setup lang="ts">
const onClickGlobal = () => {
openDialog({
// 控制弹窗的显示隐藏
visible: true,
// el-dialog的props
appendToBody: true,
draggable: true,
title: '查看用户信息',
// 渲染的内容组件
renderComponent: dialogUserInfo,
// 渲染的内容组件的props
componentProps: {
data: {
...default_user_info,
userName: 'global调用传递',
},
},
})
},
};
</script>
<template>
<el-button type="primary" @click="onClickGlobal">
【全局弹窗】:点击按钮弹窗查看用户信息
</el-button>
</template>
<!-- 全局弹窗 -->
<script setup lang="ts">
function isComponent(component: any): component is Component {
return component && component.render;
}
const onGetComponent = (item: Dialog) => {
if (!item.renderComponent) {
console.warn("renderComponent is undefined");
return null;
}
// props.renderComponent 支持俩种形式引入
// 1. 函数
// 2. 组件
if (isComponent(item.renderComponent)) return item.renderComponent;
if (
typeof item.renderComponent === "function" &&
!(item.renderComponent as any).then
) {
return defineAsyncComponent<Component>({ loader: item.renderComponent });
}
return item.renderComponent;
};
</script>
<template>
<el-dialog
v-for="(item, index) in dialogs"
:key="index"
v-bind="item"
v-model="item.visible"
@close="onCloseDialog(item, index)"
>
<component
:is="onGetComponent(item)"
v-bind="item.componentProps"
></component>
</el-dialog>
</template>
<style lang="scss" scoped></style>
// 处理全局弹窗的核心内容
const dialogs = ref<Dialog[]>([]);
const delay = 500;
function isComponent(component: any): component is Component {
return component && component.render;
}
const openDialog = (props: Dialog) => {
let renderComponent: Dialog["renderComponent"] = props.renderComponent;
// 由于这里会创建一个虚拟节点,需要使用shallowRef
if (isComponent(props.renderComponent)) {
renderComponent = shallowRef(props.renderComponent);
}
const open = () =>
dialogs.value.push(
Object.assign(props, { renderComponent, visible: true })
);
if (props?.openDelay) {
setTimeout(() => {
open();
}, props.openDelay ?? delay);
} else {
open();
}
};
const onCloseDialog = (options: Dialog, index: number) => {
options.visible = false;
setTimeout(() => {
dialogs.value.splice(index, 1);
}, options.closeDelay ?? delay);
};
export { dialogs, openDialog, onCloseDialog };
后期我会极细补充el-button
按钮组, el-image
加载方式…