跳至主要內容
Skip to content

Vue 插槽設計:Slot、Scoped Slot 與 Headless Component

插槽不是單純「把 HTML 傳進子元件」。它真正解決的是:子元件負責邏輯與結構骨架,父元件保留內容與渲染細節的決定權。 中高階面試常用這題判斷你是否能設計可復用、可擴展、又不過度耦合的元件 API。

一句話回答:Vue Slot 是父元件傳給子元件的一段渲染內容,子元件決定插入位置;Scoped Slot 則讓子元件把內部資料暴露給父元件使用,父元件決定如何渲染。插槽適合解決內容組合與渲染客製化,進一步可以形成 Renderless / Headless Component,把狀態邏輯和 UI 樣式拆開。


一、 必考觀念

1. Slot 解決什麼問題?

如果一個 Card 元件所有內容都用 props 控制:

vue
<BaseCard
  title="使用者資料"
  description="目前登入者資訊"
/>

這對簡單文字可以,但如果 title 需要 icon、description 裡有連結、底部要放按鈕,props 會越長越不彈性。

插槽讓父層可以直接傳內容:

vue
<BaseCard>
  <template #title>
    <UserIcon />
    使用者資料
  </template>

  <p>目前登入者資訊</p>

  <template #footer>
    <button>編輯</button>
  </template>
</BaseCard>

子元件負責外框,父元件負責內容。

2. 插槽本質是渲染函式

可以簡化理解成:父元件把一段「未來要渲染的內容」傳給子元件。

子元件在適當位置呼叫它:

vue
<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. 預設插槽

子元件:

vue
<template>
  <button class="base-button">
    <slot />
  </button>
</template>

父元件:

vue
<BaseButton>
  儲存
</BaseButton>

預設插槽適合元件只有一個主要內容區域。

2. 具名插槽

子元件:

vue
<template>
  <article class="panel">
    <header>
      <slot name="header" />
    </header>

    <section>
      <slot />
    </section>

    <footer>
      <slot name="footer" />
    </footer>
  </article>
</template>

父元件:

vue
<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:

vue
<slot name="empty">
  <p>目前沒有資料</p>
</slot>

如果父元件沒有傳 empty,就顯示預設內容。

這讓元件同時支援快速使用與客製化。


三、 Scoped Slot

1. 為什麼需要 Scoped Slot?

假設有一個列表元件:

vue
<UserList :users="users" />

如果 UserList 內部固定渲染使用者,就很難客製化每一列。

Scoped Slot 可以讓子元件提供資料,父元件決定怎麼畫。

子元件:

vue
<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>

父元件:

vue
<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 看起來像「子傳父」,但它不是一般事件。

更精準地說:

text
父元件提供渲染函式
子元件呼叫渲染函式時,把 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:

vue
<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 回報變更。

例如:

vue
<BaseTabs
  :model-value="activeTab"
  @update:model-value="activeTab = $event"
/>

也可以寫成:

vue
<BaseTabs v-model="activeTab" />

延伸讀:v-model 雙向綁定的原理

2. 受控狀態 + slot 客製 UI

Tabs 元件可以同時提供狀態控制與插槽:

vue
<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?

如果只有簡單文字:

vue
<BaseButton label="儲存" />

可能比 slot 更直覺。

但如果內容可能有 icon、loading、快捷鍵提示:

vue
<BaseButton>
  <SaveIcon />
  儲存
</BaseButton>

slot 會更彈性。

元件 API 設計不是 slot 越多越好,而是看客製化需求是否真的存在。


五、 Renderless / Headless Component

1. Renderless Component 是什麼?

Renderless Component 幾乎不提供 UI,只提供狀態與行為,透過 scoped slot 把資料暴露給父層。

vue
<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:

vue
<BaseModal
  title="刪除資料"
  content="確定要刪除嗎?"
/>

很快會遇到:

  • title 需要 icon。
  • content 需要表單。
  • footer 需要不同按鈕。
  • body 需要 loading 或 error。

這時具名插槽更好。

2. Slot 太多,元件 API 變複雜

如果一個元件提供十幾個 slot:

text
header
title
subtitle
toolbar
left
right
body
beforeList
item
afterList
footer
actions
empty
loading
error

使用者會不知道該用哪一個。

這時要思考:

  • 是否應該拆子元件?
  • 是否應該用 props 控制簡單選項?
  • 是否真的需要這麼多擴展點?
  • 是否可以提供更清楚的組件組合 API?

3. 插槽會增加父子耦合

Scoped Slot 會讓父元件知道子元件暴露了哪些 slot props。

例如:

vue
<template #item="{ user, selected, select }">

這些名稱就是子元件 API 的一部分。未來如果改掉 selectedselect,父層就會壞。

所以 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 操作由父層決定。

可以這樣設計:

vue
<!-- 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>

父層使用:

vue
<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. 元件設計要保留預設路徑

好的元件應該讓簡單場景簡單,複雜場景可擴展。

例如:

vue
<BaseModal title="刪除資料">
  確定要刪除嗎?
</BaseModal>

也支援:

vue
<BaseModal>
  <template #title>
    <WarningIcon />
    刪除資料
  </template>

  <DeleteConfirmForm />
</BaseModal>

這比一開始就要求所有人寫很多 slot 更友善。


總結

問題回答重點
Slot 是什麼父元件提供渲染內容,子元件決定插入位置
Props vs SlotProps 傳資料,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,彈性越高,耦合與維護成本也越高。

延伸閱讀