本文 AI 產出,尚未審核

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/InjectPinia 共享狀態。
預設內容與具名內容同時存在 若同時提供預設內容與具名插槽,預設內容仍會渲染(除非子組件使用 v-if)。 在子組件內使用 v-if="$slots.name" 只在插槽被提供時渲染。
樣式衝突 子組件內部樣式會影響插槽內容(特別是全局 CSS)。 使用 Scoped CSSCSS Modules,並在插槽外層加上 class 前綴

最佳實踐

  1. 命名一致:插槽名稱建議使用 kebab-case(例如 header, footer)或 camelCase,保持全專案一致。
  2. 提供預設內容:在子組件中為每個具名插槽提供合理的預設,降低父層忘記投射內容的風險。
  3. 限制插槽數量:一般而言 3~4 個具名插槽 已足夠,大量插槽會讓 API 難以理解。
  4. 文件化:在組件說明文件中列出所有插槽名稱、作用域資料以及預設行為,配合 Storybook 等工具示範。
  5. 使用 # 簡寫:在大型模板中,#slotName 能讓程式碼更乾淨,減少視覺雜訊。

實際應用場景

場景 為什麼使用具名插槽與 v-slot
彈窗(Modal) 不同頁面需要自訂標題、內容與操作列,具名插槽讓彈窗元件保持「只負責結構」的職責。
表單布局(Form Layout) 表單的欄位、說明文字、提交按鈕常常需要靈活調整,使用具名插槽可讓開發者自行決定每個區塊的排版。
卡片式列表(Card List) 卡片內部可能有圖片、標題、描述、操作按鈕等多個區塊,具名插槽讓卡片元件在不同情境下復用。
資料表格(DataTable) 表格欄位的渲染方式多樣(文字、圖示、按鈕),透過作用域插槽傳遞每列資料,父層自行決定顯示樣式。
Tab / Accordion 動態切換顯示的內容可透過 動態具名插槽 (:name="activeTab") 來實現,避免在父層寫大量 v-if

總結

  • 具名插槽v-slot 為 Vue3 提供了強大的模板繼承能力,使 UI 元件能保持 高內聚、低耦合
  • 透過 作用域插槽,子組件可以將資料直接交給父層決定渲染方式,實現 「資料在子層、樣式在父層」 的最佳分工。
  • 在實務開發中,適度使用具名插槽、提供預設內容、做好文件說明,即可讓元件庫更易於維護與擴展。
  • 謹記常見陷阱(名稱錯誤、過度嵌套、樣式衝突),遵循 命名一致、插槽數量適中、文件化 的最佳實踐,就能在大型專案中穩定運用模板繼承。

掌握 v-slot 與具名插槽後,你的 Vue3 專案將會變得更具彈性、更易於組合,從而提升開發效率與程式碼品質。祝你在實作的過程中玩得開心,寫出乾淨、可重用的 Vue 元件!