Vue 3 中的 Teleport(传送门)是一个非常有用的特性,它允许你在 DOM 树中的任意位置动态地渲染组件。Teleport 实质上是将组件的内容“传送”到指定的目标位置,而不受组件自身所在位置的限制。
Teleport 的使用场景包括但不限于:
-
模态框(Modal):你可以使用 Teleport 将模态框的内容渲染到
<body>
标签之外,以避免模态框受到父级容器 CSS 样式的影响,并且可以保证在层叠顺序上处于最顶层。 -
弹出菜单(Dropdown Menu):通过 Teleport,你可以将弹出菜单的内容渲染到
<body>
标签之外,以确保在 z-index 上的正确显示,并且避免被父级容器的 overflow 属性限制。 -
全局提示(Toasts/Notifications):你可以使用 Teleport 将全局提示的内容渲染到页面的特定位置,比如右上角,而不受组件所在层级的影响。
-
滚动组件(Scrollable Components):有时候你可能希望某个组件的内容在滚动时保持固定位置,这时可以使用 Teleport 将该组件的内容渲染到页面的某个固定位置,而不受滚动容器的影响。
-
特定容器之外的渲染:有些情况下,你可能需要将组件的内容渲染到与当前组件不相邻的 DOM 结构中,这时可以使用 Teleport 轻松实现。
有时我们可能会遇到这样的场景:一个组件模板的一部分在逻辑上从属于该组件,但从整个应用视图的角度来看,它在 DOM 中应该被渲染在整个 Vue 应用外部的其他地方。
这类场景最常见的例子就是全屏的模态框。理想情况下,我们希望触发模态框的按钮和模态框本身是在同一个组件中,因为它们都与组件的开关状态有关。但这意味着该模态框将与按钮一起渲染在应用 DOM 结构里很深的地方。这会导致该模态框的 CSS 布局代码很难写。
试想下面这样的 HTML 结构:
<div class="outer">
<h3>Tooltips with Vue 3 Teleport</h3>
<div>
<MyModal />
</div>
</div>
接下来我们来看看 <MyModal>
的实现:
<script setup>
import { ref } from 'vue'
const open = ref(false)
</script>
<template>
<button @click="open = true">Open Modal</button>
<div v-if="open" class="modal">
<p>Hello from the modal!</p>
<button @click="open = false">Close</button>
</div>
</template>
<style scoped>
.modal {
position: fixed;
z-index: 999;
top: 20%;
left: 50%;
width: 300px;
margin-left: -150px;
}
</style>
这个组件中有一个 <button>
按钮来触发打开模态框,和一个 class 名为 .modal
的 <div>
,它包含了模态框的内容和一个用来关闭的按钮。
当在初始 HTML 结构中使用这个组件时,会有一些潜在的问题:
-
position: fixed
能够相对于浏览器窗口放置有一个条件,那就是不能有任何祖先元素设置了transform
、perspective
或者filter
样式属性。也就是说如果我们想要用 CSStransform
为祖先节点<div class="outer">
设置动画,就会不小心破坏模态框的布局! -
这个模态框的
z-index
受限于它的容器元素。如果有其他元素与<div class="outer">
重叠并有更高的z-index
,则它会覆盖住我们的模态框。
<Teleport>
提供了一个更简单的方式来解决此类问题,让我们不需要再顾虑 DOM 结构的问题。让我们用 <Teleport>
改写一下 <MyModal>
:
<button @click="open = true">Open Modal</button>
<Teleport to="body">
<div v-if="open" class="modal">
<p>Hello from the modal!</p>
<button @click="open = false">Close</button>
</div>
</Teleport>
<Teleport>
接收一个 to
prop 来指定传送的目标。to
的值可以是一个 CSS 选择器字符串,也可以是一个 DOM 元素对象。这段代码的作用就是告诉 Vue“把以下模板片段传送到 body
标签下”。
你可以点击按钮,然后通过浏览器的开发者工具,在 <body>
标签下找到模态框元素:
我们也可以将 <Teleport>
和 <Transition> 结合使用来创建一个带动画的模态框
modal.vue
<script setup>
const props = defineProps({
show: Boolean
})
</script>
<template>
<Transition name="modal">
<div v-if="show" class="modal-mask">
<div class="modal-container">
<div class="modal-header">
<slot name="header">default header</slot>
</div>
<div class="modal-body">
<slot name="body">default body</slot>
</div>
<div class="modal-footer">
<slot name="footer">
default footer
<button
class="modal-default-button"
@click="$emit('close')"
>OK</button>
</slot>
</div>
</div>
</div>
</Transition>
</template>
<style>
.modal-mask {
position: fixed;
z-index: 9998;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
transition: opacity 0.3s ease;
}
.modal-container {
width: 300px;
margin: auto;
padding: 20px 30px;
background-color: #fff;
border-radius: 2px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.33);
transition: all 0.3s ease;
}
.modal-header h3 {
margin-top: 0;
color: #42b983;
}
.modal-body {
margin: 20px 0;
}
.modal-default-button {
float: right;
}
/*
* 对于 transition="modal" 的元素来说
* 当通过 Vue.js 切换它们的可见性时
* 以下样式会被自动应用。
*
* 你可以简单地通过编辑这些样式
* 来体验该模态框的过渡效果。
*/
.modal-enter-from {
opacity: 0;
}
.modal-leave-to {
opacity: 0;
}
.modal-enter-from .modal-container,
.modal-leave-to .modal-container {
-webkit-transform: scale(1.1);
transform: scale(1.1);
}
</style>
app.vue
<!--
可定制插槽和 CSS 过渡效果的模态框组件。
-->
<script setup>
import Modal from './Modal.vue'
import { ref } from 'vue'
const showModal = ref(false)
</script>
<template>
<button id="show-modal" @click="showModal = true">Show Modal</button>
<Teleport to="body">
<!-- 使用这个 modal 组件,传入 prop -->
<modal :show="showModal" @close="showModal = false">
<template #header>
<h3>custom header</h3>
</template>
</modal>
</Teleport>
</template>