本文 AI 產出,尚未審核

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.classbody 等),只要在渲染時已經存在於 DOM。

注意:若目標容器在渲染時尚未出現,Teleport 會暫時保留在原位,待目標出現後再搬移。

2. 基本語法

<teleport to="#modals">
  <!-- 這裡的內容會被搬到 id 為 modals 的元素內 -->
  <div class="modal">Hello Teleport!</div>
</teleport>
  • to:接受 CSS selectorDOM Element
  • disabled(可選):設定為 true 時會暫停搬移,內容仍保留在原位置。

3. 為何使用 #modals

在大型應用中,我們通常會在 index.html 中預留一個 全局容器,專門放置所有彈窗、對話框與通知:

<body>
  <div id="app"></div>
  <!-- 所有模態視窗的出口 -->
  <div id="modals"></div>
</body>

這樣的做法有兩個好處:

  1. 層級管理:彈窗不會被父層的 CSS overflow:hiddenz-index 影響。
  2. 結構清晰:所有「全局 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(或 nuxtapp.vue)中加入目標容器,或在 onMounted 後才使用 Teleport。

最佳實踐

  1. 統一管理容器:在 public/index.html 中一次性宣告所有 Teleport 目標(如 #modals#drawers),避免在組件內動態創建。
  2. 全局樣式:將彈窗、遮罩等常用樣式寫入全局 CSS,確保 Teleport 內容不受 scoped 限制。
  3. 使用 disabled:在需要暫時「關閉」Teleport(例如測試或 SSR)時,利用 disabled 屬性避免不必要的搬移。
  4. 避免過度嵌套:盡量保持 Teleport 的子樹簡潔,過深的嵌套會增加除錯難度。
  5. 命名規範:給目標容器取有意義的 id(如 #modal-root#notification-portal),提升可讀性與維護性。

實際應用場景

  1. 全局 Modal 系統
    建立一個統一的 ModalProvider,所有子組件只需 emit('open-modal', payload),Provider 內部使用 Teleport 把 Modal 渲染到 #modals,實現「集中管理」與「跨路由保持」的效果。

  2. 右下角通知 (Toast)
    將 Toast 列表放在 #toasts,每一次 store 中的通知被推入時,Toast 組件透過 Teleport 顯示,確保即使在深層子頁面中也能正常彈出。

  3. 抽屜式側邊欄
    右側抽屜常與主內容的 overflow:hidden 產生衝突,使用 Teleport 把抽屜搬到 #drawers,即可避免被父層裁切。

  4. 第三方 UI 插件的掛載點
    如前述 SweetAlert2、Tippy.js 等,直接指定 target#modals,與 Vue 的 Teleport 概念保持一致,避免 DOM 重疊。

  5. SSR + Hydration
    在 Nuxt3 或 VitePress 中,先在 app.html 放置 <div id="modals"></div>,確保伺服器端渲染的 HTML 已包含目標容器,避免 hydration 錯誤。


總結

<teleport to="#modals"> 為 Vue 3 提供了一個 乾淨、可預測且高效 的方式,讓開發者可以將彈窗、通知、抽屜等全局 UI 脫離父層結構,同時保留完整的 Vue 响應式與生命週期。透過本文的 核心概念實務範例、以及 常見陷阱與最佳實踐,你應該已能:

  • 正確設置 Teleport 目標容器 (#modals#drawers …)
  • 使用 v-ifv-show<transition> 等 Vue 功能與 Teleport 無縫結合
  • 避免樣式、Z-index、SSR 等常見問題,並在專案中落實統一管理策略

把 Teleport 當作 「全局 UI 的出口」,在未來的 Vue3 專案裡,你會發現它不僅提升了程式碼的可維護性,也讓使用者體驗更為流暢。祝開發順利,玩得開心! 🚀