vue3: 多 Modal 框业务逻辑 - 动态创建 -

2,695 阅读3分钟

vue3: 多 Modal 框业务逻辑代码优化

背景介绍

开发中经常会遇到如下场景. 点击按钮, 通过变量 visible 控制弹框的显示和隐藏:

export default defineComponent({
  setup() {
    const visible = ref(false);
    return {
      visible,
    };
  },
  render() {
    return (
      <>
        <Button
          onClick={() => {
            this.visible = true;
          }}
        >
          显示弹框
        </Button>
        <MyModal
          visible={visible}
          onClose={() => {
            this.visible = false;
          }}
        />
      </>
    );
  },
});

但是随着业务不断的迭代, 一个页面上可能会存在多个 Modal 框, 此时我们该如何更优雅的处理呢? 如:

export default defineComponent({
  setup() {
    const visible1 = ref(false);
    const visible2 = ref(false);
    const visible3 = ref(false);
    return {
      visible1,
      visible2,
      visible3,
    };
  },
  render() {
    return (
      <>
        <Button
          onClick={() => {
            this.visible1 = true;
          }}
        >
          显示弹框1
        </Button>
        <Button
          onClick={() => {
            this.visible2 = true;
          }}
        >
          显示弹框2
        </Button>
        <Button
          onClick={() => {
            this.visible2 = true;
          }}
        >
          显示弹框3
        </Button>
        <MyModal1
          visible={visible1}
          onClose={() => {
            this.visible1 = false;
          }}
        />
        <MyModal2
          visible={visible2}
          onClose={() => {
            this.visible2 = false;
          }}
        />
        <MyModal3
          visible={visible3}
          onClose={() => {
            this.visible3 = false;
          }}
        />
      </>
    );
  },
});

多个 Modal 框的场景下, 这种方式显然看起来不够优雅. 代码啰嗦不够精简.

大家平时应该都是这么开发的, 问题是有没有更好的方式呢?

思路

使用声明式组件开发模式肯定会遇到这种情况. 但是如果我们能通过函数式的方式调用组件, 可能是个不错的选择. 伪代码如下:

export default defineComponent({
  render() {
    return (
      <>
        <Button
          onClick={() => {
            MyModal1({}).then((data) => {});
          }}
        >
          显示弹框1
        </Button>
        <Button
          onClick={() => {
            MyModal2({}).then((data) => {});
          }}
        >
          显示弹框2
        </Button>
        <Button
          onClick={() => {
            MyModal3({}).then((data) => {});
          }}
        >
          显示弹框3
        </Button>
      </>
    );
  },
});

这种方式, 至少逻辑清晰, 可读性更强. 也是我比较推荐和喜欢的一种方式, 之前用 react 的时候也封装过相应的基础组件. 现在切换到 vue3 技术栈, 尝试着再实现一遍.

如何实现

先贴代码, 基础包裹组件的实现(逻辑看代码内的注释):

import { createVNode, render, VNodeTypes } from "vue";

export async function DynamicModal(component: VNodeTypes, props: object) {
  return new Promise((resolve) => {
    1️⃣ // 创建父级包裹对象
    const container = document.createElement("div");
    2️⃣ // 创建vue Node实例, 并传递props(强制传递onClose方法)
    const vm = createVNode(component, {
      ...props,
      onClose: (params: unknown) => {
        5️⃣ // 子组件销毁时调用onClose方法, 移除响应的DOM节点
        document.body.removeChild(
          document.querySelector(".ant-modal-root")?.parentNode as Node
        );
        document.body.removeChild(container);
        6️⃣ // 返回子组件传递的数据
        resolve(params);
      },
    });
    3️⃣ // 把vue Node实例渲染到包裹对象中
    render(vm, container);

    4️⃣ // 将包裹对象插入到body中
    document.body.appendChild(container);
  });
}

如何使用:

// CellModal.tsx
// 定义组件
const CellModal = defineComponent({
  props: {
    onClose: {
      type: Function as PropType<(param: unknown) => void>,
      required: true,
    },
    props1: {
      type: String as PropType<string>,
      required: true,
    },
    props2: {
      type: Object as PropType<any>,
      required: true,
    },
  },
  setup(props) {
    const visible = ref(true);
    const close = () => {
      visible.value = false;
      props.onClose({ text: "这是子组件的数据" });
    };
    return () => (
      <Modal
        title="标题"
        visible={visible.value}
        onCancel={close}
        onOk={() => {
          message.success("操作成功");
          close();
        }}
      >
        // 内容
      </Modal>
    );
  },
});

// 导出包装过的组件
export async function DynamicCellModal(props) {
  return DynamicModal(CellModal, props);
}

// Edit.tsx
// 使用组件
import { DynamicCellModal } from "./CellModal";

DynamicCellModal({ props1: "", props2: {} }).then((data) => {
  // data = { text: '这是子组件的数据' }
});

这个时候有人注意到, 调用 DynamicCellModal 时, props 没有类型检查. 好的, 安排

此时 CellModal 的 props 包含三个属性, 分别是: onClose, props1, props2; 由于 onClose 是被我们动态强制注入的, 所以不需要调用方传入. 也就是 除了 onClose 之外的所有 props 都需要得到提示

这就好办了, 首先我们要先获取组件 CellModal 的 props 类型, 然后排除一下 onClose 即可

  • 先声明类型:
1️⃣ // 获取组件的props类型
type GetPropType<T extends new (...args: any) => void> = {
  [P in keyof InstanceType<T>["$props"]]: InstanceType<T>["$props"][P];
};

2️⃣ // 删除onClose类型
type DynamicPropType<T extends new (...args: any) => void> = Omit<
  GetPropType<T>,
  "onClose"
>;
  • 使用类型(修改 CellModal.tsx 文件)
// CellModal.tsx

export async function DynamicCellModal(
  props: DynamicPropType<typeof CellModal>
) {
  return DynamicModal(CellModal, props);
}

到此, 整个改造完成.

补充一个场景
export const SuccessModal = defineComponent({
  setup() {
    2️⃣ // 这个场景需要定义两个变量
    const success = ref({ msg: '' })
    const visible = ref(false)

    return () => (
      <div>
        <Button
          onClick={() => {
            1️⃣ // 提交表单, 接口返回数据后, 显示modal展示内容
            submit().then((res: { msg: string }) => {
              visible.value = true
              success.value.msg = res.msg
            })
          }}
        >
          <div class='text'>提交</div>
        </Button>
        <Modal show={visible.value}>
          <div>{success.value.msg}</div>
        </Modal>
      </div>
    )
  }
})

如果使用我们的优化方式, 代码可以变成如下这样:

export const DynamicSuccessModal = defineComponent({
  setup() {
    return () => (
      <Button
        onClick={() => {
          submit().then((res: { msg: string }) => {
            DynamicModal({ msg: res.msg });
          });
        }}
      >
        <div class="text">提交</div>
      </Button>
    );
  },
});

大家有什么好的方式, 欢迎探讨!