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>

disabledtrue 时,内容会留在原处,不会传送。

与 Suspense 的配合

在异步组件加载时,Teleport 可以配合 <Suspense> 将加载中状态传送到页面的特定位置,形成全局 loading 效果。

<Suspense>
  <template #default>
    <AsyncComponent />
  </template>
  <template #fallback>
    <Teleport to="#loading-container">
      <div class="global-loading">加载中...</div>
    </Teleport>
  </template>
</Suspense>