基于vue3.0弹窗组件的封装

1,744 阅读3分钟

wallhaven-x11wpz.jpg

本文已参与「新人创作礼」活动,一起开启掘金创作之路。。。

奇思妙想

在之前的模式中,弹窗的封装是基于数据副本渲染,那些弹窗 body 是写在内部的。如果说,弹窗 body 是用户定义的,应该怎么做呢,如果要做一个仅仅是弹窗的框架,内容由用户传进去渲染,这份内容是组件,而不再是基于数据渲染视图。于是基于这个想法,做了一个新的弹窗,这是使用用户自定义动态组件的模式。

introduce.png

createVNode

在参考 el-dialog 的写法后,业务需求使用视图的写法,有点局限性,由于调用方可能会在逻辑文件 js 中,故改用指令式调用,这时候使用 createVNode 去进行 render,是比较符合当前的场景,这样,我就可以在 js 文件中调用了。

1.弹窗核心逻辑 ver1.0:

// createDynamicDialog.ts
import { VNode, createVNode, render, VNodeTypes, App } from "vue";
import DynamicDialog from "./DynamicDialog.vue";

/**
 * 动态创建弹窗组件 通过createVNode
 * @returns {VNode}
 */
const createDynamicDialog = (app: App) => (
  params: Record<string, any>,
  child: VNodeTypes
): Promise<VNode | null> => {
  const container = document.createElement("div");

  // 实例销毁方法
  const destroy = () => {
    render(null, container);
    document.body.removeChild(container);
  };

  /**
   * 创建VNode 第三个参数是children
   * 再利用createVNode创建children
   * 组件内使用slot插槽渲染children的内容
   * 这里默认使用default 若有更多插槽 可以继续添加其他项
   */
  const vnode = createVNode(
    DynamicDialog,
    {
      params,
      destroy
    },
    // 创建动态子组件 通过createVNode
    {
      default: createVNode(child, {
        params
      })
    }
  );

  // 存储上下文对象 须在render之前 不然组件内获取须在mounted里
  vnode.appContext = app._context;
  render(vnode, container);
  document.body.appendChild(container);
};
export default createDynamicDialog;

2.下面是框架容器盒子,通过 slot 渲染插入的组件

<!-- DynamicDialog.vue -->
<template>
  <div :class="$style.DynamicDialog">
    <!-- 头部 -->
    <header :class="$style.header">
      <h3>{{ params.title }}</h3>
      <i class="el-icon-circle-close" @click="destroy" />
    </header>

    <!-- 动态组件内容 -->
    <slot />
  </div>
</template>

3.使用

import PillarChart from "./components/PillarChart/PillarChart.vue";
// 这里就忽略 入口文件 全局注册弹窗了 这里是获取全局打开弹窗指令
const { $openDynamicDialog } = useGlobal();
// 通过第二个参数为child 这里是作为组件进行传参 body的内容就是用户定义的组件内容
$openDynamicDialog({ title: "经济态势数据分析" }, PillarChart);

这样我们就实现了用户传送组件到我们的弹窗 body 中了,这有点类似于 teleport 运输组件

弹窗 ver2.0

由于每次我们点击需要打开弹窗时,如果多次点击,那么他会多次插入到 body 中,也就是说会有多个弹窗出来,这明显不符合交互设计,所以我们需要将他改写成,每次点击只出最新的那一个。最简单粗暴就是打开之前,就将上一个给关闭掉:

let uid = 0;

const createDynamicDialog = (app: App) => (
  params: Record<string, any>,
  child: VNodeTypes
): VNode | null => {
  // 生成dom id
  const generateDomId = (id: number) => `dynamic-dialog-container${id}`;

  // 上一个dom
  const prevContainer = document.getElementById(generateDomId(uid));

  if (prevContainer) {
    render(null, prevContainer);
    document.body.removeChild(prevContainer);
  }

  uid++;

  const container = document.createElement("div");
  container.id = generateDomId(uid);

  // 实例销毁方法
  const destroy = () => {
    render(null, container);
    document.body.removeChild(container);
  };

  const vnode = createVNode(
    DynamicDialog,
    {
      params,
      destroy
    },
    // 创建动态子组件 通过createVNode
    {
      default: createVNode(child, {
        params
      })
    }
  );

  // 存储上下文对象 须在render之前 不然组件内获取须在mounted里
  vnode.appContext = app._context;
  render(vnode, container);

  document.body.appendChild(container);

  return vnode;
};

通过全局声明一个 uid,作为每个弹窗的 id 标识,每次打开之前,获取上一个 id 的弹窗,并且关掉,这样我们就实现了弹窗交互的单一开窗处理

弹窗 ver3.0

上面的 2.0 版本,确实帮我们解决了单一开窗,但是如果我们在点击相同的类型弹窗,他也会帮我们关闭上一个,再打开最新的那个,这样显然是多此一举,基于此,我们进行一个优化,弹窗 ver3.0:

import { VNode, createVNode, render, VNodeTypes, App } from "vue";
import DynamicDialog from "./DynamicDialog.vue";

let uid = 0;
let prevPromise: any;

const createDynamicDialog = (app: App) => (
  params: Record<string, any>,
  child: VNodeTypes
): Promise<VNode | null> =>
  /**
   * 目的:解决点击相同类型弹窗不在开窗
   * 实现:利用promise 做发布订阅 弹窗调用方式().then 就是destroy 执行的时机 方便穿插callback
   */
  new Promise(resolve => {
    // 生成dom id
    const generateDomId = (id: number) => `dynamic-dialog-container${id}`;

    // 上一个dom
    const prevContainer = document.getElementById(generateDomId(uid));

    if (prevContainer) {
      render(null, prevContainer);
      document.body.removeChild(prevContainer);
      // 上一个的发布destroy 外层执行callback 注意这里要用prevPromise
      prevPromise(null);
    }
    // 上一个调用者的订阅
    prevPromise = resolve;
    uid++;

    const container = document.createElement("div");
    container.id = generateDomId(uid);

    // 实例销毁方法
    const destroy = () => {
      render(null, container);
      document.body.removeChild(container);
      // 发布destroy 外层执行callback
      resolve(null);
    };

    const vnode = createVNode(
      DynamicDialog,
      {
        params,
        destroy
      },
      // 创建动态子组件 通过createVNode
      {
        default: createVNode(child, {
          params
        })
      }
    );

    // 存储上下文对象 须在render之前 不然组件内获取须在mounted里
    vnode.appContext = app._context;
    render(vnode, container);

    document.body.appendChild(container);
  });
export default createDynamicDialog;

下面是使用:

// 调用者
let isOpen = false;
const handlerClick = () => {
  if (isOpen) return;
  $openDynamicDialog({ title: "经济态势数据分析" }, PillarChart).then(() => {
    isOpen = false;
  });
  isOpen = true;
};

我们使用 promise 做发布订阅,这样调用者使用.then 的回调,就是关闭弹窗后的执行时机,将是否需要打开弹窗的逻辑交给调用者,调用者只需要设置一个 flag 就可以实现,同时在 close 时,初始回去 flag 的状态,否则就会出现问题,为什么将 flag 的设置交给调用方,之前的弹窗,我将这个设置是设置在了内部里,但是发现随着需求的增加,这里面的逻辑就会越发复杂,为了不污染内部,我将 close 的执行时机抛出去,将控制权交给调用者,你们想要在 close 做什么事情,你们自己决定,createDynamicDialog 只做他应该做的事就好。