本文 AI 產出,尚未審核

Vue3 元件基礎:Fragment(多根節點支援)

簡介

在 Vue 2 時,單一根節點(single root node) 是元件的硬性規則。每個元件的 <template> 必須只能有一個最外層元素,否則編譯會失敗。這個限制在簡單的 UI 中不會造成太大問題,但當我們需要在同一層級渲染多個相互獨立的元素(例如表格的 <tr>、列表的 <li>、或是需要在父層直接插入多個區塊的情境)時,就會感到相當不便,往往只能透過額外的 <div> 包裝,導致不必要的 DOM 結構與樣式破壞。

Vue 3 以 Fragment 為核心概念,正式打破「只能有一個根節點」的限制。元件現在可以回傳 多個根節點,Vue 會在內部自動將它們包裹在一個虛擬的 fragment(類似 React 的 <Fragment>),而不會真的在瀏覽器產生額外的 DOM 標籤。這讓開發者在設計 UI 時更自由,也能寫出更語意化、結構更乾淨的程式碼。

本文將從概念說明、實作範例、常見陷阱到最佳實踐,一步步帶你掌握 Vue 3 中的 Fragment,並了解它在實務開發中的應用價值。


核心概念

1. 什麼是 Fragment?

Fragment 是 Vue 內部用來 容納多個根節點 的虛擬容器。它本身不會產生實際的 HTML 標籤,僅在虛擬 DOM (VNode) 階段存在。當 Vue 渲染完成後,這些根節點會直接插入父元素的子樹中,就像它們本來就寫在父層一樣。

重點:Fragment 只是一個概念上的容器,不會在最終的 HTML 中看到 <fragment> 或類似的標籤

2. 為什麼需要 Fragment?

  • 避免不必要的包裝元素:過多的 <div> 會造成「div soup」問題,影響 CSS 選擇器的可讀性與效能。
  • 提升語意化:在表格、列表或 <thead><tbody> 等只能接受特定子元素的父容器中,直接返回多個 <tr><li> 等會更符合 HTML 規範。
  • 簡化組件設計:不必再為了滿足單根節點規則而寫額外的「包裝」元件,減少維護成本。

3. Vue 3 中的 Fragment 實作方式

在 Vue 3,<template> 內部可以直接寫多個平行的元素:

<template>
  <h1>標題</h1>
  <p>說明文字</p>
  <button @click="handleClick">點我</button>
</template>

編譯後,Vue 會自動把這三個節點包在一個 Fragment VNode 中,最終渲染到父元素時不會產生任何額外的標籤。


程式碼範例

範例 1:最簡單的多根節點元件

<!-- Greeting.vue -->
<template>
  <h2>Hello, {{ name }}!</h2>
  <p>今天是 {{ today }}</p>
</template>

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

const name = ref('Vue 開發者')
const today = new Date().toLocaleDateString()
</script>

說明Greeting.vue 同時回傳 <h2><p>,不需要額外的容器。使用時直接放在父層即可:

<div id="app">
  <Greeting />
</div>

範例 2:在 <table> 中使用 Fragment

<!-- TableRows.vue -->
<template>
  <tr v-for="item in items" :key="item.id">
    <td>{{ item.id }}</td>
    <td>{{ item.name }}</td>
    <td>{{ item.price }}</td>
  </tr>
</template>

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

const items = ref([
  { id: 1, name: '蘋果', price: 30 },
  { id: 2, name: '香蕉', price: 20 },
  { id: 3, name: '橙子', price: 25 }
])
</script>

說明TableRows.vue 只回傳多個 <tr>,在父層 <table> 中直接使用:

<table>
  <thead>
    <tr><th>ID</th><th>名稱</th><th>價格</th></tr>
  </thead>
  <tbody>
    <TableRows />
  </tbody>
</table>

這樣不會產生多餘的 <div>,符合 HTML 標準。

範例 3:條件渲染與 Fragment

<!-- ConditionalList.vue -->
<template>
  <ul>
    <li v-if="showHeader">--- 這是清單標題 ---</li>
    <li v-for="item in list" :key="item">{{ item }}</li>
    <li v-if="showFooter">--- 結束 ---</li>
  </ul>
</template>

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

const list = ref(['A', 'B', 'C'])
const showHeader = ref(true)
const showFooter = ref(false)
</script>

說明:即使 v-if 產生的 <li> 可能在不同渲染階段出現或消失,仍然是 同層級的多根節點。Vue 會在每次更新時正確維持 fragment 的結構。

範例 4:組合式 API + Fragment

<!-- Card.vue -->
<template>
  <div class="card-header">{{ title }}</div>
  <div class="card-body">
    <slot />
  </div>
  <div class="card-footer" v-if="showFooter">{{ footer }}</div>
</template>

<script setup>
import { defineProps } from 'vue'

const props = defineProps({
  title: String,
  footer: String,
  showFooter: { type: Boolean, default: false }
})
</script>

<style scoped>
.card-header { font-weight: bold; }
.card-body   { padding: 1rem; }
.card-footer { text-align: right; color: #666; }
</style>

說明Card.vue 本身回傳 三個平行的 <div>,父層使用時可保持原有的結構,且不會因為單根限制而被迫加多餘的包裝。

範例 5:使用 <Fragment> 組件(手動)

Vue 仍提供 Fragment 組件作為顯式的包裝方式(通常不需要):

<script setup>
import { Fragment } from 'vue'
</script>

<template>
  <Fragment>
    <h3>標題</h3>
    <p>說明文字</p>
  </Fragment>
</template>

說明:這個寫法與直接寫多根節點等價,適合在 JSX/TSX 中需要顯式指定 fragment 時使用。


常見陷阱與最佳實踐

陷阱 說明 解決方案
1. 父層不接受多個子節點 某些 HTML 元素(如 <ul><select><table><tbody>)只能接受特定子元素。若子元件返回不符合規範的節點,會產生錯誤或瀏覽器自動修正。 確認父容器的 HTML 規範,將多根節點限制在允許的範圍內(如在 <tbody> 中只返回 <tr>)。
2. 失去 key 的唯一性 v-for 產生多根節點時,若未正確提供 key,Vue 會在 diff 時產生不必要的重繪。 永遠在 v-for設定唯一的 :key,即使節點被包在 fragment 中也不例外。
3. 事件委派被意外中斷 多根節點的最外層不再是一個單一元素,若在父層使用 @click.stop 等事件修飾,可能只會作用於第一個根節點。 在需要的子節點上 個別綁定事件,或在父層使用 v-on 修飾符的捕獲階段.capture)來確保所有子節點都能捕獲。
4. CSS 樣式選擇器失效 原本依賴父元素的唯一子元素(如 .parent > *)在多根節點情況下會匹配到所有根節點,可能導致樣式意外覆寫。 使用更精確的 類別或屬性選擇器,或在根節點上自行加上區分用的 class。
5. SSR (Server‑Side Rendering) 與 Hydration 若在 SSR 中使用 fragment,必須保證客戶端渲染的結果與伺服器端產出的 markup 完全一致。 Vue 3 已內建對 fragment 的完整支援,只要不要在渲染過程中根據環境條件改變根節點的數量即可。

最佳實踐

  1. 盡量在語意上使用 fragment:例如表格列、列表項目、表單區塊等,避免無意義的 <div> 包裝。
  2. 保持根節點的可預測性:在同一個元件中,根節點的數量與順序盡量固定,減少因條件渲染導致的結構變化。
  3. 使用 key:即使在 fragment 中,也要為每個根節點提供唯一的 key,特別是與 v-for 搭配時。
  4. 適度使用 <Fragment> 組件:在 JSX/TSX 中或需要顯式包裝時使用,普通模板中直接寫多根節點即可。
  5. 測試與檢視最終 DOM:使用瀏覽器開發者工具確認渲染結果,確保沒有意外產生的包裝元素。

實際應用場景

1. 表格動態列渲染

在大型資料表格中,常需要根據不同的資料類型渲染不同的 <tr> 結構。透過 fragment,子元件可以直接回傳多個 <tr>,父層 <tbody> 仍保持純粹的表格語意。

2. 表單分段

一個複雜的表單可能被拆成多個「段落」元件,每個段落內部有多個 <input><label>。使用 fragment 可以避免在每段外層多包一層 <div>,讓表單的結構更貼近 <form> 本身的層次。

3. 版面布局(Grid / Flex)

在使用 CSS Grid 或 Flexbox 時,常希望子元素直接作為 grid/flex 的子項目。若每個子元件都需要回傳多個元素,fragment 能讓它們直接成為 grid/flex 的子項目,而不被額外的容器干擾。

4. 動畫與過渡

Vue 的 <TransitionGroup> 必須接受多個子節點才能正確執行列表動畫。配合 fragment,開發者可以把單一元件拆成多個可過渡的元素,而不需要在外層再包一層 <div>

5. 內容投影(Slots)

在自訂的 UI 元件中,常會使用 default slot 讓使用者自行決定要插入多少個元素。若子元件本身回傳多根節點,slot 仍能正確渲染,保持投影內容的原始結構。


總結

Vue 3 引入的 Fragment 讓元件不再受「單根節點」的束縛,開發者可以:

  • 直接回傳多個根節點,減少不必要的 DOM 包裝。
  • 保持 HTML 語意,在表格、列表、表單等需要嚴格子元素規範的情境下更自然地使用。
  • 提升開發效率,寫出更簡潔、可維護的元件結構。

在實務開發中,只要注意父容器的 HTML 規範、正確使用 key、以及避免因條件渲染改變根節點數量,就能安全且高效地利用 fragment。未來隨著 Vue 生態持續演進,Fragment 仍將是構建乾淨、可擴充 UI 的基礎工具之一。祝你在 Vue 3 的旅程中玩得開心,寫出更優雅的元件!