对于element-plus弹窗组件的思考

前言

Dialog组件是我们日常开发中使用最多的组件之一,怎样使用才能让代码更加优雅是我们一直讨论的问题,特别是现在VueCompositionApi更加方便我们逻辑的拆分,在这种背景下,我们怎么更好的使用弹窗才能使代码更加清晰呢?

日常开发现状

通过状态去驱动弹窗的状态

思考

如果按上面的做法,每写一个弹窗组件则父组件里面需要引入组件在模板中使用组件声明visible声明data声明点击显示以及关闭的事件。虽然这样看似没什么问题,但如果弹窗有多个呢?visible1、visible2...吗?业务再复杂点呢?这简直是恐怖片,毕竟开发过程中最困扰我的就是变量的命名。

那我们要怎么去优化这些痛点呢?其实visible的值到底是什么我们并不关注,我们只需要显示关闭的方法而已,而data这个变量一般都是为了将数据传给Dialog组件使用而已,如果可以直接传给子组件的话,我们都不需要这个变量。当然,如果可以不在父组件引入这些Dialog组件那就更好了,这样逻辑拆分就可以更加彻底.

我期望的

  • 尽可能少的命名变量。
  • 使用一个函数暴露出来关闭/显示的方法。
  • 不需要声明一个变量来传递数据给Dialog组件
  • 是否能不在模板中挂载组件,这样逻辑更加聚焦,可以的话一个hook就是一个弹窗相关的所有代码,而使用者只需要关注怎么关闭/打开弹窗就行。
  • 类型推导更加便捷,因为我是typescript重度患者

实现思路

封装一个hook管理弹窗的显示/隐藏,并且暴露出开启关闭的方法。同时将弹窗的内容用函数来渲染(h函数或者JSX都行,重点推荐JSX)。

那么还有一个问题,弹窗不想主动挂载到模板中要怎么实现?这里我借鉴了大多组件库的思路,将大量公共的东西使用Provide挂载在顶级节点。每次使用hook就会主动在最顶层创建一个弹窗,并且管理起来。

实现方案

tsx 复制代码
// Provider.tsx
import { defineComponent, ref, provide } from 'vue';
import { ElDialog, ElButton } from 'element-plus';
import {
  IDialogRefsItem,
  ICreateDialogOptions,
  DialogInjectProps,
} from './type';

export default defineComponent({
  name: 'Provider',
  components: {
    ElDialog,
    ElButton,
  },
  setup() {
    const dialogRefs = ref<IDialogRefsItem[]>([]);
    const createId = () => `${Date.now()}_${Math.random().toString(16)}`;
    const create = (config: () => ICreateDialogOptions) => {
      const id = createId();
      dialogRefs.value.push({
        id,
        config,
        visible: false,
      });
      return id;
    };
    const open = (id: string) => {
      const row = dialogRefs.value.find(v => v.id === id);
      if (row) {
        row.visible = true;
      }
    };
    const close = (id: string) => {
      const row = dialogRefs.value.find(v => v.id === id);
      if (row) {
        row.visible = false;
        const { onClose } = row.config();
        onClose?.();
      }
    };
    const destroy = (id: string) => {
      const index = dialogRefs.value.findIndex(v => v.id === id);
      if (index !== -1) {
        dialogRefs.value.splice(index, 1);
      }
    };
    provide<DialogInjectProps>('DIALOG_INJECT_KEY', {
      create,
      open,
      close,
      destroy,
    });
    return {
      dialogRefs,
      close,
    };
  },
  render() {
    return (
      <>
        {this.$slots?.default?.()}
        {this.dialogRefs.map((v) => {
          const { config, id, visible } = v;
          const {
            showCancelBtn = true,
            showConfirmBtn = true,
            confirmText = '确定',
            cancelText = '取消',
            onConfirm = () => {},
            render,
            renderHeader,
            renderFooter,
            content,
            onClose,
            onCancel = () => {
              this.close(id);
            },
            ...rest
          } = config();
          return (
            <el-dialog
              {...rest}
              model-value={visible}
              onClose={() => this.close(id)}
              v-slots={{
                default: render ?? (() => content),
                header: renderHeader ?? undefined,
                footer:
                  renderFooter ?? (() => (
                    <>
                      {showCancelBtn && (
                        <el-button onClick={onCancel}>{cancelText}</el-button>
                      )}
                      {showConfirmBtn && (
                        <el-button type='primary' onClick={onConfirm}>
                          {confirmText}
                        </el-button>
                      )}
                    </>
                  )),
              }}
            />
          );
        })}
      </>
    );
  },
});
tsx 复制代码
// useDialog.ts
import { inject, onBeforeUnmount } from 'vue';
import {
  DialogInjectProps,
  ICreateDialogOptions,
} from './type';

export const useDialog = (options: () => ICreateDialogOptions) => {
  const dialogInjectProps = inject<DialogInjectProps>('DIALOG_INJECT_KEY');
  const dialogId = dialogInjectProps?.create(options);
  if (!dialogInjectProps) {
    console.error('useDialog需要配合DialogProvider使用');
  }
  /** 打开弹窗 */
  const openDialog = () => {
    if (dialogId) {
      dialogInjectProps?.open(dialogId);
    }
  };
  /** 关闭弹窗 */
  const closeDialog = () => {
    if (dialogId) {
      dialogInjectProps?.close(dialogId);
    }
  };
  /** 组件销毁时销毁弹窗 */
  onBeforeUnmount(() => {
    if (dialogId) {
      dialogInjectProps?.destroy(dialogId);
    }
  });
  return {
    openDialog,
    closeDialog,
  };
};
typescript 复制代码
// type.ts
import type { DialogProps } from 'element-plus';

export interface ICreateDialogOptions extends DialogProps {
  /** Dialog 对话框 Dialog 的内容 */
  content?: string;
  /** 当关闭 Dialog 时,销毁其中的元素 */
  render?: () => JSX.Element | null | string | number | JSX.Element[];
  /** 当关闭 Dialog 时,销毁其中的元素 */
  renderHeader?: () => JSX.Element | null | string | number | JSX.Element[];
  /** 当关闭 Dialog 时,销毁其中的元素 */
  renderFooter?: () => JSX.Element | null | string | number | JSX.Element[];
  /** 弹窗关闭回调事件 */
  onClose?: () => void;
  /** 确定按钮文案 */
  confirmText?: string;
  /** 点击确定按钮 */
  onConfirm?: () => void;
  /** 取消按钮文案 */
  cancelText?: string;
  /** 点击取消按钮 */
  onCancel?: () => void;
  /** 是否显示确定按钮 */
  showConfirmBtn?: boolean;
  /** 是否显示取消按钮 */
  showCancelBtn?: boolean;
}

export interface IDialogRefsItem {
  id: string;
  visible: boolean;
  config: () => ICreateDialogOptions;
}

export interface DialogInjectProps {
  /** 创建弹窗 */
  create: (options: () => ICreateDialogOptions) => string;
  /** 打开弹窗 */
  open: (id: string) => void;
  /** 关闭弹窗 */
  close: (id: string) => void;
  /** 销毁弹窗 */
  destroy: (id: string) => void;
}

效果展示

App.vue挂载Provider

vue 复制代码
<template>
  <Provider>
    <router-view />
  </Provider>
</template>

父组件

vue 复制代码
<template>
  <el-button @click="handleUpdate">修改</el-button>
</template>
<script>
const { handleUpdate } = useUpdate();
</script>

对应编辑的hook

jsx 复制代码
export const useUpdate = () => {
  // 表单数据
  const formData = ref({
    userId: '',
    username: '',
  });
  const { openDialog, closeDialog } = useDialog(() => ({
    title: '编辑账号',
    width: 600,
    render() {
      return (
        <el-form />
      );
    },
  }));
  const handleUpdate = async (data) => {
    formData.value.userId = data.userId;
    formData.value.username = data.username ?? '';
    openDialog();
  };
  return {
    handleUpdate
  };
};

结语

本方案未必是最优解,只是暂时对我来说相对比较方便,当然还有一些小问题,比如因为挂载在Provider下导致ref='el'的语法糖失效,还有作用域失效,在render中非全局引入组件只能直接引入组件使用(<x-button/>无法resolve,只能<XButton/>),具体要怎么解决有兴趣的可以探索一下,类似element-plusMessageBox中的修改appContext的方法或许可行。

相关推荐
passerby60612 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了2 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅2 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅2 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅3 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment3 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅3 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊3 小时前
jwt介绍
前端
爱敲代码的小鱼3 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax
吹牛不交税3 小时前
admin.net-v2 框架使用笔记-netcore8.0/10.0版
vue.js·.netcore