跳至主要內容
Skip to content

一、 前言

在開發網頁應用時,我們經常遇到需要「暫時」將某個元件脫離原本的 DOM 結構,移動到 body 或全螢幕層次的場景。常見的做法是使用 Vue 3 的 Teleport 元件。然而,我們發現許多開發者忽略了 Teleport 的一個強大屬性:disabled

透過這個屬性,我們可以在「全螢幕覆蓋」與「嵌入式佈局」之間無縫切換,而不需要維護兩套元件或複雜的邏輯。

二、 為什麼需要 Teleport?打破層疊上下文 (Stacking Context)

在沒有 Teleport 之前,我們常遇到 UI 佈局的「死穴」:

  1. 嵌套的 z-index:當父容器有 z-index: 1,內部的 Modal 無論設為 z-index: 9999 也無法蓋過其他 z-index: 2 的同級容器。
  2. overflow: hidden 截斷:如果 Modal 的父容器設定了 overflow: hiddentransform,Modal 往往會被無情地裁切。

Teleport 讓我們能保留元件的虛擬邏輯結構,但在 DOM 物理結構上「瞬移」到 body 或其他專用掛載點,徹底解決上述佈局限制。

三、 Teleport 的隱藏技:disabled 屬性

Teleport 元件除了 to 屬性能指定目標容器外,還有一個 disabled 布林屬性。當 disabledtrue 時,該元件所包含的內容會原地待在它原本宣佈的位置,而不會被傳送到 to 的目標容器中。

這在處理「同一元件需要不同位置呈現」的需求時非常有用。

四、 實戰範例:錄影機元件的模式切換

我們以一個影片錄製儀器元件為例。在錄製期間,為了讓受測者專注,我們希望畫面全螢幕覆蓋;但在錄製完成進入「回看預覽」階段時,我們希望它能縮回到頁面原本設計的區塊中,以便使用者查看測試日誌或上傳進度。

以下是簡化後的關鍵實作:

javascript
<template>
  <Teleport
    to='body'
    :disabled='state === "preview" || state === "success"'
  >
    <div
      class='video-recorder'
      :class='{ "fixed inset-0 z-50": state !== "preview" }'
    >
      <!-- 錄製/預覽內容 -->
      <video v-if='state === "recording"' ref='videoRef' autoplay />
      <video v-else-if='state === "preview"' :src='previewUrl' controls />
    </div>
  </Teleport>
</template>

<script setup lang="ts">
import { ref } from 'vue'

const state = ref('ready') // ready, recording, preview, success
const previewUrl = ref('')

// 切換至預覽模式時,Teleport 將會自動將 div 歸位
const handleStop = (blobUrl) => {
  previewUrl.value = blobUrl
  state.value = 'preview'
}
</script>

五、 深入理解:Teleport 內部行為與內容特性

許多開發者誤以為內容「傳送」後就與原元件脫節,事實上 Vue 保持了強大的內部連結:

1. 元件作用域 (Scope)

儘管內容在 DOM 中易位,但它依然是掛載 Teleport 的父元件的子元件。這意味著:

  • 它可以訪問父元件的 PropsInjected 數據。
  • 它依然受父元件的狀態控制(如同步重新渲染)。

2. 事件冒泡 (Event Bubbling)

這是 Teleport 最神奇的特性之一。Vue 的事件冒泡是基於元件層級的,而非 DOM 層級。 如果你在 Teleport 內容內點擊一個按鈕,該事件會沿著 Vue 元件鏈「虛擬地」冒泡回父元件,即使在 DOM 結構中它們已經相隔千山萬水。這讓跨層級的交互逻辑依然能保持高度內聚。

3. 生命週期與 Refs

  • 生命週期onMountedonUnmounted 等勾鉤函數只在元件初次掛載/銷毀時觸發,單純的 to 標靶變動或 disabled 切換並不會導致元件被銷毀重造。
  • Template Refs:依然可以透過 ref 正常獲取 Teleport 內部的 DOM 元素。

六、 技術實務:適配、錯誤處理與進階模式

在這種高度依賴硬體(攝影機)與 DOM 變動的場景下,我們必須考慮更深層的實務問題。

1. 環境適配與約束

針對行動端與桌面端,建議區分攝影機的解析度策略。例如使用 ideal 來提供偏好設定,而非強制的 exact,這能增加裝置的相容性。

2. 錯誤處理與權限

存取攝像頭時,最常見的錯誤是 NotAllowedError。我們建議務必提供完整的錯誤處理邏輯。

javascript
async function initCamera() {
  try {
    const stream = await navigator.mediaDevices.getUserMedia({
      video: { facingMode: "user" },
      audio: true,
    });
    // 成功後的處理
  } catch (error) {
    if (error.name === "NotAllowedError") {
      console.error("提示:使用者拒絕了攝影機權限");
    } else if (error.name === "NotFoundError") {
      console.error("提示:找不到可用的攝影機裝置");
    }
  }
}

3. 相容性探測

在錄製影片前,應先檢查瀏覽器是否支援預期的 MIME Type。

javascript
const getSupportedMimeType = () => {
  const types = ["video/mp4", "video/webm;codecs=h264", "video/webm"];
  for (const type of types) {
    if (MediaRecorder.isTypeSupported(type)) {
      return type;
    }
  }
  return "";
};

4. Scoped CSS 的陷阱

由於 Vue 的 Scoped CSS 是透過為元素添加 data-v-xxxx 屬性來實現的,且樣式表僅存在於定義該樣式的元件中。

  • 注意:如果你的樣式定義在父元件的 <style scoped> 中,而被 Teleport 傳送的元素剛好是該元件的「子根元素」(sub-root),樣式可能失效。
  • 建議:對於需要 Teleport 的 UI 元素,使用全域樣式或 CSS Modules,或者將樣式定義在被傳送的元件內部。

5. 與 Transition 配合

Teleport 可以無縫嵌套在 <Transition> 中:

html
<Transition name="fade">
  <Teleport to="body" v-if="isVisible">
    <div class="modal">這是一個帶動畫的傳送彈窗</div>
  </Teleport>
</Transition>

七、 總結

Teleportdisabled 屬性讓 Vue 元件具備了靈活移換位場地的能力。它不只是將內容移往 body 的快捷鍵,更是實現「模式切換」流暢體驗的關鍵。

透過動態控制彈出/歸位,我們可以寫出結構更乾淨、邏輯更內聚的元件,這也正是我們追求的前端開發藝術。

延伸閱讀與資源