Vue3 Teleport
平时组件模板里写的东西,最终会挂到它所在的位置,但弹窗、菜单、提示这一类的元素常有问题,它们会被父级的 overflow: hidden、transform、z-index 卡住,该冒出来的冒不出来。
Teleport(传送到) 能把一段模板”传送到”页面上任意一个位置去挂载,而这段模板对应的组件逻辑还留在原组件里,弹窗、浮层几乎都要靠它,是现代组件不可或缺的一招。
为什么需要它
先看问题,弹窗写在组件里的某个角落:
<div class="card" style="overflow: hidden">
<button @click="show = true">打开弹窗</button>
<!-- 弹窗实际会跟着 card 一起被裁掉 -->
<div v-if="show" class="modal">...</div>
</div>
overflow: hidden 一设置,弹窗超出容器部分就被切掉了,浏览器顶部 position: fixed 也救不回来。
归根结底一句话,元素渲染在哪,就受哪的层层约束,想摆脱约束,就得让它渲染到别处,这就是 Teleport 的用武之地。
基本用法:<Teleport to="目标">
to 填一个 CSS 选择器,Vue 会把里面的内容挂到那个元素上,最常用的是 body:
<template>
<Teleport to="body">
<div class="modal">
<p>弹窗内容</p>
<button @click="close">关闭</button>
</div>
</Teleport>
</template>
<script setup>
const show = ref(false)
</script>
使用前后对比:
- 不使用 Teleport,
.modal渲染在组件当前位置,受父容器约束。 - 使用后,
.modal的 DOM 直接进body,页面最外层,不再被任何容器裁剪。
to 也可以指向其他选择器,比如 to="#app"、to=".container",只要是页面上存在的元素即可。
目标元素找不到会怎样?
会退回在原位置渲染,同时在控制台给一条警告,所以别把 to 写成不存在的类名,弹窗位置会和预期不符。
逻辑归属不变
DOM 被传送走了,但组件逻辑没有跟着走,props、emits、作用域插槽这些依然属于写 Teleport 的组件,使用体验和普通组件一致:
- 父组件传的 props 照常在
<Teleport>里生效。 - 模板里的
v-if、v-for照常判断。 scoped样式也照常生效,.modal的类名还能被子级穿透选择器命中。
原生事件冒泡会变吗
会。DOM 结构变了,原生事件按新的 DOM 树冒泡,而不是回到子组件里,如果监听了 document 上的点击来”点空白关闭弹窗”,要留意事件源的判断。
条件使用:disabled
disabled 为真时关闭传送到,内容退回原位渲染,适合小屏设备把弹层降级成页内模块:
<Teleport :disabled="isMobile">
<div class="drawer">...</div>
</Teleport>
组合套路:Teleport + Transition
Teleport 管挂哪,Transition(Vue3 过渡动画)管出现和离开的动画,两者互不干扰,正好叠着用。弹窗淡入淡出再加”挂到 body”是最常见的写法:
<Teleport to="body">
<Transition>
<!-- 淡入淡出,挂载在 body 下 -->
<div v-if="show" class="modal">...</div>
</Transition>
</Teleport>
挂到 body 后,弹窗和页面其他元素处于同一层,这时层级冲突就要靠 z-index 和遮罩层解决,别忘记加一个半透明背景用于点击关闭。
速查卡片
| 场景 | 写法 |
|---|---|
| 弹窗浮层挂外层 | <Teleport to="body"> |
| 挂到其他元素 | to="#app" 或任意选择器 |
| 临时退回原位 | :disabled="条件" |
| 淡入淡出弹窗 | <Teleport> 里包 <Transition> |
| 目标不存在 | 退回原位置并给警告,稳步排查即可 |