Vue 3 的 <Teleport>(传送门)是一个内置组件,它允许你将组件模板的一部分 HTML 结构传送 到当前组件 DOM 层次结构之外的某个 DOM 节点中,同时保持组件内部的逻辑状态(如 props、data、事件等)依然与父组件相连。
简单来说:逻辑上它在组件里,物理上(DOM)它在别处。
1.核心概念与语法
<Teleport> 不需要引入,直接在模板中使用即可。
-
to(必填): 指定传送的目标。可以是 CSS 选择器字符串(如"body","#id",".class")或实际的 DOM 元素。 -
disabled(可选):boolean类型。如果为true,内容将保留在原来的位置,不会被传送。
HTML
<Teleport to="选择器">
</Teleport>
2.使用场景
通常用于处理那些在视觉上需要"跳出"父容器限制的 UI 元素。
场景一:模态框 (Modal/Dialog)
这是最经典的使用场景。如果模态框嵌套在深层组件中,父级组件如果有 overflow: hidden 或 z-index 设置,模态框可能会被遮挡或无法全屏覆盖。使用 Teleport 将其直接挂载到 body 下,可以完美解决定位层级问题。
场景二:全局通知 (Toast/Notification)
消息提示通常需要浮在页面最顶层,无论在哪个组件触发,都应该渲染在页面的固定位置(如右上角)。
场景三:复杂的下拉菜单或 Tooltip
当 Tooltip 被放置在带有滚动条的容器内时,如果它太长,可能会被容器裁剪。将其传送至 body 可以避免裁剪问题。
举个栗子:模态框
子组件:TeleModal.vue
ts
<template>
<Teleport to="body">
<Transition name="fade">
<div v-if="isOpen" class="modal-overlay" @click="close">
<div class="modal-content" @click.stop>
<header>
<h3>{{ title }}</h3>
</header>
<main>
<slot></slot>
</main>
<footer>
<button @click="close">关闭</button>
<button @click="confirm" class="primary">确认</button>
</footer>
</div>
</div>
</Transition>
</Teleport>
</template>
<script setup lang="ts">
// 定义 Props 类型
interface Props {
isOpen: boolean;
title?: string;
}
// 定义 Emits 类型
interface Emits {
// update:属性名 这种命名格式,就是 Vue 3 专门为了配合 v-model(双向绑定) 而设计的一套强制约定。
(e: "update:isOpen", value: boolean): void;
(e: "confirm"): void;
}
// 使用宏定义 Props 和 Emits (TS 语法)
const props = withDefaults(defineProps<Props>(), {
title: "提示",
});
const emit = defineEmits<Emits>();
// 定义方法触发emit,关闭模态框逻辑
const close = () => {
emit("update:isOpen", false);
};
const confirm = () => {
emit("confirm");
close();
};
</script>
<style scoped>
/* 简单的遮罩层样式 */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 9999; /* 确保在最上层 */
}
.modal-content {
background: white;
padding: 20px;
border-radius: 8px;
min-width: 300px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.primary {
background-color: #42b983;
color: white;
margin-left: 10px;
}
/* Transition 动画 */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>
父组件:注意:虽然 DOM 被移动到了 body,但在 Vue 的组件树逻辑中,
TeleModal依然是父组件的子组件,数据流(Props/Events)完全正常工作。
ts
<template>
<div class="parent-container">
<h1>父组件页面</h1>
<p>即使这里的父级设置了 overflow: hidden,Modal 依然会全屏显示。</p>
<button @click="showModal = true">打开模态框</button>
<TeleModal
v-model:isOpen="showModal"
title="用户协议"
@confirm="handleConfirm"
>
<p>这里是模态框的具体内容...</p>
<p>这是一个 TypeScript 编写的 Teleport 示例。</p>
</TeleModal>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import TeleModal from './TeleModal.vue';
const showModal = ref<boolean>(false);
const handleConfirm = () => {
console.log('用户点击了确认');
};
</script>
<style scoped>
.parent-container {
/* 模拟一个受限的布局环境 */
transform: translateZ(0);
overflow: hidden;
height: 200px;
border: 1px solid #ccc;
padding: 20px;
}
</style>
3.注意事项
- 有条件地传送 (
disabled属性),有时候我们希望在特定条件下传送。
ts
<Teleport to="body" :disabled="isMobile">
<div class="menu">...</div>
</Teleport>
- 多个 Teleport 指向同一个目标,多个
<Teleport>组件可以将内容发送给同一个目标元素(例如都传送到body)。它们会按照挂载顺序依次追加(append),而不会相互覆盖。 - 目标元素必须存在,
to属性所指向的 DOM 元素,必须在组件挂载之前就已经存在于页面中。例如body,或者在index.html中预先写好的DOM元素。
4.总结
| 特性 | 说明 |
|---|---|
| 逻辑层级 | 保持不变。组件仍然能够访问父组件的 provide/inject,触发 emits,接收 props。 |
| DOM层级 | 发生改变。实际 HTML 节点被移动到了 to 指定的位置。 |
| 样式影响 | scoped 样式依然有效,但要注意 CSS 选择器的继承关系(因为父级 DOM 变了,可能导致依赖父级类名的全局 CSS 失效)。 |