Home
avatar

.Wang

部分组件的拆分以及优化-Dialog优化

近期在回顾公司项目的开发,发现了一些问题:组件的拆分以及业务组件的使用很容易混淆,其实这些也不算什么大问题。现在有很多重复性较高的代码,不论是组件的封装还是弹窗的重复使用等等。但我的想法是这些不是很重要,没必要重复调用使用,对我来说一次即可。

  1. el-dialog, vxe-dialog组件弹窗的使用:现在很多业务组件的弹窗都是使用el-dialog或者vxe-dialog组件进行封装,现在每个业务组件的弹窗的代码重复度较高。 那么这些组件的封装,是否可以进行抽离呢?
  2. el-image, img组件的使用:对于列表图片,详情图片,以及预览图片还可以提升加载性能以及对其优化。
  3. el-button操作组的使用:通常操作组带来的是多个可操作的按钮(按钮组), 当然每个按钮的形状。主题色等也是不同的,并且部分按钮还附带操作权限等等。

我对以上几个问题进行说明记录。

Dialog 的处理

先大概看一下之前的逻辑:

A

之前是点击一个按钮弹一个弹窗,但是这个弹窗的代码大致如下:

<!-- 创建订单的弹窗 -->
<template>
	<el-dialog v-model="dialogVisible">
		<!-- 内容 -->
	</el-dialog>
</template>

<!-- 编辑订单的弹窗 -->
<template>
	<el-dialog v-model="dialogVisible">
		<!-- 内容 -->
	</el-dialog>
</template>

<!-- ... -->

el-dialog外层的属性都是大差不差的,props都是内置的属性,但是只有内容区域是不同的。内容区域是不是可以是一个动态组件?

组件的封装

一个完善的中后台系统,它带有不同业务场景下的弹窗,大致可分为:

  1. 全局弹窗: 需要显示在APP层, 例如:消息公告,通知等。
  2. 局部弹窗: 需要显示在页面层,例如:修改订单,添加订单等。

局部弹窗

局部弹窗我采用的是函数式弹窗,也就是通过代码的形式去调用渲染el-dialog, 当然也可使用组件的形式,大致逻辑如下:

A

代码如下:

<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组件 + 动态组件实现:

A

大致代码如下:

<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加载方式…

Vue