Teleport в Vue.js
Что такое Teleport?
Teleport — это встроенный компонент Vue 3, который позволяет отрендерить содержимое в любом месте DOM, даже вне корневого элемента Vue-приложения.
Это особенно полезно для модальных окон, всплывающих подсказок, уведомлений и других элементов, которые должны находиться вне текущей иерархии.
Базовое использование
<template>
<div class="component">
<button @click="showModal = true">Открыть модальное окно</button>
<Teleport to="body">
<div v-if="showModal" class="modal">
<p>Я отрендерен в body!</p>
<button @click="showModal = false">Закрыть</button>
</div>
</Teleport>
</div>
</template>
<script setup>
import { ref } from 'vue'
const showModal = ref(false)
</script>
<style scoped>
.modal {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
padding: 20px;
z-index: 1000;
}
</style>
Содержимое <Teleport> будет отрендерено в <body>, а не внутри .component.
Селекторы для to
Атрибут to принимает CSS селектор или DOM элемент:
CSS селектор
<!-- По ID -->
<Teleport to="#modal-container">
<div>Контент</div>
</Teleport>
<!-- По классу -->
<Teleport to=".modal-wrapper">
<div>Контент</div>
</Teleport>
<!-- По тегу -->
<Teleport to="body">
<div>Контент</div>
</Teleport>
DOM элемент
<script setup>
import { ref, onMounted } from 'vue'
const targetElement = ref(null)
onMounted(() => {
targetElement.value = document.getElementById('custom-container')
})
</script>
<template>
<Teleport :to="targetElement" v-if="targetElement">
<div>Контент</div>
</Teleport>
</template>
Отключение Teleport
Можно динамически включать/отключать teleportацию:
<template>
<Teleport to="body" :disabled="!isMobile">
<div class="modal">
На мобильных — в body, на десктопе — здесь
</div>
</Teleport>
</template>
<script setup>
import { ref, onMounted } from 'vue'
const isMobile = ref(false)
onMounted(() => {
isMobile.value = window.innerWidth < 768
})
</script>
Несколько Teleport в одну цель
Можно "телепортировать" несколько компонентов в одно место:
<!-- Component1.vue -->
<Teleport to="#notifications">
<div>Уведомление 1</div>
</Teleport>
<!-- Component2.vue -->
<Teleport to="#notifications">
<div>Уведомление 2</div>
</Teleport>
<!-- index.html -->
<div id="app"></div>
<div id="notifications"></div>
Оба уведомления будут отрендерены в #notifications в порядке монтирования.
Практические примеры
Модальное окно
<!-- Modal.vue -->
<template>
<Teleport to="body">
<Transition name="modal">
<div v-if="isOpen" class="modal-overlay" @click="close">
<div class="modal-content" @click.stop>
<button class="modal-close" @click="close">×</button>
<slot />
</div>
</div>
</Transition>
</Teleport>
</template>
<script setup>
defineProps({
isOpen: Boolean
})
const emit = defineEmits(['close'])
function close() {
emit('close')
}
</script>
<style scoped>
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-content {
background: white;
padding: 20px;
border-radius: 8px;
max-width: 500px;
position: relative;
}
.modal-close {
position: absolute;
top: 10px;
right: 10px;
background: none;
border: none;
font-size: 24px;
cursor: pointer;
}
.modal-enter-active,
.modal-leave-active {
transition: opacity 0.3s;
}
.modal-enter-from,
.modal-leave-to {
opacity: 0;
}
</style>
Использование:
<template>
<button @click="isModalOpen = true">Открыть</button>
<Modal :is-open="isModalOpen" @close="isModalOpen = false">
<h2>Заголовок</h2>
<p>Содержимое модального окна</p>
</Modal>
</template>
<script setup>
import { ref } from 'vue'
import Modal from './Modal.vue'
const isModalOpen = ref(false)
</script>
Всплывающие уведомления
<!-- NotificationContainer.vue -->
<template>
<Teleport to="body">
<div class="notifications">
<TransitionGroup name="notification">
<div
v-for="notification in notifications"
:key="notification.id"
class="notification"
:class="`notification-${notification.type}`"
>
{{ notification.message }}
</div>
</TransitionGroup>
</div>
</Teleport>
</template>
<script setup>
import { ref } from 'vue'
const notifications = ref([])
function addNotification(message, type = 'info') {
const id = Date.now()
notifications.value.push({ id, message, type })
setTimeout(() => {
removeNotification(id)
}, 3000)
}
function removeNotification(id) {
const index = notifications.value.findIndex(n => n.id === id)
if (index > -1) {
notifications.value.splice(index, 1)
}
}
defineExpose({ addNotification })
</script>
<style scoped>
.notifications {
position: fixed;
top: 20px;
right: 20px;
z-index: 9999;
}
.notification {
background: white;
padding: 15px 20px;
margin-bottom: 10px;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
min-width: 250px;
}
.notification-success {
border-left: 4px solid #52c41a;
}
.notification-error {
border-left: 4px solid #f5222d;
}
.notification-enter-active,
.notification-leave-active {
transition: all 0.3s;
}
.notification-enter-from {
transform: translateX(100%);
opacity: 0;
}
.notification-leave-to {
transform: translateY(-20px);
opacity: 0;
}
</style>
Tooltip
<!-- Tooltip.vue -->
<template>
<div
ref="triggerRef"
@mouseenter="show"
@mouseleave="hide"
>
<slot />
</div>
<Teleport to="body">
<div
v-if="isVisible"
ref="tooltipRef"
class="tooltip"
:style="tooltipStyle"
>
{{ text }}
</div>
</Teleport>
</template>
<script setup>
import { ref, computed } from 'vue'
defineProps({
text: String
})
const isVisible = ref(false)
const triggerRef = ref(null)
const position = ref({ x: 0, y: 0 })
function show() {
if (triggerRef.value) {
const rect = triggerRef.value.getBoundingClientRect()
position.value = {
x: rect.left + rect.width / 2,
y: rect.top - 10
}
}
isVisible.value = true
}
function hide() {
isVisible.value = false
}
const tooltipStyle = computed(() => ({
position: 'fixed',
left: `${position.value.x}px`,
top: `${position.value.y}px`,
transform: 'translate(-50%, -100%)'
}))
</script>
<style scoped>
.tooltip {
background: #333;
color: white;
padding: 5px 10px;
border-radius: 4px;
font-size: 14px;
white-space: nowrap;
pointer-events: none;
z-index: 9999;
}
</style>
Teleport vs CSS position: fixed
Без Teleport
<template>
<div class="parent" style="position: relative; overflow: hidden">
<div class="modal" style="position: fixed">
<!-- Может быть обрезано из-за overflow: hidden -->
</div>
</div>
</template>
Проблемы:
- Может быть обрезано родительским
overflow - Наследует
z-indexконтекст - Сложно управлять порядком наложения
С Teleport
<template>
<div class="parent" style="position: relative; overflow: hidden">
<Teleport to="body">
<div class="modal" style="position: fixed">
<!-- Всегда отображается правильно -->
</div>
</Teleport>
</div>
</template>
Преимущества:
- Не зависит от родительских стилей
- Свой контекст наложения
- Легко управлять z-index
Работа с SSR
При использовании SSR, целевой элемент должен существовать:
<template>
<Teleport to="body">
<!-- В SSR body существует -->
<div>Контент</div>
</Teleport>
<Teleport to="#app-modals" :disabled="!mounted">
<!-- Отключаем пока не смонтировано -->
<div>Контент</div>
</Teleport>
</template>
<script setup>
import { ref, onMounted } from 'vue'
const mounted = ref(false)
onMounted(() => {
mounted.value = true
})
</script>
Ограничения
Целевой элемент должен существовать
<!-- Неправильно -->
<Teleport to="#non-existent">
<div>Ошибка!</div>
</Teleport>
<!-- Правильно -->
<Teleport to="body">
<div>Работает!</div>
</Teleport>
Teleport не изменяет логическую иерархию
<script setup>
import { provide } from 'vue'
provide('theme', 'dark')
</script>
<template>
<Teleport to="body">
<!-- inject всё ещё работает! -->
<ChildComponent />
</Teleport>
</template>
Props, events, provide/inject продолжают работать, так как меняется только DOM, а не иерархия компонентов.
Вывод
Teleport:
- Рендерит контент в любом месте DOM
- Не изменяет логическую иерархию компонентов
- Полезен для модальных окон, tooltips, уведомлений
- Решает проблемы с overflow и z-index
- Можно отключать динамически
- Поддерживает множественные teleport в одну цель
На собеседовании:
Важно уметь:
- Объяснить, что такое Teleport и зачем он нужен
- Показать примеры использования (модальные окна, уведомления)
- Рассказать о проблемах, которые решает Teleport
- Объяснить, что логическая иерархия не меняется
- Привести отличия от CSS position: fixed