vue3中Teleport的用法以及使用场景
1. 基本概念
Teleport
是 Vue3 提供的一个内置组件,它可以将组件的内容传送到 DOM 树的任何位置,而不受组件层级的限制。这在处理模态框、通知、弹出菜单等需要突破组件层级限制的场景中特别有用。
1.1 基本语法
<template>
<teleport to="body">
<!-- 这里的内容会被传送到 body 标签下 -->
<div class="modal">
<!-- 模态框内容 -->
</div>
</teleport>
</template>
2. 常见使用场景
2.1 模态框
<!-- Modal.vue -->
<template>
<teleport to="body">
<div v-if="isOpen" class="modal-overlay">
<div class="modal">
<div class="modal-header">
<h3>{{ title }}</h3>
<button @click="close">×</button>
</div>
<div class="modal-body">
<slot></slot>
</div>
<div class="modal-footer">
<slot name="footer">
<button @click="close">关闭</button>
</slot>
</div>
</div>
</div>
</teleport>
</template>
<script setup>
const props = defineProps({
isOpen: Boolean,
title: String
})
const emit = defineEmits(['update:isOpen'])
const close = () => {
emit('update:isOpen', false)
}
</script>
<style scoped>
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal {
background: white;
padding: 20px;
border-radius: 8px;
min-width: 300px;
}
</style>
2.2 通知提示
<!-- Notification.vue -->
<template>
<teleport to="#notifications-container">
<div
v-if="show"
:class="['notification', type]"
@click="close"
>
<div class="notification-content">
{{ message }}
</div>
</div>
</teleport>
</template>
<script setup>
import { ref, onMounted } from 'vue'
const props = defineProps({
message: String,
type: {
type: String,
default: 'info'
},
duration: {
type: Number,
default: 3000
}
})
const show = ref(true)
const close = () => {
show.value = false
}
onMounted(() => {
if (props.duration > 0) {
setTimeout(close, props.duration)
}
})
</script>
<style scoped>
.notification {
position: fixed;
top: 16px;
right: 16px;
padding: 12px 24px;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s;
}
.info {
background: #e6f7ff;
border: 1px solid #91d5ff;
}
.success {
background: #f6ffed;
border: 1px solid #b7eb8f;
}
.error {
background: #fff2f0;
border: 1px solid #ffccc7;
}
</style>
2.3 上下文菜单
<!-- ContextMenu.vue -->
<template>
<teleport to="body">
<div
v-if="show"
class="context-menu"
:style="position"
>
<slot></slot>
</div>
</teleport>
</template>
<script setup>
import { ref, computed } from 'vue'
const props = defineProps({
show: Boolean,
x: Number,
y: Number
})
const position = computed(() => ({
left: props.x + 'px',
top: props.y + 'px'
}))
</script>
<style scoped>
.context-menu {
position: fixed;
background: white;
border: 1px solid #eee;
border-radius: 4px;
padding: 8px 0;
min-width: 160px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}
</style>
3. 高级用法
3.1 动态目标
<template>
<teleport :to="target">
<div class="content">
动态传送的内容
</div>
</teleport>
</template>
<script setup>
import { ref, onMounted } from 'vue'
const target = ref('body')
onMounted(() => {
// 可以根据条件动态改变目标
if (window.innerWidth < 768) {
target.value = '#mobile-container'
}
})
</script>
3.2 多个 Teleport 到同一目标
<template>
<teleport to="#notifications">
<div class="notification">通知 1</div>
</teleport>
<teleport to="#notifications">
<div class="notification">通知 2</div>
</teleport>
</template>
3.3 条件性传送
<template>
<teleport to="body" :disabled="isMobile">
<div class="modal">
<!-- 在移动端不会被传送,保持原位置 -->
</div>
</teleport>
</template>
<script setup>
import { ref, onMounted } from 'vue'
const isMobile = ref(false)
onMounted(() => {
isMobile.value = window.innerWidth < 768
window.addEventListener('resize', () => {
isMobile.value = window.innerWidth < 768
})
})
</script>
4. 实际应用示例
4.1 全局加载指示器
<!-- LoadingIndicator.vue -->
<template>
<teleport to="body">
<div v-if="loading" class="loading-overlay">
<div class="loading-spinner"></div>
<div class="loading-text">{{ message }}</div>
</div>
</teleport>
</template>
<script setup>
defineProps({
loading: Boolean,
message: {
type: String,
default: '加载中...'
}
})
</script>
<style scoped>
.loading-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(255, 255, 255, 0.9);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 9999;
}
</style>
4.2 图片预览器
<!-- ImageViewer.vue -->
<template>
<teleport to="body">
<div
v-if="visible"
class="image-viewer"
@click="close"
>
<img
:src="imageUrl"
@click.stop
>
<div class="controls">
<button @click.stop="prev"><</button>
<button @click.stop="next">></button>
</div>
</div>
</teleport>
</template>
<script setup>
const props = defineProps({
visible: Boolean,
imageUrl: String,
images: Array
})
const emit = defineEmits(['update:visible'])
const close = () => {
emit('update:visible', false)
}
const prev = () => {
// 实现上一张逻辑
}
const next = () => {
// 实现下一张逻辑
}
</script>
5. 最佳实践
5.1 目标元素管理
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<title>Vue App</title>
</head>
<body>
<div id="app"></div>
<!-- 为 Teleport 预留的容器 -->
<div id="modals"></div>
<div id="notifications"></div>
<div id="tooltips"></div>
</body>
</html>
5.2 组件封装
<!-- BaseModal.vue -->
<template>
<teleport to="#modals">
<transition name="modal">
<div
v-if="modelValue"
class="modal-container"
@click.self="close"
>
<div class="modal-content">
<slot></slot>
</div>
</div>
</transition>
</teleport>
</template>
<script setup>
defineProps({
modelValue: Boolean
})
const emit = defineEmits(['update:modelValue'])
const close = () => {
emit('update:modelValue', false)
}
</script>
6. 注意事项
- 目标元素存在性检查
<template>
<teleport to="#target" :disabled="!targetExists">
<div>内容</div>
</teleport>
</template>
<script setup>
import { ref, onMounted } from 'vue'
const targetExists = ref(false)
onMounted(() => {
targetExists.value = !!document.querySelector('#target')
})
</script>
- SSR 兼容性
<template>
<client-only>
<teleport to="body">
<div>仅客户端渲染的内容</div>
</teleport>
</client-only>
</template>
- 清理工作
<script setup>
import { onUnmounted } from 'vue'
onUnmounted(() => {
// 确保清理所有传送的内容
const target = document.querySelector('#target')
if (target) {
target.innerHTML = ''
}
})
</script>