Skip to content

一个模态框的完整封装

​ 通过函数调用,即可渲染一个模态框组件。模态框里面渲染的内容由传入的参数决定的,传入什么组件就渲染什么内容。

​ 传入的实参组件内部想要关闭模态框只需要使用emit('close')分发自定义事件即可。

简单版本

ts
import { h, render, renderSlot, ref, computed } from "vue";
import type { Component } from "vue";
import "./index.css";

/**
 * 渲染模态框
 * @param content 模态框内容组件
 * @param props 模态框内容组件的props
 */
export function renderModal(content: Component, props: Record<any, any>) {
  // 容器
  const mask = document.createElement("div");
  mask.classList.add("modal-mask-container");
  // 模态框的class
  const classList = ref(["modal-container", "modal-enter"]);
  const className = computed(() => classList.value.join(" "));

  // 模态框虚拟DOM
  const container = h(
    // 定义组件
    {
      name: "ModalContainer",
      setup(_, context) {
        return () =>
          h(
            "div",
            {
              onClick(e: Event) {
                // 停止事件发生
                e.stopPropagation();
              },
              class: className.value,
            },
            [renderSlot(context.slots, "default")]
          );
      },
    },
    null,
    // 使用组件
    {
      default: () =>
        h(content, {
          ...props,
          onClose: handleUnload,
        }),
    }
  );

  // 卸载容器
  const handleUnload = () => {
    classList.value.push("modal-leave");
    setTimeout(() => {
      // 移除整个模态框
      mask.remove();
    }, 300);
  };

  // 点击遮罩层关闭容器
  mask.onclick = handleUnload;

  // 渲染
  render(container, mask);

  // 去除入场动画
  setTimeout(() => {
    classList.value.pop();
  }, 300);

  // 挂载
  document.body.appendChild(mask);
}
import { h, render, renderSlot, ref, computed } from "vue";
import type { Component } from "vue";
import "./index.css";

/**
 * 渲染模态框
 * @param content 模态框内容组件
 * @param props 模态框内容组件的props
 */
export function renderModal(content: Component, props: Record<any, any>) {
  // 容器
  const mask = document.createElement("div");
  mask.classList.add("modal-mask-container");
  // 模态框的class
  const classList = ref(["modal-container", "modal-enter"]);
  const className = computed(() => classList.value.join(" "));

  // 模态框虚拟DOM
  const container = h(
    // 定义组件
    {
      name: "ModalContainer",
      setup(_, context) {
        return () =>
          h(
            "div",
            {
              onClick(e: Event) {
                // 停止事件发生
                e.stopPropagation();
              },
              class: className.value,
            },
            [renderSlot(context.slots, "default")]
          );
      },
    },
    null,
    // 使用组件
    {
      default: () =>
        h(content, {
          ...props,
          onClose: handleUnload,
        }),
    }
  );

  // 卸载容器
  const handleUnload = () => {
    classList.value.push("modal-leave");
    setTimeout(() => {
      // 移除整个模态框
      mask.remove();
    }, 300);
  };

  // 点击遮罩层关闭容器
  mask.onclick = handleUnload;

  // 渲染
  render(container, mask);

  // 去除入场动画
  setTimeout(() => {
    classList.value.pop();
  }, 300);

  // 挂载
  document.body.appendChild(mask);
}

复杂版本

ts
import { h, render, renderSlot, ref, computed } from "vue";
import type { Component } from "vue";
import "./index.css";

/**
 * 渲染模态框
 * @param content 模态框内容组件
 * @param props 模态框内容组件的props
 * @param offset 动画开始时模态框的偏移量
 */
export function renderModal(
  content: Component,
  props: Record<any, any>,
  offset?: {
    x: number;
    y: number;
  }
) {
  // 容器
  let mask = document.createElement("div");
  mask.classList.add("modal-mask-container");
  // 模态框的class
  const classList = ref(["modal-container"]);
  const className = computed(() => classList.value.join(" "));
  // 设置动画偏移量
  let ob = new ResizeObserver(([entry]) => {
    if (offset !== undefined) {
      // 若传入了模态框起始偏移量
      mask.style.setProperty("--top", `${offset.y}px`);
      mask.style.setProperty("--left", `${offset.x}px`);
      mask.style.setProperty("--width", `${entry.contentRect.width}px`);
      mask.style.setProperty("--height", `${entry.contentRect.height}px`);
      classList.value.push("modal-enter-offset");
    } else {
      // 无偏移量,居中进场、离场
      classList.value.push("modal-enter");
    }
    // 断开监听
    ob.disconnect();
  });
  // 模态框虚拟DOM
  let container = h(
    // 定义组件
    {
      name: "ModalContainer",
      setup(_, context) {
        return () =>
          h(
            "div",
            {
              onClick(e: Event) {
                // 停止事件发生
                e.stopPropagation();
              },
              class: className.value,
            },
            [renderSlot(context.slots, "default")]
          );
      },
    },
    null,
    // 使用组件
    {
      default: () =>
        h(content, {
          ...props,
          onClose: handleUnload,
        }),
    }
  );

  // 卸载容器
  const handleUnload = () => {
    offset
      ? classList.value.push("modal-leave-offset")
      : classList.value.push("modal-leave");
    setTimeout(() => {
      // 移除整个模态框
      mask && mask.remove();
      // 释放内存
      // @ts-ignore
      mask = null;
      // @ts-ignore
      container = null;
      // @ts-ignore
      ob = null;
    }, 280);
  };

  // 点击遮罩层关闭容器
  mask.onclick = handleUnload;

  // 渲染
  render(container, mask);
  // 渲染后才能监听
  ob.observe(container.el as HTMLElement);

  // 去除入场动画
  setTimeout(() => {
    if (
      mask &&
      (classList.value.includes("modal-enter") ||
        classList.value.includes("modal-enter-offset"))
    )
      classList.value.pop();
  }, 300);

  // 挂载
  document.body.appendChild(mask);
}
import { h, render, renderSlot, ref, computed } from "vue";
import type { Component } from "vue";
import "./index.css";

/**
 * 渲染模态框
 * @param content 模态框内容组件
 * @param props 模态框内容组件的props
 * @param offset 动画开始时模态框的偏移量
 */
export function renderModal(
  content: Component,
  props: Record<any, any>,
  offset?: {
    x: number;
    y: number;
  }
) {
  // 容器
  let mask = document.createElement("div");
  mask.classList.add("modal-mask-container");
  // 模态框的class
  const classList = ref(["modal-container"]);
  const className = computed(() => classList.value.join(" "));
  // 设置动画偏移量
  let ob = new ResizeObserver(([entry]) => {
    if (offset !== undefined) {
      // 若传入了模态框起始偏移量
      mask.style.setProperty("--top", `${offset.y}px`);
      mask.style.setProperty("--left", `${offset.x}px`);
      mask.style.setProperty("--width", `${entry.contentRect.width}px`);
      mask.style.setProperty("--height", `${entry.contentRect.height}px`);
      classList.value.push("modal-enter-offset");
    } else {
      // 无偏移量,居中进场、离场
      classList.value.push("modal-enter");
    }
    // 断开监听
    ob.disconnect();
  });
  // 模态框虚拟DOM
  let container = h(
    // 定义组件
    {
      name: "ModalContainer",
      setup(_, context) {
        return () =>
          h(
            "div",
            {
              onClick(e: Event) {
                // 停止事件发生
                e.stopPropagation();
              },
              class: className.value,
            },
            [renderSlot(context.slots, "default")]
          );
      },
    },
    null,
    // 使用组件
    {
      default: () =>
        h(content, {
          ...props,
          onClose: handleUnload,
        }),
    }
  );

  // 卸载容器
  const handleUnload = () => {
    offset
      ? classList.value.push("modal-leave-offset")
      : classList.value.push("modal-leave");
    setTimeout(() => {
      // 移除整个模态框
      mask && mask.remove();
      // 释放内存
      // @ts-ignore
      mask = null;
      // @ts-ignore
      container = null;
      // @ts-ignore
      ob = null;
    }, 280);
  };

  // 点击遮罩层关闭容器
  mask.onclick = handleUnload;

  // 渲染
  render(container, mask);
  // 渲染后才能监听
  ob.observe(container.el as HTMLElement);

  // 去除入场动画
  setTimeout(() => {
    if (
      mask &&
      (classList.value.includes("modal-enter") ||
        classList.value.includes("modal-enter-offset"))
    )
      classList.value.pop();
  }, 300);

  // 挂载
  document.body.appendChild(mask);
}
css
.modal-mask-container {
  position: fixed;
  inset: 0;
  z-index: 2025;
  background-color: var(--mask-color);
  animation: fade var(--time-fast) 1 ease;
  color: var(--text-color-1);
  display: flex;
  align-items: center;
  justify-content: center;
}

.modal-leave-offset {
  animation: modalOffset var(--time-normal) 1 ease reverse;
}

.modal-enter-offset {
  animation: modalOffset var(--time-normal) 1 ease;
}

@keyframes modalOffset {
  from {
    position: absolute;
    /*故意拉的偏移量,最好是获取模态框大小/2*/
    top: calc(var(--top) - calc(var(--height) / 2));
    left: calc(var(--left) - calc(var(--width) / 2));
    transform: scale(0.3);
    opacity: 0.3;
  }
  to {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: scale(1) translate(-50%, -50%);
    opacity: 1;
  }
}

.modal-leave {
  animation: modal var(--time-normal) 1 ease reverse;
}

.modal-enter {
  animation: modal var(--time-normal) 1 ease;
}

@keyframes modal {
  from {
    opacity: 0.3;
    transform: scale(0.3);
  }
}

@keyframes fade {
  from {
    background-color: #00000000;
  }
  to {
    background-color: var(--mask-color);
  }
}
.modal-mask-container {
  position: fixed;
  inset: 0;
  z-index: 2025;
  background-color: var(--mask-color);
  animation: fade var(--time-fast) 1 ease;
  color: var(--text-color-1);
  display: flex;
  align-items: center;
  justify-content: center;
}

.modal-leave-offset {
  animation: modalOffset var(--time-normal) 1 ease reverse;
}

.modal-enter-offset {
  animation: modalOffset var(--time-normal) 1 ease;
}

@keyframes modalOffset {
  from {
    position: absolute;
    /*故意拉的偏移量,最好是获取模态框大小/2*/
    top: calc(var(--top) - calc(var(--height) / 2));
    left: calc(var(--left) - calc(var(--width) / 2));
    transform: scale(0.3);
    opacity: 0.3;
  }
  to {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: scale(1) translate(-50%, -50%);
    opacity: 1;
  }
}

.modal-leave {
  animation: modal var(--time-normal) 1 ease reverse;
}

.modal-enter {
  animation: modal var(--time-normal) 1 ease;
}

@keyframes modal {
  from {
    opacity: 0.3;
    transform: scale(0.3);
  }
}

@keyframes fade {
  from {
    background-color: #00000000;
  }
  to {
    background-color: var(--mask-color);
  }
}