Vue3 实现 Modal 组件
使用 Vue3 设计和实现一个可复用的 Modal 弹窗组件
问题
如何使用 Vue3 实现一个功能完整的 Modal 组件,支持组件引入和 API 调用两种方式?
解答
组件设计思路
Modal 组件需要支持不同场景的复用,比如新增和编辑弹窗,只需通过传入不同参数来显示不同内容,避免重复开发。
核心功能包括:遮罩层、标题、主体内容、确定和取消按钮。主体内容需要灵活支持字符串、HTML 或自定义组件。
目录结构
├── plugins
│ └── modal
│ ├── Content.tsx // 维护 Modal 内容,支持 h 函数和 JSX
│ ├── Modal.vue // 基础组件
│ ├── config.ts // 全局默认配置
│ ├── index.ts // 入口文件
│ ├── locale // 国际化
│ │ ├── index.ts
│ │ └── lang
│ │ ├── en-US.ts
│ │ ├── zh-CN.ts
│ │ └── zh-TW.ts
│ └── modal.type.ts // TypeScript 类型声明
组件实现
Modal.vue 基础结构
<template>
<Teleport to="body" :disabled="!isTeleport">
<div v-if="modelValue" class="modal">
<div class="modal__mask"></div>
<div class="modal__wrapper">
<div class="modal__container">
<div class="modal__header">
<span>{{ title }}</span>
</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>
</div>
</Teleport>
</template>
使用 Teleport 将 Modal 传送到 body,避免 z-index 和定位问题。
主体内容处理
支持三种方式传入内容:
<!-- 1. 默认插槽 -->
<Modal v-model="show" title="演示 slot">
<div>hello world~</div>
</Modal>
<!-- 2. 字符串 -->
<Modal v-model="show" title="演示 content" content="hello world~" />
// 3. h 函数
$modal.show({
title: '演示 h 函数',
content(h) {
return h('div', {
style: 'color:red;',
onClick: ($event) => console.log('clicked', $event.target)
}, 'hello world~');
}
});
// 4. JSX 语法
$modal.show({
title: '演示 JSX',
content() {
return (
<div onClick={($event) => console.log('clicked', $event.target)}>
hello world~
</div>
);
}
});
API 调用方式
Vue3 中使用 createVNode 和 render 实现动态创建组件:
import { createVNode, render } from 'vue';
import Modal from './Modal.vue';
const container = document.createElement('div');
const vnode = createVNode(Modal);
render(vnode, container);
const instance = vnode.component;
document.body.appendChild(container);
挂载到全局属性:
// index.ts
export default {
install(app) {
app.config.globalProperties.$modal = {
show(options) {
// 创建组件实例并显示
}
};
}
};
事件处理
使用 Composition API 处理确定和取消事件:
// Modal.vue
const emit = defineEmits(['update:modelValue', 'confirm', 'cancel']);
const handleConfirm = () => {
emit('confirm');
emit('update:modelValue', false);
};
const handleCancel = () => {
emit('cancel');
emit('update:modelValue', false);
};
API 调用时通过 _hub 属性监听事件:
app.config.globalProperties.$modal = {
show({ onConfirm, onCancel, ...options }) {
const { props, _hub } = instance;
const _closeModal = () => {
props.modelValue = false;
};
if (onConfirm) {
_hub.on('confirm', () => {
onConfirm();
_closeModal();
});
}
if (onCancel) {
_hub.on('cancel', () => {
onCancel();
_closeModal();
});
}
}
};
关键点
- 使用
Teleport将 Modal 挂载到 body,解决层级和定位问题 - 通过
createVNode和render实现 API 调用方式,替代 Vue2 的Vue.extend - 主体内容支持插槽、字符串、h 函数和 JSX 四种方式,提供灵活性
- 使用
app.config.globalProperties挂载全局方法,适配 Vue3 的 setup 语法 - 通过 emit 和事件中心
_hub两种方式处理事件,同时支持组件和 API 调用
目录