本文 AI 產出,尚未審核

Vue3 元件通信:深入掌握 Scoped Slots(插槽傳遞資料)


簡介

在 Vue3 中,元件通信是開發大型應用時最常遇到的挑戰之一。雖然 propsemitprovide/inject 等方式已能滿足大部分需求,但在 父子元件之間需要靈活地把資料「倒回」給子元件的插槽 時,scoped slots(作用域插槽)提供了更強大的表現力。

透過 scoped slots,父元件可以將內部狀態或方法「暴露」給插槽內容,讓使用者(子元件的插槽使用者)自行決定如何呈現。這不僅提升了元件的 可重用性,也讓 UI 結構與資料邏輯徹底解耦。本文將從概念、語法到實務範例,完整說明如何在 Vue3 中運用 scoped slots。


核心概念

1. 什麼是 Scoped Slot?

普通的 <slot> 只能接受靜態的 HTML 結構,無法取得父元件的任何資料。
Scoped Slot 則允許父元件 向插槽傳遞一組「作用域 (scope)」資料,子元件在插槽內部可以直接使用這些資料,就像在同一個 Vue 實例中一樣。

關鍵點

  • 資料是由 提供插槽的元件(父)傳出,使用插槽的地方(子)接收。
  • 插槽內容仍然屬於子元件的模板,因而可以使用子元件的 datacomputedmethods 等。

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-slotv-bind 可能產生衝突,導致資料被覆寫 把所有要傳遞的資料集中在 v-slot 中,避免在同一個 <slot> 同時使用 v-bind
未設定 key (在 v-for 中使用 slot) Vue 無法正確追蹤項目,導致重繪錯誤 為每個 slot 加上唯一 :key,如 :key="item.id"
在 TypeScript 中未宣告插槽資料型別 IDE 無法提供型別提示,容易寫錯屬性名稱 使用 defineSlots(Vue 3.4+)或在父元件的 v-slot 解構時加上型別註解

最佳實踐總結

  1. 只在需要「自訂渲染」時使用 scoped slot,保持元件 API 的簡潔。
  2. 保持插槽名稱語意化(如 headeritemdefault),提升可讀性。
  3. 對於頻繁使用的作用域資料,考慮使用 defineExpose + ref,讓外部直接存取,而非每次都透過插槽傳遞。
  4. 在大型專案中,為每個插槽建立文件(README 或 Storybook),說明可用的 props、型別與使用範例。

實際應用場景

場景 為何使用 Scoped Slot
資料表格 (Data Table) 父元件負責資料取得、分頁、排序,子元件只負責「每一列」的渲染,讓表格樣式可以在不同頁面自由客製化。
彈性表單 (Dynamic Form) 表單元件提供欄位資料、驗證規則,使用者透過 scoped slot 注入自訂的 UI(如下拉、日期選擇器),同時保有表單的統一驗證機制。
列表與搜尋 (List with Search) 搜尋條件與過濾邏輯在父元件,實際的列表項目樣式交給插槽自行決定,適合多種展示風格(卡片、表格、文字列)。
分頁導航 (Pagination) 分頁元件只負責計算頁碼、觸發事件,外觀(如頁碼按鈕樣式、前後頁文字)交給 scoped slot,讓 UI 團隊能快速調整風格。
多語系或主題切換 使用 scoped slot 把 localetheme 等資訊傳給子元件,子元件根據這些資訊渲染不同的文字或樣式,避免在每個子元件內重複寫 inject

案例說明:假設我們有一個「商品列表」元件 ProductGrid.vue,它會根據 API 回傳的商品資料產生格子。不同的業務需求可能需要「卡片樣式」或「列表樣式」兩種展示方式,透過 scoped slot,我們只需要在 ProductGrid 中提供 product 資料,父元件自行決定渲染方式,完全不需要在 ProductGrid 裡寫條件判斷,達到 單一職責高可重用性


總結

  • Scoped Slot 是 Vue3 中讓父元件「倒回」資料給子元件插槽的強大機制,能夠在保持 資料與 UI 分離 的同時,提供 高度客製化 的渲染彈性。
  • 透過 v-slot 的解構語法,我們可以清楚地看到傳遞的屬性名稱與用途,並且在 具名插槽動態插槽TypeScript 等情境下仍能保持良好的可讀性與型別安全。
  • 使用時要注意 避免過度濫用正確設定 : 綁定提供唯一 key,以及在大型專案中 文件化每個插槽的 API

掌握了 scoped slots 之後,你將能夠更靈活地設計可重用元件、減少重複程式碼,並在團隊協作時提供清晰的介面約定。快把本文的範例搬進你的專案,從小型列表到複雜的資料表格,都能體驗到 插槽傳遞資料 帶來的開發效率提升吧! 🚀