Vue3 教學:模板語法 – Template Inheritance(v-slot 與具名插槽)
簡介
在 Vue3 中,**插槽(slot)是組件間內容分發的核心機制,讓父層可以在子層的特定位置投射自訂的 HTML。隨著應用規模的成長,單純的預設插槽往往不足以滿足版面繼承(template inheritance)**的需求,這時就需要 具名插槽(named slots) 搭配 v-slot 指令 來實現更彈性的模板組合。
掌握 v-slot 與具名插槽不僅能讓 UI 元件保持 單一職責、高可重用性,同時也能在大型專案中避免「深層傳遞」與「樣板程式碼」的問題,提升開發效率與維護性。
核心概念
1. 什麼是具名插槽(Named Slots)?
預設插槽只允許一個 <slot></slot>,子組件會把父層傳入的內容直接渲染在此位置。具名插槽則允許在子組件內部定義多個 <slot name="...">,父層可以針對不同名稱投射不同的內容。
<!-- ChildComponent.vue -->
<template>
<header>
<slot name="header">預設標題</slot>
</header>
<main>
<slot>預設內容</slot>
</main>
<footer>
<slot name="footer">預設版權資訊</slot>
</footer>
</template>
2. v-slot 指令的語法
在 Vue3 中,v-slot 取代了舊版的 slot-scope,提供了 作用域插槽(scoped slots) 與具名插槽的統一寫法。
<!-- 父層使用具名插槽 -->
<ChildComponent>
<template v-slot:header>
<h1>自訂標題</h1>
</template>
<template v-slot:footer>
<p>© 2025 My Company</p>
</template>
<!-- 預設插槽直接寫在子組件標籤內 -->
<p>這是主要內容。</p>
</ChildComponent>
3. 簡寫語法
如果插槽名稱是 default(預設插槽),可以直接使用 v-slot 而不必加冒號;若名稱是 header,則 v-slot:header 或 #header(簡寫)皆可。
<!-- 簡寫範例 -->
<ChildComponent>
<template #header>
<h1>簡寫標題</h1>
</template>
<template #default>
<p>直接使用 #default 也可以。</p>
</template>
</ChildComponent>
4. 作用域插槽(Scoped Slots)與資料傳遞
具名插槽本身也可以傳遞 作用域資料,子組件在 <slot> 標籤上提供 v-bind,父層在 v-slot 中解構取得。
<!-- ChildComponent.vue -->
<template>
<ul>
<slot name="item" v-for="(user, i) in users" :key="i" :user="user"></slot>
</ul>
</template>
<script setup>
import { ref } from 'vue'
const users = ref([
{ name: 'Alice', age: 28 },
{ name: 'Bob', age: 34 }
])
</script>
<!-- 父層使用 -->
<ChildComponent>
<template #item="{ user }">
<li>{{ user.name }} ({{ user.age }} 歲)</li>
</template>
</ChildComponent>
重點:
{ user }是解構自子組件傳出的user物件,讓父層能自由決定渲染方式。
5. 多層插槽的「模板繼承」技巧
在複雜 UI(如卡片、表單)中,常會有「父組件 → 中間組件 → 子組件」的多層結構。只要每層都使用具名插槽與 v-slot,就能實現 模板繼承:子層只負責提供佈局與資料,父層決定最終的呈現。
<!-- BaseLayout.vue (最底層) -->
<template>
<section class="base">
<slot name="title"></slot>
<slot name="body"></slot>
</section>
</template>
<!-- Card.vue (中間層) -->
<template>
<BaseLayout>
<template #title>
<h2 class="card-title">{{ cardTitle }}</h2>
</template>
<template #body>
<slot></slot> <!-- 交給最外層決定內容 -->
</template>
</BaseLayout>
</template>
<script setup>
defineProps({ cardTitle: String })
</script>
<!-- App.vue (最外層) -->
<template>
<Card cardTitle="使用者資訊">
<p>這是一段說明文字。</p>
<ul>
<li>項目 A</li>
<li>項目 B</li>
</ul>
</Card>
</template>
上述例子展現了 「底層提供佈局」+「中層封裝」+「外層填充內容」 的典型模板繼承模式。
程式碼範例
下面提供 5 個實務中常見的範例,每個範例都包含詳細註解,方便你快速上手。
範例 1:基本具名插槽
<!-- Modal.vue -->
<template>
<div class="modal">
<header class="modal-header">
<slot name="header">預設標題</slot>
</header>
<section class="modal-body">
<slot>預設內容</slot>
</section>
<footer class="modal-footer">
<slot name="footer">
<button @click="$emit('close')">關閉</button>
</slot>
</footer>
</div>
</template>
<!-- 使用 Modal -->
<Modal @close="show = false">
<template #header>
<h3>自訂對話框標題</h3>
</template>
<p>這裡放置彈窗的主要資訊。</p>
<template #footer>
<button @click="confirm()">確定</button>
<button @click="$emit('close')">取消</button>
</template>
</Modal>
範例 2:作用域插槽傳遞資料
<!-- DataTable.vue -->
<template>
<table>
<thead>
<tr>
<th v-for="col in columns" :key="col">{{ col }}</th>
</tr>
</thead>
<tbody>
<tr v-for="row in rows" :key="row.id">
<slot name="cell" :row="row"></slot>
</tr>
</tbody>
</table>
</template>
<script setup>
defineProps({
columns: Array,
rows: Array
})
</script>
<!-- 父層使用 DataTable -->
<DataTable :columns="['姓名', '年齡']" :rows="users">
<template #cell="{ row }">
<td>{{ row.name }}</td>
<td>{{ row.age }}</td>
</template>
</DataTable>
範例 3:多層模板繼承(卡片 + 列表)
<!-- CardWrapper.vue -->
<template>
<div class="card">
<header class="card-header">
<slot name="title"></slot>
</header>
<section class="card-content">
<slot></slot>
</section>
</div>
</template>
<!-- ListInCard.vue -->
<template>
<CardWrapper>
<template #title>
<h3>{{ listTitle }}</h3>
</template>
<ul>
<slot name="items"></slot>
</ul>
</CardWrapper>
</template>
<script setup>
defineProps({ listTitle: String })
</script>
<!-- App.vue -->
<ListInCard listTitle="待辦清單">
<template #items>
<li>購物</li>
<li>寫程式</li>
<li>閱讀</li>
</template>
</ListInCard>
範例 4:動態具名插槽(根據條件切換)
<!-- DynamicSlots.vue -->
<template>
<div>
<slot :name="activeSlot"></slot>
</div>
</template>
<script setup>
import { ref } from 'vue'
const activeSlot = ref('first')
</script>
<!-- 父層 -->
<DynamicSlots>
<template #first>
<p>第一段內容</p>
</template>
<template #second>
<p>第二段內容</p>
</template>
</DynamicSlots>
技巧:透過
:name綁定變數,可在執行時切換顯示不同具名插槽,適合 Tab、Accordion 等 UI。
範例 5:預設插槽 + 具名插槽混用(表單元件)
<!-- FormWrapper.vue -->
<template>
<form @submit.prevent="$emit('submit')">
<slot name="label"></slot>
<slot> <!-- 預設插槽,放輸入框 -->
<input type="text" />
</slot>
<slot name="actions">
<button type="submit">送出</button>
</slot>
</form>
</template>
<!-- 使用 -->
<FormWrapper @submit="handleSubmit">
<template #label>
<label for="email">Email:</label>
</template>
<input id="email" type="email" v-model="email" />
<template #actions>
<button type="reset">重設</button>
<button type="submit">送出</button>
</template>
</FormWrapper>
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方案 |
|---|---|---|
| 插槽名稱拼寫錯誤 | v-slot:header 寫成 v-slot:headr,Vue 會直接渲染預設內容,且不會拋錯。 |
使用 IDE 插件或 TypeScript 讓插槽名稱受限於子組件的 props 定義。 |
| 作用域資料未解構 | v-slot="{ user }" 寫成 v-slot="user",會得到整個物件而非解構,導致模板錯誤。 |
明確解構:v-slot="{ user }";若只需要全部物件,可使用 v-slot="slotProps"。 |
| 過度嵌套插槽 | 多層嵌套導致「插槽穿透」不易追蹤。 | 保持插槽層級在 2~3 層,必要時使用 Provide/Inject 或 Pinia 共享狀態。 |
| 預設內容與具名內容同時存在 | 若同時提供預設內容與具名插槽,預設內容仍會渲染(除非子組件使用 v-if)。 |
在子組件內使用 v-if="$slots.name" 只在插槽被提供時渲染。 |
| 樣式衝突 | 子組件內部樣式會影響插槽內容(特別是全局 CSS)。 | 使用 Scoped CSS 或 CSS Modules,並在插槽外層加上 class 前綴。 |
最佳實踐:
- 命名一致:插槽名稱建議使用 kebab-case(例如
header,footer)或 camelCase,保持全專案一致。 - 提供預設內容:在子組件中為每個具名插槽提供合理的預設,降低父層忘記投射內容的風險。
- 限制插槽數量:一般而言 3~4 個具名插槽 已足夠,大量插槽會讓 API 難以理解。
- 文件化:在組件說明文件中列出所有插槽名稱、作用域資料以及預設行為,配合 Storybook 等工具示範。
- 使用
#簡寫:在大型模板中,#slotName能讓程式碼更乾淨,減少視覺雜訊。
實際應用場景
| 場景 | 為什麼使用具名插槽與 v-slot |
|---|---|
| 彈窗(Modal) | 不同頁面需要自訂標題、內容與操作列,具名插槽讓彈窗元件保持「只負責結構」的職責。 |
| 表單布局(Form Layout) | 表單的欄位、說明文字、提交按鈕常常需要靈活調整,使用具名插槽可讓開發者自行決定每個區塊的排版。 |
| 卡片式列表(Card List) | 卡片內部可能有圖片、標題、描述、操作按鈕等多個區塊,具名插槽讓卡片元件在不同情境下復用。 |
| 資料表格(DataTable) | 表格欄位的渲染方式多樣(文字、圖示、按鈕),透過作用域插槽傳遞每列資料,父層自行決定顯示樣式。 |
| Tab / Accordion | 動態切換顯示的內容可透過 動態具名插槽 (:name="activeTab") 來實現,避免在父層寫大量 v-if。 |
總結
- 具名插槽 與
v-slot為 Vue3 提供了強大的模板繼承能力,使 UI 元件能保持 高內聚、低耦合。 - 透過 作用域插槽,子組件可以將資料直接交給父層決定渲染方式,實現 「資料在子層、樣式在父層」 的最佳分工。
- 在實務開發中,適度使用具名插槽、提供預設內容、做好文件說明,即可讓元件庫更易於維護與擴展。
- 謹記常見陷阱(名稱錯誤、過度嵌套、樣式衝突),遵循 命名一致、插槽數量適中、文件化 的最佳實踐,就能在大型專案中穩定運用模板繼承。
掌握 v-slot 與具名插槽後,你的 Vue3 專案將會變得更具彈性、更易於組合,從而提升開發效率與程式碼品質。祝你在實作的過程中玩得開心,寫出乾淨、可重用的 Vue 元件!