本文 AI 產出,尚未審核

Vue3 元件通信(Component Communication) – Teleport 跨層渲染

簡介

在單頁應用 (SPA) 中,我們常會遇到「彈窗、提示訊息或全域通知」需要脫離原本的 DOM 結構,直接渲染到最外層的 <body> 或其他指定容器。若僅靠 CSS 的 position: fixed,仍會受到父層 overflow:hiddenz-index 等限制,導致 UI 表現不如預期。

Vue 3 提供的 Teleport 正是為了解決這類「跨層渲染」的需求。它讓開發者可以 將子元件的渲染節點搬移 到任意 DOM 位置,同時保持完整的響應式、事件傳遞與生命週期。掌握 Teleport,能讓你的 UI 結構更彈性、代碼更乾淨,並減少因層級限制產生的 BUG。

以下文章將從概念說明、實作範例、常見陷阱與最佳實踐,帶你一步步深入了解 Teleport 的使用方式,並提供實務上常見的應用情境。

核心概念

1. Teleport 的基本語法

<teleport to="selector">
  <!-- 需要被搬移的內容 -->
</teleport>
  • to 屬性接受 CSS selectorDOM 元素,指定最終渲染的目標位置。
  • 若目標在渲染時不存在,Vue 會在 掛載完成後 重新搜尋,直到找到為止。

注意:Teleport 並不會改變元件的 作用域,子元件仍能存取父層的 propsprovide/injectemit 等。

2. 多層 Teleport

在大型專案中,可能會同時需要 多個不同層級的 Teleport(例如模態框、全域提示、浮動工具列)。Vue 允許在同一個父元件內嵌套多個 Teleport,且每個 Teleport 都是獨立的渲染單位。

<template>
  <div class="app">
    <!-- Modal -->
    <teleport to="#modal-root">
      <Modal />
    </teleport>

    <!-- Toast -->
    <teleport to="#toast-root">
      <Toast />
    </teleport>
  </div>
</template>

3. 何時使用 Teleport

使用情境 為什麼需要 Teleport
Modal / Dialog 需要脫離父層 overflow:hiddenz-index 限制
全域通知 (Toast) 直接掛在 <body>,保證層級最高
Portal UI (如 dropdown、tooltip) 保持位置計算簡潔,避免父層 CSS 影響
SSR / Hydration Teleport 會在伺服器端渲染正確的 DOM 結構,避免客戶端重新定位

程式碼範例

以下示範 4 個實用範例,涵蓋從最簡單的 Teleport 到結合 provide/inject、動態目標與過渡效果的進階用法。

範例 1:最簡單的 Modal

<!-- App.vue -->
<template>
  <button @click="show = true">開啟 Modal</button>

  <teleport to="body">
    <div v-if="show" class="modal-backdrop" @click.self="show = false">
      <div class="modal-content">
        <h3>這是一個 Modal</h3>
        <p>點擊背景即可關閉。</p>
      </div>
    </div>
  </teleport>
</template>

<script setup>
import { ref } from 'vue'
const show = ref(false)
</script>

<style scoped>
.modal-backdrop {
  position: fixed; inset: 0; background: rgba(0,0,0,0.5);
  display: flex; align-items: center; justify-content: center;
}
.modal-content {
  background: #fff; padding: 20px; border-radius: 8px;
}
</style>

說明:使用 @click.self 只在點擊背景時關閉,避免點到內容區也觸發。

範例 2:動態 Teleport 目標

<!-- ToastContainer.vue -->
<template>
  <div id="toast-root"></div>
</template>

<script setup>
import { onMounted, ref } from 'vue'

const target = ref('#toast-root')

onMounted(() => {
  // 若根據使用者主題或語系切換目標容器
  if (document.body.classList.contains('dark')) {
    target.value = '#dark-toast-root'
  }
})
</script>
<!-- SomeComponent.vue -->
<template>
  <button @click="addToast">顯示 Toast</button>

  <teleport :to="target">
    <div v-if="visible" class="toast">{{ message }}</div>
  </teleport>
</template>

<script setup>
import { ref } from 'vue'
const target = ref('#toast-root')
const visible = ref(false)
const message = ref('這是通知訊息')

function addToast() {
  visible.value = true
  setTimeout(() => visible.value = false, 3000)
}
</script>

<style scoped>
.toast {
  position: fixed; bottom: 20px; right: 20px;
  background: #333; color: #fff; padding: 10px 15px;
  border-radius: 4px;
}
</style>

說明:to 可以是 reactive 的,讓目標容器在運行時切換。

範例 3:結合 provide / inject 的跨層資料共享

<!-- Provider.vue -->
<template>
  <teleport to="#modal-root">
    <Modal />
  </teleport>
  <slot />
</template>

<script setup>
import { provide, ref } from 'vue'
import Modal from './Modal.vue'

const isOpen = ref(false)
const toggle = () => (isOpen.value = !isOpen.value)

provide('modalState', { isOpen, toggle })
</script>
<!-- Modal.vue -->
<template>
  <div v-if="modal.isOpen" class="modal">
    <slot />
    <button @click="modal.toggle">關閉</button>
  </div>
</template>

<script setup>
import { inject } from 'vue'
const modal = inject('modalState')
</script>

<style scoped>
.modal { /* 樣式同範例 1 */ }
</style>

說明:即使 Modal 被 Teleport#modal-root,仍能透過 inject 直接取得父層提供的狀態,保持資料同步。

範例 4:加入過渡動畫

<template>
  <button @click="show = true">顯示側邊抽屜</button>

  <teleport to="body">
    <transition name="slide">
      <aside v-if="show" class="drawer">
        <button @click="show = false">✕ 關閉</button>
        <slot />
      </aside>
    </transition>
  </teleport>
</template>

<script setup>
import { ref } from 'vue'
const show = ref(false)
</script>

<style scoped>
.drawer {
  position: fixed; top: 0; right: 0; width: 300px; height: 100%;
  background: #fafafa; box-shadow: -2px 0 8px rgba(0,0,0,0.2);
  padding: 20px;
}
.slide-enter-active, .slide-leave-active { transition: transform .3s ease; }
.slide-enter-from, .slide-leave-to { transform: translateX(100%); }
</style>

說明<transition> 能與 Teleport 無縫結合,讓跨層渲染的 UI 同樣享有 Vue 的過渡效果。

常見陷阱與最佳實踐

陷阱 原因 解法
目標容器不存在 Teleport 會在掛載時找不到 selector,導致內容不顯示。 在根模板或 index.html 中預先建立目標容器,或使用 v-if 延遲渲染。
樣式遺失 Teleport 的內容不在原本的父層,會失去父層的 CSS 繼承(如 font-familycolor)。 把共用樣式寫在全局 CSS,或在 Teleport 內部重新引入必要的 class。
事件冒泡失效 若在 Teleport 內部使用 @click.stop,事件不會冒泡到原本的父元件。 仍可透過 emitprovide/inject 直接傳遞資料。
SSR Hydration 不一致 伺服器端渲染時,目標容器位置與客戶端不相同可能導致 hydration 警告。 確保 to 指向的容器在 both server & client 中均已存在。
過度使用 Teleport 把大量 UI 都搬到 <body> 會讓 DOM 結構變得難以追蹤。 僅在 需要脫離層級限制 時使用,其他情況仍保留在本地結構。

最佳實踐

  1. 預先建立目標容器:在 public/index.html 中加入 <div id="modal-root"></div><div id="toast-root"></div> 等,確保 Teleport 永遠有落腳點。
  2. 使用 v-if 控制渲染:避免在目標容器尚未掛載前渲染 Teleport,減少不必要的警告。
  3. 統一樣式管理:將跨層 UI 的樣式抽離成全局 CSS,或使用 CSS Modules + :global
  4. 保持可讀性:在大型專案中,將 Teleport 包裝成 獨立元件(如 ModalPortal.vue),讓使用者只需 <ModalPortal> 即可。
  5. 測試與可訪問性:確保 Teleport 內的焦點管理、鍵盤導航與 ARIA 標記正確,避免因搬移 DOM 而失去可訪問性。

實際應用場景

1. 多層級的模態框系統

大型管理平台常同時開啟「新增資料」與「檢視細節」的對話框。透過 Teleport,每個模態框都渲染到同一個 #modal-root,再以 z-index 控制堆疊順序,避免因父層 overflow:hidden 被裁切。

2. 全域通知 (Toast)

即時訊息、表單驗證錯誤或系統提示,通常需要在畫面最上層顯示。使用 Teleport 把 Toast 元件搬到 <body>,即使在深層的子路由或動態載入的組件內觸發,也能保證顯示位置正確。

3. 動態工具列 (Dropdown / Popover)

下拉選單、日期選擇器等 UI 元件在滾動容器內時,若直接渲染會受到父層 overflow:auto 影響。利用 Teleport 把彈出層搬到根容器,配合座標計算即可實現「不被裁切」的彈出效果。

4. 服務端渲染 (SSR) 的 SEO 需求

在 SSR 環境中,某些彈窗內容需要在首屏就渲染(例如「首次訪問提示」),TelePort 能在伺服器端直接產生正確的 DOM 結構,避免客戶端重新定位造成閃爍。

總結

  • Teleport 是 Vue 3 為跨層渲染設計的強大工具,讓 UI 元件可以脫離父層限制,直接掛載到任意 DOM 節點。
  • 它保留了完整的 響應式、事件傳遞與生命週期,不會因搬移而失去與父層的資料關聯。
  • 使用時要注意 目標容器的存在、樣式繼承與 SSR 的一致性,並遵循 預先建立容器、使用 v-if 控制渲染、統一樣式管理 的最佳實踐。
  • 在實務開發中,Teleport 常見於 Modal、Toast、Dropdown、Portal UI 等需要「脫離層級」的情境,能大幅提升 UI 的彈性與可維護性。

掌握 Teleport 後,你的 Vue3 應用將能更從容地處理跨層 UI,提供使用者更流暢、穩定的互動體驗。祝開發順利,玩得開心!