Vue3 元件通信:深入掌握 Scoped Slots(插槽傳遞資料)
簡介
在 Vue3 中,元件通信是開發大型應用時最常遇到的挑戰之一。雖然 props、emit、provide/inject 等方式已能滿足大部分需求,但在 父子元件之間需要靈活地把資料「倒回」給子元件的插槽 時,scoped slots(作用域插槽)提供了更強大的表現力。
透過 scoped slots,父元件可以將內部狀態或方法「暴露」給插槽內容,讓使用者(子元件的插槽使用者)自行決定如何呈現。這不僅提升了元件的 可重用性,也讓 UI 結構與資料邏輯徹底解耦。本文將從概念、語法到實務範例,完整說明如何在 Vue3 中運用 scoped slots。
核心概念
1. 什麼是 Scoped Slot?
普通的 <slot> 只能接受靜態的 HTML 結構,無法取得父元件的任何資料。
Scoped Slot 則允許父元件 向插槽傳遞一組「作用域 (scope)」資料,子元件在插槽內部可以直接使用這些資料,就像在同一個 Vue 實例中一樣。
關鍵點:
- 資料是由 提供插槽的元件(父)傳出,使用插槽的地方(子)接收。
- 插槽內容仍然屬於子元件的模板,因而可以使用子元件的
data、computed、methods等。
2. 基本語法
<!-- 父元件:定義作用域插槽 -->
<ChildComponent v-slot="{ item, index }">
<!-- 這裡可以直接使用 item、index -->
<li>{{ index }} - {{ item.name }}</li>
</ChildComponent>
v-slot後面接 解構的變數(或單一變數),對應父元件在<slot>標籤上:prop所傳出的屬性。- 若只傳遞單一屬性,可簡寫為
v-slot="slotProps",然後在模板中使用slotProps.xxx。
3. 在子元件中宣告作用域插槽
<!-- ChildComponent.vue -->
<template>
<ul>
<!-- 透過 v-for 產生多筆資料,並把每筆資料傳給插槽 -->
<slot v-for="(item, index) in list"
:item="item"
:index="index"
:key="item.id">
<!-- 預設內容(若父元件未提供插槽) -->
<li>{{ item.name }}</li>
</slot>
</ul>
</template>
<script setup>
import { ref } from 'vue'
const list = ref([
{ id: 1, name: 'Apple' },
{ id: 2, name: 'Banana' },
{ id: 3, name: 'Cherry' }
])
</script>
:item="item"、:index="index"就是 向外部暴露的屬性,父元件在v-slot中解構即可取得。
程式碼範例
以下提供 五個實用範例,從最基礎到進階應用,幫助你快速上手。
範例 1️⃣ 基本的 Scoped Slot
<!-- Parent.vue -->
<template>
<ItemList v-slot="{ item }">
<!-- 只想改變文字顏色 -->
<li :style="{ color: item.id % 2 ? 'tomato' : 'steelblue' }">
{{ item.name }}
</li>
</ItemList>
</template>
<script setup>
import ItemList from './ItemList.vue'
</script>
說明:
ItemList只提供item,父元件自行決定<li>的樣式,展示了「資料與樣式分離」的好處。
範例 2️⃣ 同時傳遞多個屬性
<!-- Parent.vue -->
<template>
<PaginatedTable v-slot="{ row, rowIndex, columns }">
<tr :class="{ highlighted: rowIndex === selected }">
<td v-for="col in columns" :key="col.key">
{{ row[col.key] }}
</td>
</tr>
</PaginatedTable>
</template>
<script setup>
import PaginatedTable from './PaginatedTable.vue'
import { ref } from 'vue'
const selected = ref(2) // 假設要高亮第 3 筆
</script>
說明:
PaginatedTable內部包含分頁、欄位設定等邏輯,父元件只負責「如何渲染每一列」,大幅提升可重用性。
範例 3️⃣ 使用具名插槽 (named scoped slot)
<!-- Card.vue -->
<template>
<div class="card">
<header class="card-header">
<slot name="header" :title="title"></slot>
</header>
<section class="card-body">
<slot></slot> <!-- 預設插槽 -->
</section>
<footer class="card-footer">
<slot name="footer" :actions="actions"></slot>
</footer>
</div>
</template>
<script setup>
defineProps({
title: String,
actions: Array
})
</script>
<!-- 使用 Card.vue -->
<template>
<Card :title="pageTitle" :actions="pageActions">
<!-- 具名作用域插槽 -->
<template #header="{ title }">
<h2>{{ title }}</h2>
</template>
<p>這裡是卡片的主要內容。</p>
<template #footer="{ actions }">
<button v-for="a in actions" :key="a.id" @click="a.handler">
{{ a.label }}
</button>
</template>
</Card>
</template>
<script setup>
import Card from './Card.vue'
const pageTitle = '使用者資訊'
const pageActions = [
{ id: 1, label: '編輯', handler: () => console.log('edit') },
{ id: 2, label: '刪除', handler: () => console.log('delete') }
]
</script>
說明:具名作用域插槽讓卡片的「標題」與「操作列」分別接受不同的資料,提升可讀性與維護性。
範例 4️⃣ 搭配 <script setup> 的型別推斷 (TypeScript)
<!-- ListWithSearch.vue -->
<template>
<div>
<input v-model="query" placeholder="搜尋…" />
<ul>
<slot v-for="item in filtered" :key="item.id" :item="item"></slot>
</ul>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
interface Fruit {
id: number
name: string
}
const items = ref<Fruit[]>([
{ id: 1, name: 'Apple' },
{ id: 2, name: 'Banana' },
{ id: 3, name: 'Cherry' }
])
const query = ref('')
const filtered = computed(() =>
items.value.filter(i => i.name.toLowerCase().includes(query.value.toLowerCase()))
)
</script>
<!-- Parent 使用 TypeScript -->
<template>
<ListWithSearch v-slot="{ item }">
<li>{{ item.id }} - {{ item.name }}</li>
</ListWithSearch>
</template>
<script setup lang="ts">
import ListWithSearch from './ListWithSearch.vue'
</script>
說明:使用
lang="ts"時,v-slot的型別會自動推斷為Fruit,IDE 能提供完整的自動完成與檢查。
範例 5️⃣ 動態插槽名稱 + Scoped Slot(高階技巧)
<!-- DynamicTabs.vue -->
<template>
<div class="tabs">
<nav>
<button v-for="tab in tabs" :key="tab.name"
@click="active = tab.name"
:class="{ active: active === tab.name }">
{{ tab.label }}
</button>
</nav>
<section>
<!-- 動態具名作用域插槽 -->
<slot :name="active" :data="tabData[active]"></slot>
</section>
</div>
</template>
<script setup>
import { ref } from 'vue'
const tabs = [
{ name: 'info', label: '資訊' },
{ name: 'settings', label: '設定' }
]
const active = ref('info')
const tabData = {
info: { text: '這是資訊頁面' },
settings: { text: '這是設定頁面' }
}
</script>
<!-- 父元件使用 DynamicTabs -->
<template>
<DynamicTabs>
<template #info="{ data }">
<p>資訊內容:{{ data.text }}</p>
</template>
<template #settings="{ data }">
<p>設定內容:{{ data.text }}</p>
</template>
</DynamicTabs>
</template>
<script setup>
import DynamicTabs from './DynamicTabs.vue'
</script>
說明:透過 動態具名插槽,
DynamicTabs可以根據當前選中的 tab 自動切換插槽內容,且仍保有作用域資料的傳遞。
常見陷阱與最佳實踐
| 陷阱 | 可能的問題 | 解決方式 / 最佳實踐 |
|---|---|---|
忘記在子元件 <slot> 上加 : 前綴 |
傳遞的屬性會被視為字串,子元件無法取得正確資料 | 必須寫成 :prop="value",或使用 v-bind |
| 插槽名稱拼寫錯誤 | 父元件找不到對應的具名插槽,會退回預設內容 | 使用 IDE 的自動完成或在父子元件間統一常量(如 const SLOT_HEADER = 'header') |
| 過度使用 Scoped Slot | 產生過多層級的插槽會讓模板難以閱讀 | 僅在 「資料需要由父元件決定呈現方式」 時使用,若僅是簡單資料傳遞,使用 props 即可 |
在同一插槽中同時使用 v-slot 與 v-bind |
可能產生衝突,導致資料被覆寫 | 把所有要傳遞的資料集中在 v-slot 中,避免在同一個 <slot> 同時使用 v-bind |
未設定 key (在 v-for 中使用 slot) |
Vue 無法正確追蹤項目,導致重繪錯誤 | 為每個 slot 加上唯一 :key,如 :key="item.id" |
| 在 TypeScript 中未宣告插槽資料型別 | IDE 無法提供型別提示,容易寫錯屬性名稱 | 使用 defineSlots(Vue 3.4+)或在父元件的 v-slot 解構時加上型別註解 |
最佳實踐總結:
- 只在需要「自訂渲染」時使用 scoped slot,保持元件 API 的簡潔。
- 保持插槽名稱語意化(如
header、item、default),提升可讀性。 - 對於頻繁使用的作用域資料,考慮使用
defineExpose+ref,讓外部直接存取,而非每次都透過插槽傳遞。 - 在大型專案中,為每個插槽建立文件(README 或 Storybook),說明可用的 props、型別與使用範例。
實際應用場景
| 場景 | 為何使用 Scoped Slot |
|---|---|
| 資料表格 (Data Table) | 父元件負責資料取得、分頁、排序,子元件只負責「每一列」的渲染,讓表格樣式可以在不同頁面自由客製化。 |
| 彈性表單 (Dynamic Form) | 表單元件提供欄位資料、驗證規則,使用者透過 scoped slot 注入自訂的 UI(如下拉、日期選擇器),同時保有表單的統一驗證機制。 |
| 列表與搜尋 (List with Search) | 搜尋條件與過濾邏輯在父元件,實際的列表項目樣式交給插槽自行決定,適合多種展示風格(卡片、表格、文字列)。 |
| 分頁導航 (Pagination) | 分頁元件只負責計算頁碼、觸發事件,外觀(如頁碼按鈕樣式、前後頁文字)交給 scoped slot,讓 UI 團隊能快速調整風格。 |
| 多語系或主題切換 | 使用 scoped slot 把 locale、theme 等資訊傳給子元件,子元件根據這些資訊渲染不同的文字或樣式,避免在每個子元件內重複寫 inject。 |
案例說明:假設我們有一個「商品列表」元件
ProductGrid.vue,它會根據 API 回傳的商品資料產生格子。不同的業務需求可能需要「卡片樣式」或「列表樣式」兩種展示方式,透過 scoped slot,我們只需要在ProductGrid中提供product資料,父元件自行決定渲染方式,完全不需要在ProductGrid裡寫條件判斷,達到 單一職責 與 高可重用性。
總結
- Scoped Slot 是 Vue3 中讓父元件「倒回」資料給子元件插槽的強大機制,能夠在保持 資料與 UI 分離 的同時,提供 高度客製化 的渲染彈性。
- 透過
v-slot的解構語法,我們可以清楚地看到傳遞的屬性名稱與用途,並且在 具名插槽、動態插槽、TypeScript 等情境下仍能保持良好的可讀性與型別安全。 - 使用時要注意 避免過度濫用、正確設定
:綁定、提供唯一key,以及在大型專案中 文件化每個插槽的 API。
掌握了 scoped slots 之後,你將能夠更靈活地設計可重用元件、減少重複程式碼,並在團隊協作時提供清晰的介面約定。快把本文的範例搬進你的專案,從小型列表到複雜的資料表格,都能體驗到 插槽傳遞資料 帶來的開發效率提升吧! 🚀