Vue3 – Teleport 與 Portals
用法 <teleport to="#modals">
簡介
在單頁應用程式 (SPA) 中,我們常會遇到「彈出視窗、全螢幕載入指示、全局通知」等需要 脫離父層結構直接插入到特定 DOM 的情境。傳統的做法是透過 document.body.appendChild 或是使用第三方套件手動搬移元素,但這樣會破壞 Vue 的虛擬 DOM 管理,導致狀態同步變得不可靠。
Vue 3 引入了 Teleport(又稱 Portals)概念,讓開發者可以在 模板語法中聲明 要搬移的內容,同時保留完整的響應式與生命週期。只要在 <teleport> 標籤上指定 to 屬性,內容就會被「傳送」到目標容器,而不必改變原本的組件結構。
此篇文章將深入說明 <teleport to="#modals"> 的使用方式,從核心概念到實務範例,並提供常見陷阱與最佳實踐,幫助你在 Vue3 專案中輕鬆管理彈窗與全局 UI。
核心概念
1. Teleport 是什麼?
- Teleport 是 Vue 3 內建的特殊組件,用來 將子樹搬移到任意 DOM 節點。
- 它不會改變子組件的 作用域、響應式、事件傳遞,所有行為仍然像在原本位置一樣。
- 目標容器可以是 任意 CSS 選擇器(
#id、.class、body等),只要在渲染時已經存在於 DOM。
注意:若目標容器在渲染時尚未出現,Teleport 會暫時保留在原位,待目標出現後再搬移。
2. 基本語法
<teleport to="#modals">
<!-- 這裡的內容會被搬到 id 為 modals 的元素內 -->
<div class="modal">Hello Teleport!</div>
</teleport>
to:接受 CSS selector 或 DOM Element。disabled(可選):設定為true時會暫停搬移,內容仍保留在原位置。
3. 為何使用 #modals?
在大型應用中,我們通常會在 index.html 中預留一個 全局容器,專門放置所有彈窗、對話框與通知:
<body>
<div id="app"></div>
<!-- 所有模態視窗的出口 -->
<div id="modals"></div>
</body>
這樣的做法有兩個好處:
- 層級管理:彈窗不會被父層的 CSS
overflow:hidden、z-index影響。 - 結構清晰:所有「全局 UI」集中在同一個節點,方便統一樣式與事件處理。
程式碼範例
以下提供 5 個實用範例,從最簡單的訊息框到結合動態插槽的完整彈窗系統。
範例 1 – 基本訊息框
<!-- App.vue -->
<template>
<button @click="show = true">顯示訊息框</button>
<teleport to="#modals">
<div v-if="show" class="toast">
這是一個簡易訊息框
<button @click="show = false">關閉</button>
</div>
</teleport>
</template>
<script setup>
import { ref } from 'vue'
const show = ref(false)
</script>
<style scoped>
.toast {
position: fixed;
bottom: 20px;
right: 20px;
background: #333;
color: #fff;
padding: 12px 20px;
border-radius: 4px;
}
</style>
說明:
show為響應式變數,當變為true時,訊息框會透過 Teleport 渲染到#modals,即使App本身被overflow:hidden包住也不受影響。
範例 2 – 動態 Modal 元件
<!-- Modal.vue -->
<template>
<teleport to="#modals">
<div class="overlay" @click.self="close">
<div class="dialog">
<slot></slot>
<button class="close" @click="close">✕</button>
</div>
</div>
</teleport>
</template>
<script setup>
import { defineEmits } from 'vue'
const emit = defineEmits(['close'])
function close() {
emit('close')
}
</script>
<style scoped>
.overlay {
position: fixed;
inset: 0;
background: rgba(0,0,0,0.5);
display: flex;
align-items: center;
justify-content: center;
}
.dialog {
background: #fff;
padding: 24px;
border-radius: 6px;
min-width: 300px;
}
.close {
position: absolute;
top: 8px;
right: 8px;
background: transparent;
border: none;
font-size: 1.2rem;
}
</style>
<!-- 使用 Modal -->
<template>
<button @click="open = true">開啟自訂 Modal</button>
<Modal v-if="open" @close="open = false">
<h3>這是標題</h3>
<p>內容可以放任何 Vue 元件或文字。</p>
</Modal>
</template>
<script setup>
import { ref } from 'vue'
import Modal from './Modal.vue'
const open = ref(false)
</script>
說明:
<teleport>包裹了整個遮罩層,讓 Modal 完全脫離父層結構,且仍能透過v-if控制顯示與隱藏。
範例 3 – 多個目標容器
有時會需要同時在 不同位置 顯示不同類型的 UI(例如左側抽屜與右側通知)。只要在 index.html 中預留多個容器,即可使用不同的 to。
<!-- index.html -->
<body>
<div id="app"></div>
<div id="modals"></div>
<div id="drawers"></div>
</body>
<!-- Drawer.vue -->
<template>
<teleport to="#drawers">
<aside class="drawer" v-show="visible">
<slot></slot>
</aside>
</teleport>
</template>
<script setup>
import { defineProps } from 'vue'
const props = defineProps({ visible: Boolean })
</script>
<style scoped>
.drawer {
position: fixed;
left: 0;
top: 0;
bottom: 0;
width: 260px;
background: #fafafa;
box-shadow: 2px 0 6px rgba(0,0,0,.1);
transform: translateX(-100%);
transition: transform .3s;
}
.drawer[style*="display: block"] {
transform: translateX(0);
}
</style>
說明:
Drawer.vue只會被搬移到#drawers,不會干擾#modals的內容,保持 UI 結構清晰。
範例 4 – 搭配 v-show 與動畫
Vue 內建的過渡 (Transition) 也可以與 Teleport 完美結合,製作出 淡入淡出 的彈窗。
<template>
<button @click="show = true">開啟動畫 Modal</button>
<teleport to="#modals">
<transition name="fade">
<div v-if="show" class="modal-bg" @click.self="show = false">
<div class="modal-content">
<p>這是一個有動畫的 Modal。</p>
</div>
</div>
</transition>
</teleport>
</template>
<script setup>
import { ref } from 'vue'
const show = ref(false)
</script>
<style scoped>
.fade-enter-active, .fade-leave-active {
transition: opacity .3s;
}
.fade-enter-from, .fade-leave-to {
opacity: 0;
}
.modal-bg {
position: fixed;
inset: 0;
background: rgba(0,0,0,0.4);
display: flex;
align-items: center;
justify-content: center;
}
.modal-content {
background: #fff;
padding: 24px;
border-radius: 8px;
}
</style>
說明:
<transition>包裹在v-if內部,動畫會在 Teleport 目標容器中正確執行,使用者感受不會有任何差異。
範例 5 – 動態插入第三方套件 (如 SweetAlert2)
有時需要把 第三方 UI 套件 的根元素掛到 #modals,避免與 Vue 的根節點衝突。
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import Swal from 'sweetalert2'
const app = createApp(App)
// 在全局掛載 SweetAlert2,讓它自動使用 #modals 作為容器
Swal.mixin({
target: '#modals' // SweetAlert2 支援的 target 屬性
})
app.mount('#app')
說明:透過
target設定,SweetAlert2 的彈窗會直接渲染到#modals,與 Vue 的 Teleport 概念相同,維持 UI 統一。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方案 |
|---|---|---|
| 目標容器尚未渲染 | 若 to 指向的元素在 Vue 初始化時不存在,Teleport 會暫時保留在原位。 |
在 index.html 中提前放置目標容器,或使用 v-if 確保容器已載入。 |
| 樣式遺失 | Teleport 跨層級搬移,若樣式使用了父層的 scoped CSS,子樹可能找不到樣式。 |
將共用樣式放在全局 CSS,或使用 ::v-deep/:deep() 手動穿透。 |
| Z-index 衝突 | 多個 Teleport 目標可能同時顯示,Z-index 設定不當會導致遮罩層錯位。 | 為每個目標容器設定明確的 z-index,或在 Teleport 內部使用具體層級。 |
| 事件冒泡 | Teleport 內的事件仍會冒泡至父組件,可能觸發不預期的行為。 | 使用 .stop 或 .prevent 修飾符,或在子組件內部 emit 自訂事件。 |
| SSR (Server‑Side Rendering) | 在 SSR 環境下,目標容器必須在服務端渲染階段就存在。 | 在 app.html(或 nuxt 的 app.vue)中加入目標容器,或在 onMounted 後才使用 Teleport。 |
最佳實踐
- 統一管理容器:在
public/index.html中一次性宣告所有 Teleport 目標(如#modals、#drawers),避免在組件內動態創建。 - 全局樣式:將彈窗、遮罩等常用樣式寫入全局 CSS,確保 Teleport 內容不受
scoped限制。 - 使用
disabled:在需要暫時「關閉」Teleport(例如測試或 SSR)時,利用disabled屬性避免不必要的搬移。 - 避免過度嵌套:盡量保持 Teleport 的子樹簡潔,過深的嵌套會增加除錯難度。
- 命名規範:給目標容器取有意義的 id(如
#modal-root、#notification-portal),提升可讀性與維護性。
實際應用場景
全局 Modal 系統
建立一個統一的ModalProvider,所有子組件只需emit('open-modal', payload),Provider 內部使用 Teleport 把 Modal 渲染到#modals,實現「集中管理」與「跨路由保持」的效果。右下角通知 (Toast)
將 Toast 列表放在#toasts,每一次store中的通知被推入時,Toast 組件透過 Teleport 顯示,確保即使在深層子頁面中也能正常彈出。抽屜式側邊欄
右側抽屜常與主內容的overflow:hidden產生衝突,使用 Teleport 把抽屜搬到#drawers,即可避免被父層裁切。第三方 UI 插件的掛載點
如前述 SweetAlert2、Tippy.js 等,直接指定target為#modals,與 Vue 的 Teleport 概念保持一致,避免 DOM 重疊。SSR + Hydration
在 Nuxt3 或 VitePress 中,先在app.html放置<div id="modals"></div>,確保伺服器端渲染的 HTML 已包含目標容器,避免 hydration 錯誤。
總結
<teleport to="#modals"> 為 Vue 3 提供了一個 乾淨、可預測且高效 的方式,讓開發者可以將彈窗、通知、抽屜等全局 UI 脫離父層結構,同時保留完整的 Vue 响應式與生命週期。透過本文的 核心概念、實務範例、以及 常見陷阱與最佳實踐,你應該已能:
- 正確設置 Teleport 目標容器 (
#modals、#drawers…) - 使用
v-if、v-show、<transition>等 Vue 功能與 Teleport 無縫結合 - 避免樣式、Z-index、SSR 等常見問題,並在專案中落實統一管理策略
把 Teleport 當作 「全局 UI 的出口」,在未來的 Vue3 專案裡,你會發現它不僅提升了程式碼的可維護性,也讓使用者體驗更為流暢。祝開發順利,玩得開心! 🚀