Vue3 实现 Modal 组件

使用 Vue3 设计和实现一个可复用的 Modal 弹窗组件

问题

如何使用 Vue3 实现一个功能完整的 Modal 组件,支持模板调用和 API 调用两种方式?

解答

设计思路

Modal 组件需要实现以下功能:

  • 遮罩层、标题、主体内容、确定和取消按钮
  • 主体内容支持字符串、插槽、h 函数和 JSX
  • 支持模板方式和 API 方式调用
  • 使用 Teleport 将组件挂载到 body

目录结构

plugins/modal
├── Modal.vue           # 基础组件
├── Content.tsx         # 内容渲染组件
├── index.ts           # 入口文件
├── config.ts          # 全局配置
├── modal.type.ts      # TypeScript 类型
└── locale             # 国际化
    └── lang

组件实现

Modal.vue 核心代码:

<template>
  <Teleport to="body" :disabled="!isTeleport">
    <div v-if="modelValue" class="modal">
      <div class="mask"></div>
      <div class="modal__wrapper">
        <div class="modal__header">{{ title }}</div>
        <div class="modal__content">
          <Content v-if="typeof content === 'function'" :render="content" />
          <slot v-else>{{ content }}</slot>
        </div>
        <div class="modal__footer">
          <button @click="handleCancel">取消</button>
          <button @click="handleConfirm">确定</button>
        </div>
      </div>
    </div>
  </Teleport>
</template>

<script setup>
import { getCurrentInstance, onBeforeMount } from 'vue';
import Content from './Content';

const props = defineProps({
  modelValue: Boolean,
  title: String,
  content: [String, Function]
});

const emit = defineEmits(['update:modelValue', 'confirm', 'cancel']);

const instance = getCurrentInstance();

onBeforeMount(() => {
  instance._hub = {
    'on-cancel': () => {},
    'on-confirm': () => {}
  };
});

const handleConfirm = () => {
  instance._hub['on-confirm']();
  emit('confirm');
  emit('update:modelValue', false);
};

const handleCancel = () => {
  instance._hub['on-cancel']();
  emit('cancel');
  emit('update:modelValue', false);
};
</script>

模板调用方式

<!-- 使用插槽 -->
<Modal v-model="show" title="演示 slot">
  <div>hello world~</div>
</Modal>

<!-- 使用字符串 -->
<Modal v-model="show" title="演示 content" content="hello world~" />

API 调用方式

index.ts 实现:

import { createVNode, render } from 'vue';
import Modal from './Modal.vue';

const create = (options) => {
  const container = document.createElement('div');
  const vnode = createVNode(Modal, {
    modelValue: true,
    ...options
  });
  
  render(vnode, container);
  document.body.appendChild(container);
  
  const instance = vnode.component;
  const { props, _hub } = instance;
  
  const _closeModal = () => {
    props.modelValue = false;
    container.parentNode.removeChild(container);
  };
  
  Object.assign(_hub, {
    'on-confirm': async () => {
      if (options.onConfirm) {
        await options.onConfirm();
      }
      _closeModal();
    },
    'on-cancel': () => {
      if (options.onCancel) {
        options.onCancel();
      }
      _closeModal();
    }
  });
  
  return instance;
};

export default {
  install(app) {
    app.config.globalProperties.$modal = {
      show: create
    };
  }
};

使用 h 函数:

$modal.show({
  title: '演示 h 函数',
  content(h) {
    return h(
      'div',
      {
        style: 'color:red;',
        onClick: ($event) => console.log('clicked', $event.target)
      },
      'hello world~'
    );
  },
  onConfirm: () => console.log('确认'),
  onCancel: () => console.log('取消')
});

使用 JSX:

$modal.show({
  title: '演示 JSX',
  content() {
    return (
      <div onClick={($event) => console.log('clicked', $event.target)}>
        hello world~
      </div>
    );
  }
});

关键点

  • 使用 Teleport 将 Modal 挂载到 body,避免样式层级问题
  • 通过 createVNode 和 render 实现 API 调用方式,替代 Vue2 的 Vue.extend
  • 内容支持多种形式:字符串、插槽、h 函数和 JSX,提高组件灵活性
  • 使用 _hub 对象管理事件回调,统一处理模板调用和 API 调用的事件
  • 通过 app.config.globalProperties 挂载全局方法,适配 Vue3 的 Composition API