Vue 3 Teleport 把弹窗渲染到 body
FreeGuideOnline
最新
2026-07-04
vue <button @click="show = true">打开弹窗
这是一个被传送到 body 的弹窗
<button @click="show = false">关闭
### 关键点解析
1. **`to` 属性**:指定传送目标。可以是 CSS 选择器字符串(如 `"body"`, `"#teleport-target"`),也可以是实际的 DOM 元素对象。`"body"` 会被解析为 `document.body`。
2. **条件渲染 `v-if`**:`<Teleport>` 内部可以正常使用 Vue 指令。当 `show` 为 `true` 时,`.modal` 这个 div 会被挂载到 `<body>` 底部,而不是当前组件的模板位置。
3. **逻辑与样式完全独立**:虽然弹窗的 DOM 被移动了,但它的数据、方法和生命周期仍然属于当前组件。组件销毁时,传送过去的 DOM 也会被自动移除。
## 完整的自定义 Modal 组件示例
更好的实践是将弹窗封装成独立的 `<Modal>` 组件,内部使用 Teleport。
**Modal.vue**
```vue
<template>
<Teleport to="body">
<div v-if="isOpen" class="modal-overlay" @click.self="$emit('close')">
<div class="modal-content" role="dialog" aria-modal="true">
<slot />
<button class="close-btn" @click="$emit('close')">关闭</button>
</div>
</div>
</Teleport>
</template>
<script setup>
defineProps({
isOpen: Boolean
});
defineEmits(['close']);
</script>
<style scoped>
/* 注意:传送出去的 DOM 仍然会保留 scoped 的 data-v-xxx 属性 */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
}
.modal-content {
background: #fff;
border-radius: 8px;
padding: 2rem;
max-width: 90vw;
max-height: 90vh;
overflow: auto;
}
</style>
使用组件
<template>
<div>
<button @click="showModal = true">删除项目</button>
<Modal :is-open="showModal" @close="showModal = false">
<h2>确认删除</h2>
<p>你确定要删除这条数据吗?</p>
</Modal>
</div>
</template>
<script setup>
import { ref } from 'vue';
import Modal from './Modal.vue';
const showModal = ref(false);
</script>
设计要点
- 点击遮罩关闭:通过
@click.self监听.modal-overlay自身点击,避免内容区点击误关闭。 - 无障碍支持:添加
role="dialog"和aria-modal="true",可结合焦点管理进一步提升可访问性。 - 样式作用域:
scoped样式会自动为传送出去的 DOM 添加唯一的data-v-xxxx属性,因此样式不会泄漏到全局,同时又能正常生效。
使用多个 Teleport 和禁用条件
有时我们希望在特定条件下不使用 Teleport,或者需要渲染到不同的目标。
- 多个传送目标:可以创建多个
<Teleport>指向不同的选择器,例如一个弹窗到 body,一个通知到#notifications容器。 disabled属性:动态控制是否启用传送。例如在移动端某些场景下,你可能希望弹窗留在组件内。
<Teleport to="body" :disabled="isMobile">
<Modal ... />
</Teleport>
当 disabled 为 true 时,内容会留在原处,不会传送。
与 Suspense 的配合
在异步组件加载时,Teleport 可以配合 <Suspense> 将加载中状态传送到页面的特定位置,形成全局 loading 效果。
<Suspense>
<template #default>
<AsyncComponent />
</template>
<template #fallback>
<Teleport to="#loading-container">
<div class="global-loading">加载中...</div>
</Teleport>
</template>
</Suspense>