iT邦幫忙

2025 iThome 鐵人賽

DAY 29
1
Vue.js

在 Vue 過氣前要學的三十件事系列 第 29

在 Vue 過氣前要學的第二十九件事 - 先用飛雷神做個標記

  • 分享至 

  • xImage
  •  

前言

<Teleport> 是 Vue3 的內置組件,用以將 DOM 內容傳遞到指定的地方
https://ithelp.ithome.com.tw/upload/images/20250929/20172784YD5GSX1guk.png
而不受限於某個父元素底下,無法使用相關功能。

舉一個小小例子:

帶有呼叫 dialog 按鈕的組件

當你在頁面內部有個按鈕,按了後會顯示 dialog 並在背景底部有遮罩,

那就會有個問題:

按鈕在頁面組件深處,
dialog 和遮罩的 z-index 應高於所有元素,
卻受限於父元素內無法輕易達成操作。


遮罩範圍應該是整個螢幕,而不是父容器的寬高範圍

<script setup>
import MyCard from "./components/Card.vue";
</script>

<template>
  <div class="page">
    <div class="content">
      <MyCard />
    </div>
  </div>
</template>

App.vue

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

const showDialog = ref(false);

function toggleDialog() {
  showDialog.value = !showDialog.value;
}
</script>

<template>
  <div class="container">
    <button @click="toggleDialog">開啟 Dialog</button>
    <div v-if="showDialog">
      <div class="overlay" @click="toggleDialog"></div>
      <div class="dialog">
        <p>這是一個 Dialog</p>
        <button @click="toggleDialog">關閉</button>
      </div>
    </div>
  </div>
</template>

<style scoped>
.container {
  position: relative;
  z-index: 0;
  /* 其餘 style */
}
.overlay {
  position: absolute;
  top: 0;
  left: 0;
  width: 100vw;
  height: 100vh;
  /* 其餘 style */
}
.dialog {
  position: absolute;
  z-index: 101;
  /* 其餘 style */
}
</style>

MyCard.vue

遮罩實際內容大小

其實這邊 overlay 是超出內容元素的,
但因為人在屋簷下,不得不低頭
https://ithelp.ithome.com.tw/upload/images/20250929/20172784yfpsva8vBg.jpg
沒辦法展現原本的樣子。

使用

那我們這邊就來利用 <Teleport> 來輕鬆解決這個問題

<template>
  <div class="container">
    <button @click="toggleDialog">開啟 Dialog</button>
     <!-- <Teleport>是內置元件 不用引入 -->
    <Teleport defer to="body">
      <div v-if="showDialog">
      <div class="overlay" @click="toggleDialog"></div>
      <div class="dialog">
        <p>這是一個 Dialog</p>
        <button @click="toggleDialog">關閉</button>
      </div>
    </div>
    </Teleport>
  </div>
</template>

MyCard.vue
這邊有幾個屬性可以來講一下使用方法 :

  1. to : 用來指定傳遞到哪個元素底下, 你可以有幾種指定方式
  • DOM 元素,ex:
    • 'body' => <body>
  • CSS 選擇器,ex:
    • .container =><div class="container"></div>
    • #main-content => <div id="main-content"></div>
  1. defer : vue3.5+
    延遲 Teleport,直到目標元素掛載後才解析,避免報錯。

<Teleport> 掛載時,傳送的 to 目標必須已經存在於 DOM 中。理想情況下,
這應該是整個 Vue 應用 DOM > 樹外部的一個元素。如果目標元素也是由 Vue 渲染的,
你需要確保在掛載 <Teleport> 之前先掛載該元素。

原始碼

// node_modules>@vue>runtime-dow>dist>runtime-dom.esm-browser.js
const TeleportImpl = {
  name: "Teleport",
  __isTeleport: true,
  process(n1, n2, container, anchor, parentComponent, parentSuspense, namespace, slotScopeIds, optimized, internals) {
   // 省略部分程式碼
    if (n1 == null) {
      // 省略部分程式碼
      if (isTeleportDeferred(n2.props)) {
        n2.el.__isMounted = false; // 暫時標記 Teleport 的根節點未掛載,延遲處理
        queuePostRenderEffect(() => {
          mountToTarget(); // 待目標元件掛載
          delete n2.el.__isMounted; // 刪除剛剛用來延遲掛載的暫時標記
        }, parentSuspense);
      } else {
        mountToTarget();
      }

defer 屬性

禁用情境

https://ithelp.ithome.com.tw/upload/images/20250929/201727846npMVr5Ifh.png
可能在某些情況,例如手機板你不希望顯示 dialog,而是有其他處理方式,
那你也可以在這種情況下停用 Teleport。

<Teleport :disabled="isMobile">
  ...
</Teleport>

SSR

請避免在 SSR 的同時把 Teleport 的目標設為 body——通常 <body>
會包含其他服務端渲染出來的內容,這會使得 Teleport 無法確定激活的正確起始位置。

推薦用一個獨立的只包含 teleport 的內容的容器,例如

<div id="teleported"></div>。

index.html

結語

Teleport 的存在並不影響父子組件的邏輯關係
該傳的參數跟事件照常觸發,只是更改 DOM 元素的渲染結構

明天就是最後一篇了!
https://ithelp.ithome.com.tw/upload/images/20250929/20172784u1LazQVRGe.png

一些小練習

  1. 如果今天要 <teleport> 的元素本身帶有 position:absolute,那傳遞到<body>可能會有什麼問題?

資料來源

Teleport — vue doc


上一篇
在 Vue 過氣前要學的第二十八件事 - 我不想用 Nuxt 但又想要 SSR
系列文
在 Vue 過氣前要學的三十件事29
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言