Vue 插槽設計:Slot、Scoped Slot 與 Headless Component
插槽不是單純「把 HTML 傳進子元件」。它真正解決的是:子元件負責邏輯與結構骨架,父元件保留內容與渲染細節的決定權。 中高階面試常用這題判斷你是否能設計可復用、可擴展、又不過度耦合的元件 API。
一句話回答:Vue Slot 是父元件傳給子元件的一段渲染內容,子元件決定插入位置;Scoped Slot 則讓子元件把內部資料暴露給父元件使用,父元件決定如何渲染。插槽適合解決內容組合與渲染客製化,進一步可以形成 Renderless / Headless Component,把狀態邏輯和 UI 樣式拆開。
一、 必考觀念
1. Slot 解決什麼問題?
如果一個 Card 元件所有內容都用 props 控制:
<BaseCard
title="使用者資料"
description="目前登入者資訊"
/>這對簡單文字可以,但如果 title 需要 icon、description 裡有連結、底部要放按鈕,props 會越長越不彈性。
插槽讓父層可以直接傳內容:
<BaseCard>
<template #title>
<UserIcon />
使用者資料
</template>
<p>目前登入者資訊</p>
<template #footer>
<button>編輯</button>
</template>
</BaseCard>子元件負責外框,父元件負責內容。
2. 插槽本質是渲染函式
可以簡化理解成:父元件把一段「未來要渲染的內容」傳給子元件。
子元件在適當位置呼叫它:
<template>
<section class="card">
<header>
<slot name="title" />
</header>
<main>
<slot />
</main>
<footer>
<slot name="footer" />
</footer>
</section>
</template>面試時可以說:
Slot 不是子元件拿到一段 HTML 字串,而是父元件提供渲染內容,子元件決定它出現在哪裡。
3. Props、Emit、Slot 的分工
| API | 解決問題 |
|---|---|
| Props | 傳資料 |
| Emit | 回報事件 |
| Slot | 傳內容與結構 |
| Scoped Slot | 子元件給資料,父元件決定渲染 |
如果只是傳一個字串,用 props。
如果要讓父層客製化一段 UI,用 slot。
如果子元件有資料,但父層要決定怎麼畫,用 scoped slot。
延伸讀:Vue 元件通訊方式有哪些及原理。
二、 預設插槽與具名插槽
1. 預設插槽
子元件:
<template>
<button class="base-button">
<slot />
</button>
</template>父元件:
<BaseButton>
儲存
</BaseButton>預設插槽適合元件只有一個主要內容區域。
2. 具名插槽
子元件:
<template>
<article class="panel">
<header>
<slot name="header" />
</header>
<section>
<slot />
</section>
<footer>
<slot name="footer" />
</footer>
</article>
</template>父元件:
<BasePanel>
<template #header>
<h2>訂單明細</h2>
</template>
<OrderDetail />
<template #footer>
<button>取消</button>
<button>確認</button>
</template>
</BasePanel>具名插槽適合固定 layout 中有多個可替換區域,例如:
- Modal header / body / footer。
- Table header / row / empty。
- Card title / default / actions。
- Layout sidebar / main / toolbar。
3. 插槽預設內容
子元件可以提供 fallback:
<slot name="empty">
<p>目前沒有資料</p>
</slot>如果父元件沒有傳 empty,就顯示預設內容。
這讓元件同時支援快速使用與客製化。
三、 Scoped Slot
1. 為什麼需要 Scoped Slot?
假設有一個列表元件:
<UserList :users="users" />如果 UserList 內部固定渲染使用者,就很難客製化每一列。
Scoped Slot 可以讓子元件提供資料,父元件決定怎麼畫。
子元件:
<template>
<ul>
<li
v-for="user in users"
:key="user.id"
>
<slot
name="item"
:user="user"
:selected="selectedId === user.id"
:select="() => select(user.id)"
/>
</li>
</ul>
</template>父元件:
<UserList :users="users">
<template #item="{ user, selected, select }">
<button
:class="{ active: selected }"
@click="select"
>
{{ user.name }}
</button>
</template>
</UserList>子元件管理列表與選中邏輯,父元件決定每列 UI。
2. Scoped Slot 的資料方向
Scoped Slot 看起來像「子傳父」,但它不是一般事件。
更精準地說:
父元件提供渲染函式
子元件呼叫渲染函式時,把 slot props 傳進去
父元件用這些 slot props 產生內容所以它非常適合「資料在子元件,渲染權在父元件」的場景。
3. 常見使用場景
Scoped Slot 常用在:
- Table row / cell。
- List item。
- Form field。
- Select option。
- Tree node。
- Upload item。
- Dropdown trigger / menu item。
例如 Table:
<DataTable :rows="rows">
<template #cell-status="{ row }">
<StatusBadge :status="row.status" />
</template>
<template #actions="{ row }">
<button @click="edit(row)">編輯</button>
</template>
</DataTable>這樣 Table 處理資料與結構,業務頁面處理欄位渲染。
四、 受控元件與插槽
1. 什麼是受控元件?
受控元件指的是:元件的核心狀態由父層控制,子元件透過 props 接收狀態,透過 emit 回報變更。
例如:
<BaseTabs
:model-value="activeTab"
@update:model-value="activeTab = $event"
/>也可以寫成:
<BaseTabs v-model="activeTab" />延伸讀:v-model 雙向綁定的原理。
2. 受控狀態 + slot 客製 UI
Tabs 元件可以同時提供狀態控制與插槽:
<BaseTabs
v-model="activeTab"
:items="tabs"
>
<template #tab="{ item, active }">
<span :class="{ active }">
{{ item.label }}
</span>
</template>
</BaseTabs>這種 API 很常見:
v-model控制目前狀態。- props 傳入資料。
- scoped slot 客製渲染。
- emit 回報變更。
3. 什麼時候不要用 slot?
如果只有簡單文字:
<BaseButton label="儲存" />可能比 slot 更直覺。
但如果內容可能有 icon、loading、快捷鍵提示:
<BaseButton>
<SaveIcon />
儲存
</BaseButton>slot 會更彈性。
元件 API 設計不是 slot 越多越好,而是看客製化需求是否真的存在。
五、 Renderless / Headless Component
1. Renderless Component 是什麼?
Renderless Component 幾乎不提供 UI,只提供狀態與行為,透過 scoped slot 把資料暴露給父層。
<MousePosition v-slot="{ x, y }">
<p>Mouse: {{ x }}, {{ y }}</p>
</MousePosition>MousePosition 管理事件監聽與座標,父層決定 UI。
2. Headless Component 是什麼?
Headless Component 和 Renderless 概念接近:它提供互動邏輯、狀態、可及性行為,但不綁定具體樣式。
適合:
- Dropdown。
- Dialog。
- Tabs。
- Combobox。
- Select。
- Menu。
例如一個 Headless Select 應該處理:
- open / close。
- keyboard navigation。
- active option。
- selected value。
- ARIA attributes。
- 點外面關閉。
但樣式與 DOM 細節交給使用者客製。
3. Headless 不代表不用管可及性
很多人以為 Headless 只是「不寫樣式」。其實成熟的 Headless 元件最有價值的部分通常是:
- 鍵盤操作。
- focus 管理。
- ARIA。
- 點擊外部關閉。
- 選項高亮與選中。
- 狀態一致性。
也就是它把難寫的互動邏輯封裝起來,把樣式權交給使用者。
六、 插槽設計取捨
1. Slot 太少,元件不彈性
如果 Modal 只能傳 title 和 content:
<BaseModal
title="刪除資料"
content="確定要刪除嗎?"
/>很快會遇到:
- title 需要 icon。
- content 需要表單。
- footer 需要不同按鈕。
- body 需要 loading 或 error。
這時具名插槽更好。
2. Slot 太多,元件 API 變複雜
如果一個元件提供十幾個 slot:
header
title
subtitle
toolbar
left
right
body
beforeList
item
afterList
footer
actions
empty
loading
error使用者會不知道該用哪一個。
這時要思考:
- 是否應該拆子元件?
- 是否應該用 props 控制簡單選項?
- 是否真的需要這麼多擴展點?
- 是否可以提供更清楚的組件組合 API?
3. 插槽會增加父子耦合
Scoped Slot 會讓父元件知道子元件暴露了哪些 slot props。
例如:
<template #item="{ user, selected, select }">這些名稱就是子元件 API 的一部分。未來如果改掉 selected 或 select,父層就會壞。
所以 slot props 要穩定、命名清楚,不要暴露太多內部細節。
七、 追問題庫
Q1:Slot 和 Props 的差異?
Props 傳資料,Slot 傳內容或渲染結構。
如果只是控制顏色、尺寸、狀態,用 props。若父層需要決定一段 UI 長什麼樣,用 slot。
Q2:Scoped Slot 解決什麼問題?
Scoped Slot 解決「資料在子元件,但渲染想交給父元件決定」的問題。
子元件透過 slot props 暴露資料和操作,父元件使用這些資料產生內容。
Q3:Slot 是否會破壞封裝?
會增加擴展彈性,也會增加 API 面積。
好的 slot 只暴露穩定、必要的資料;壞的 slot 會把子元件內部細節都暴露出去,讓父子耦合變高。
Q4:Headless Component 適合什麼場景?
適合互動邏輯複雜、樣式高度客製的元件,例如 Dropdown、Select、Tabs、Dialog。
它提供狀態、事件、鍵盤操作與可及性邏輯,把視覺樣式交給使用者。
Q5:Slot 和 Provide / Inject 怎麼選?
Slot 用來客製渲染內容。
Provide / Inject 用來跨層傳遞上下文。
例如 Form 可以用 Provide / Inject 給 FormItem 傳上下文;FormItem 的 label 或 error 顯示區域則可以用 slot 客製。
八、 實作題
題目:設計一個可客製欄位的 DataTable
需求:
- 接收 rows。
- 提供預設欄位渲染。
- 支援自訂 cell。
- 支援 empty 狀態。
- row 操作由父層決定。
可以這樣設計:
<!-- DataTable.vue -->
<template>
<table>
<tbody v-if="rows.length">
<tr
v-for="row in rows"
:key="row.id"
>
<td>
<slot
name="name"
:row="row"
>
{{ row.name }}
</slot>
</td>
<td>
<slot
name="status"
:row="row"
>
{{ row.status }}
</slot>
</td>
<td>
<slot
name="actions"
:row="row"
/>
</td>
</tr>
</tbody>
<tbody v-else>
<tr>
<td colspan="3">
<slot name="empty">
目前沒有資料
</slot>
</td>
</tr>
</tbody>
</table>
</template>父層使用:
<DataTable :rows="users">
<template #status="{ row }">
<StatusBadge :status="row.status" />
</template>
<template #actions="{ row }">
<button @click="edit(row)">編輯</button>
<button @click="remove(row)">刪除</button>
</template>
</DataTable>面試時可以補充:
row.id要穩定。- slot props 是公開 API,要控制命名。
- 欄位很多時可以改成 column config + cell slot。
- 大型表格還要考慮虛擬列表與渲染性能。
九、 資深視角
1. 插槽是元件 API,不只是語法
每個 slot 名稱、slot props、fallback 都是公開 API。
一旦很多頁面使用,就不能隨便改。
所以設計插槽時要問:
- 哪些區域真的需要客製?
- slot props 是否穩定?
- fallback 是否好用?
- 是否需要同時支援簡單 props 和進階 slot?
2. Headless Component 要控制複雜度
Headless 很強,但也容易過度抽象。
如果只是簡單 button 或 card,不需要做成 headless。
適合 Headless 的場景通常具備:
- 互動狀態多。
- 可及性要求高。
- 樣式需求差異大。
- 多個產品場景會復用同一套行為。
3. 元件設計要保留預設路徑
好的元件應該讓簡單場景簡單,複雜場景可擴展。
例如:
<BaseModal title="刪除資料">
確定要刪除嗎?
</BaseModal>也支援:
<BaseModal>
<template #title>
<WarningIcon />
刪除資料
</template>
<DeleteConfirmForm />
</BaseModal>這比一開始就要求所有人寫很多 slot 更友善。
總結
| 問題 | 回答重點 |
|---|---|
| Slot 是什麼 | 父元件提供渲染內容,子元件決定插入位置 |
| Props vs Slot | Props 傳資料,Slot 傳內容與結構 |
| Scoped Slot | 子元件提供資料,父元件決定如何渲染 |
| 受控元件 | 狀態由父層控制,子層透過 emit 回報變更 |
| Headless Component | 封裝狀態與互動邏輯,不綁定樣式 |
| 資深取捨 | Slot 是公開 API,要控制彈性與耦合 |
一句完整的面試回答可以是:
Vue Slot 用來讓父元件傳入內容,子元件決定內容插入位置;Props 偏資料傳遞,Slot 偏渲染結構客製。Scoped Slot 則是子元件把資料或操作透過 slot props 暴露給父元件,父元件決定怎麼渲染,常用在 Table、List、Select 這類需要客製每一項 UI 的元件。更進一步可以做 Renderless 或 Headless Component,把狀態、鍵盤操作、可及性、選中邏輯封裝起來,但不綁定樣式。設計時要注意 slot 名稱和 slot props 都是公開 API,彈性越高,耦合與維護成本也越高。