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 的完整支援,只要不要在渲染過程中根據環境條件改變根節點的數量即可。 |
最佳實踐
- 盡量在語意上使用 fragment:例如表格列、列表項目、表單區塊等,避免無意義的
<div>包裝。 - 保持根節點的可預測性:在同一個元件中,根節點的數量與順序盡量固定,減少因條件渲染導致的結構變化。
- 使用
key:即使在 fragment 中,也要為每個根節點提供唯一的key,特別是與v-for搭配時。 - 適度使用
<Fragment>組件:在 JSX/TSX 中或需要顯式包裝時使用,普通模板中直接寫多根節點即可。 - 測試與檢視最終 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 的旅程中玩得開心,寫出更優雅的元件!