一、 前言
在開發網頁應用時,我們經常遇到需要「暫時」將某個元件脫離原本的 DOM 結構,移動到 body 或全螢幕層次的場景。常見的做法是使用 Vue 3 的 Teleport 元件。然而,我們發現許多開發者忽略了 Teleport 的一個強大屬性:disabled。
透過這個屬性,我們可以在「全螢幕覆蓋」與「嵌入式佈局」之間無縫切換,而不需要維護兩套元件或複雜的邏輯。
二、 為什麼需要 Teleport?打破層疊上下文 (Stacking Context)
在沒有 Teleport 之前,我們常遇到 UI 佈局的「死穴」:
- 嵌套的 z-index:當父容器有
z-index: 1,內部的 Modal 無論設為z-index: 9999也無法蓋過其他z-index: 2的同級容器。 - overflow: hidden 截斷:如果 Modal 的父容器設定了
overflow: hidden或transform,Modal 往往會被無情地裁切。
Teleport 讓我們能保留元件的虛擬邏輯結構,但在 DOM 物理結構上「瞬移」到 body 或其他專用掛載點,徹底解決上述佈局限制。
三、 Teleport 的隱藏技:disabled 屬性
Teleport 元件除了 to 屬性能指定目標容器外,還有一個 disabled 布林屬性。當 disabled 為 true 時,該元件所包含的內容會原地待在它原本宣佈的位置,而不會被傳送到 to 的目標容器中。
這在處理「同一元件需要不同位置呈現」的需求時非常有用。
四、 實戰範例:錄影機元件的模式切換
我們以一個影片錄製儀器元件為例。在錄製期間,為了讓受測者專注,我們希望畫面全螢幕覆蓋;但在錄製完成進入「回看預覽」階段時,我們希望它能縮回到頁面原本設計的區塊中,以便使用者查看測試日誌或上傳進度。
以下是簡化後的關鍵實作:
<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 的父元件的子元件。這意味著:
- 它可以訪問父元件的
Props與Injected數據。 - 它依然受父元件的狀態控制(如同步重新渲染)。
2. 事件冒泡 (Event Bubbling)
這是 Teleport 最神奇的特性之一。Vue 的事件冒泡是基於元件層級的,而非 DOM 層級。 如果你在 Teleport 內容內點擊一個按鈕,該事件會沿著 Vue 元件鏈「虛擬地」冒泡回父元件,即使在 DOM 結構中它們已經相隔千山萬水。這讓跨層級的交互逻辑依然能保持高度內聚。
3. 生命週期與 Refs
- 生命週期:
onMounted、onUnmounted等勾鉤函數只在元件初次掛載/銷毀時觸發,單純的to標靶變動或disabled切換並不會導致元件被銷毀重造。 - Template Refs:依然可以透過
ref正常獲取 Teleport 內部的 DOM 元素。
六、 技術實務:適配、錯誤處理與進階模式
在這種高度依賴硬體(攝影機)與 DOM 變動的場景下,我們必須考慮更深層的實務問題。
1. 環境適配與約束
針對行動端與桌面端,建議區分攝影機的解析度策略。例如使用 ideal 來提供偏好設定,而非強制的 exact,這能增加裝置的相容性。
2. 錯誤處理與權限
存取攝像頭時,最常見的錯誤是 NotAllowedError。我們建議務必提供完整的錯誤處理邏輯。
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。
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> 中:
<Transition name="fade">
<Teleport to="body" v-if="isVisible">
<div class="modal">這是一個帶動畫的傳送彈窗</div>
</Teleport>
</Transition>七、 總結
Teleport 的 disabled 屬性讓 Vue 元件具備了靈活移換位場地的能力。它不只是將內容移往 body 的快捷鍵,更是實現「模式切換」流暢體驗的關鍵。
透過動態控制彈出/歸位,我們可以寫出結構更乾淨、邏輯更內聚的元件,這也正是我們追求的前端開發藝術。