2024-10-03|閱讀時間 ‧ 約 0 分鐘

EP26 - 插槽

Slots是什麼卡帶插槽嗎?好想玩任天堂唷www
明年出的Switch2我一定要買一台~扯遠了
Slots是可以在子組件中提供一個可供父組件填充的內容區域

插槽內容與插槽插入點 - Slot Content and Outlet​

我們已經學習了組件可以接受 props,而 props 可以是任何類型的 JavaScript 值。但模板內容呢?在某些情況下,我們可能希望將模板片段傳遞給子組件,讓子組件在其模板中渲染該片段。

例如,我們可能有一個 <FancyButton> 組件,支持如下使用方式:

<template>
<FancyButton>
Click me! <!-- 插槽內容 -->
</FancyButton>
</template>

<FancyButton> 的模板看起來像這樣:

<template>
<button class="fancy-btn">
<slot></slot> <!-- 插槽插入點 -->
</button>
</template>

<slot> 元素是一個插槽插入點,表示應該在何處渲染父組件提供的插槽內容。

插槽示意圖

最終渲染的 DOM:

<button class="fancy-btn">Click me!</button>

Try it in the playground

使用插槽後,<FancyButton> 負責渲染外層的 <button>(及其漂亮的樣式),而內部內容則由父組件提供。

另一種理解插槽的方法是將它們與 JavaScript 函數進行比較:

// 父組件傳遞插槽內容
FancyButton('Click me!')

// FancyButton 在其模板中渲染插槽內容
function FancyButton(slotContent) {
return `<button class="fancy-btn">
${slotContent}
</button>`;
}

插槽內容不僅限於文本。它可以是任何有效的模板內容。例如,我們可以傳遞多個元素,甚至其他組件:

<template>
<FancyButton>
<span style="color:red">Click me!</span>
<AwesomeIcon name="plus" />
</FancyButton>
</template>

Try it in the playground

通過使用插槽,我們的 <FancyButton> 更加靈活和可重用。我們現在可以在不同地方使用它並提供不同的內部內容,但都具有相同的漂亮樣式。

Vue 組件的插槽機制受到原生 Web Component <slot> 元素的啟發,但具有我們將在後面看到的附加功能。

渲染範圍 - Render Scope

插槽內容可以訪問父組件的數據範圍,因為它是在父組件中定義的。例如:

<template>
<span>{{ message }}</span>
<FancyButton>{{ message }}</FancyButton>
</template>

在這裡,兩個 {{ message }} 插值會渲染相同的內容。

插槽內容無法訪問子組件的數據。Vue 模板中的表達式只能訪問它所定義的範圍,這與 JavaScript 的詞法作用域一致。換句話說:

父模板中的表達式只能訪問父範圍;子模板中的表達式只能訪問子範圍。

預設內容 - Fallback Content​

有時候在插槽中指定預設內容(即默認內容)是很有用的,這些內容只有在沒有提供插槽內容時才會渲染。例如,在一個 <SubmitButton> 組件中:

<template>
<button type="submit">
<slot></slot>
</button>
</template>

我們可能希望在 <button> 內渲染 "Submit" 文本,如果父組件沒有提供任何插槽內容。為了讓 "Submit" 成為預設內容,我們可以將它放在 <slot> 標籤之間:

<template>
<button type="submit">
<slot>
Submit <!-- 預設內容 -->
</slot>
</button>
</template>

現在當我們在父組件中使用 <SubmitButton>,但沒有提供任何插槽內容時:

<template>
<SubmitButton />
</template>

這將渲染預設內容 "Submit":

<button type="submit">Submit</button>

但如果我們提供了內容:

<template>
<SubmitButton>Save</SubmitButton>
</template>

那麼提供的內容將會被渲染:

<button type="submit">Save</button>

Try it in the playground

命名插槽 - Named Slots

有時在單個組件中擁有多個插槽出口是很有用的。例如,在一個 <BaseLayout> 組件中,其模板如下:

<template>
<div class="container">
<header>
<!-- 我們希望這裡有標頭內容 -->
</header>
<main>
<!-- 我們希望這裡有主要內容 -->
</main>
<footer>
<!-- 我們希望這裡有頁腳內容 -->
</footer>
</div>
</template>

在這些情況下,<slot> 元素有一個特殊的屬性 name,可以用來為不同的插槽分配唯一的 ID,以確定內容應該在哪裡渲染:

<template>
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
</template>

一個沒有 name<slot> 插槽默認被認為是 "default"。

在父組件中使用 <BaseLayout> 時,我們需要一種方式來傳遞多個插槽內容片段,每個片段針對不同的插槽出口。這就是命名插槽的用途。

要傳遞一個命名插槽,我們需要使用 <template> 元素和 v-slot 指令,然後將插槽的名稱作為 v-slot 的參數:

<template>
<BaseLayout>
<template v-slot:header>
<!-- 標頭插槽的內容 -->
</template>
</BaseLayout>
</template>

v-slot 有一個專用的簡寫 #,所以 <template v-slot:header> 可以簡寫為 <template #header>。可以理解為“將此模板片段渲染在子組件的 'header' 插槽中”。

這裡是使用簡寫語法將所有三個插槽內容傳遞給 <BaseLayout> 的代碼:

<template>
<BaseLayout>
<template #header>
<h1>這裡可能是一個頁面標題</h1>
</template>

<template #default>
<p>主要內容的一個段落。</p>
<p>再來一個段落。</p>
</template>

<template #footer>
<p>這裡是一些聯繫信息</p>
</template>
</BaseLayout>
</template>

當一個組件同時接受默認插槽和命名插槽時,所有頂層的非 <template> 節點都會被默認視為默認插槽的內容。因此,上述代碼也可以寫成:

<template>
<BaseLayout>
<template #header>
<h1>這裡可能是一個頁面標題</h1>
</template>

<!-- 默認插槽 -->
<p>主要內容的一個段落。</p>
<p>再來一個段落。</p>

<template #footer>
<p>這裡是一些聯繫信息</p>
</template>
</BaseLayout>
</template>

現在,所有在 <template> 元素內的內容都會被傳遞到相應的插槽中。最終渲染的 HTML 將是:

<div class="container">
<header>
<h1>這裡可能是一個頁面標題</h1>
</header>
<main>
<p>主要內容的一個段落。</p>
<p>再來一個段落。</p>
</main>
<footer>
<p>這裡是一些聯繫信息</p>
</footer>
</div>

Try it in the playground

同樣,使用 JavaScript 函數類比可能有助於更好地理解命名插槽:

// 傳遞多個具有不同名稱的插槽片段
BaseLayout({
header: `...`,
default: `...`,
footer: `...`
})

// <BaseLayout> 在不同的地方渲染它們
function BaseLayout(slots) {
return `<div class="container">
<header>${slots.header}</header>
<main>${slots.default}</main>
<footer>${slots.footer}</footer>
</div>`
}

條件插槽 - Conditional Slots

有時候,你會希望根據插槽是否存在來渲染某些內容。

你可以使用 $slots 屬性結合 v-if 指令來實現這一點。

在下面的例子中,我們定義了一個 Card 組件,其中包含三個條件插槽:標頭、頁腳和默認插槽。當標頭、頁腳或默認插槽存在時,我們希望包裝它們以提供額外的樣式:

<template>
<div class="card">
<div v-if="$slots.header" class="card-header">
<slot name="header" />
</div>

<div v-if="$slots.default" class="card-content">
<slot />
</div>

<div v-if="$slots.footer" class="card-footer">
<slot name="footer" />
</div>
</div>
</template>

Try it in the playground

動態插槽名稱 - Dynamic Slot Names​

動態指令參數也適用於 v-slot,允許定義動態插槽名稱:

<base-layout>
<template v-slot:[dynamicSlotName]>
...
</template>

<!-- 簡寫 -->
<template #[dynamicSlotName]>
...
</template>
</base-layout>

請注意,表達式受動態指令參數的語法限制

作用域插槽 - Scoped Slots

正如在「渲染範圍」中所討論的,插槽內容無法訪問子元件中的狀態。

然而,在某些情況下,我們希望插槽的內容可以同時使用來自父元件和子元件的數據。為了實現這一點,我們需要一種方式讓子元件在渲染插槽時將數據傳遞給插槽。

事實上,我們可以做到這一點 - 我們可以將屬性傳遞給插槽插座,就像將 props 傳遞給元件一樣:

<!-- <MyComponent> 模板 -->
<div>
<slot :text="greetingMessage" :count="1"></slot>
</div>

在單一默認插槽和命名插槽中接收插槽 props 的方式略有不同。我們首先展示如何在單一默認插槽中接收 props,通過在子元件標籤上直接使用 v-slot

<MyComponent v-slot="slotProps">
{{ slotProps.text }} {{ slotProps.count }}
</MyComponent>

Try it in the playground

插槽 props 由子元件傳遞,可以作為對應的 v-slot 指令的值,並在插槽內的表達式中訪問。

你可以將作用域插槽想像成傳遞給子元件的一個函數。子元件隨後調用它,並將 props 作為參數傳遞:

MyComponent({
// 傳遞默認插槽,但作為函數
default: (slotProps) => {
return `${slotProps.text} ${slotProps.count}`
}
})

function MyComponent(slots) {
const greetingMessage = 'hello'
return `<div>${
// 用 props 調用插槽函數!
slots.default({ text: greetingMessage, count: 1 })
}</div>`
}

事實上,這與作用域插槽的編譯方式以及如何在手動渲染函數中使用作用域插槽非常接近。

注意 v-slot="slotProps" 與插槽函數簽名的匹配方式。就像函數參數一樣,我們可以在 v-slot 中使用解構:

<MyComponent v-slot="{ text, count }">
{{ text }} {{ count }}
</MyComponent>

命名作用域插槽 - Named Scoped Slots

命名作用域插槽的工作方式類似 - 插槽 props 可以作為 v-slot 指令的值訪問:v-slot:name="slotProps"。使用簡寫時,它看起來像這樣:

<MyComponent>
<template #header="headerProps">
{{ headerProps }}
</template>

<template #default="defaultProps">
{{ defaultProps }}
</template>

<template #footer="footerProps">
{{ footerProps }}
</template>
</MyComponent>

向命名插槽傳遞 props:

<slot name="header" message="hello"></slot>

注意插槽的名稱不會包含在 props 中,因為它是保留的 - 因此結果 headerProps 將會是 { message: 'hello' }

如果你將命名插槽與默認作用域插槽混合使用,則需要為默認插槽使用顯式的 <template> 標籤。試圖將 v-slot 指令直接放在元件上將導致編譯錯誤。這是為了避免默認插槽的 props 範圍的任何模糊。例如:

<!-- <MyComponent> 模板 -->
<div>
<slot :message="hello"></slot>
<slot name="footer" />
</div>

<!-- 此模板不會編譯 -->
<MyComponent v-slot="{ message }">
<p>{{ message }}</p>
<template #footer>
<!-- message 屬於默認插槽,在這裡不可用 -->
<p>{{ message }}</p>
</template>
</MyComponent>

使用顯式的 <template> 標籤為默認插槽有助於明確表示 message prop 在其他插槽中不可用:

<MyComponent>
<!-- 使用顯式默認插槽 -->
<template #default="{ message }">
<p>{{ message }}</p>
</template>

<template #footer>
<p>這裡是一些聯繫信息</p>
</template>
</MyComponent>

Fancy List 範例

你可能會想知道作用域插槽的好用例是什麼。這裡有一個例子:假設我們有一個 <FancyList> 元件,它渲染一個項目列表 - 它可能封裝了加載遠程數據、使用數據顯示列表甚至高級功能(如分頁或無限滾動)的邏輯。然而,我們希望它在每個項目的外觀上更加靈活,並將每個項目的樣式留給消費它的父元件。因此,希望的用法可能如下所示:

<FancyList :api-url="url" :per-page="10">
<template #item="{ body, username, likes }">
<div class="item">
<p>{{ body }}</p>
<p>by {{ username }} | {{ likes }} likes</p>
</div>
</template>
</FancyList>

<FancyList> 內部,我們可以使用不同的項目數據多次渲染相同的 <slot>(注意我們使用 v-bind 將一個物件作為插槽 props 傳遞):

<ul>
<li v-for="item in items">
<slot name="item" v-bind="item"></slot>
</li>
</ul>

Try it in the playground

無渲染元件

我們上面討論的 <FancyList> 用例封裝了可重用的邏輯(數據抓取、分頁等)和視覺輸出,同時通過作用域插槽將部分視覺輸出委託給消費元件。

如果我們進一步推進這個概念,我們可以創建僅封裝邏輯且不自行渲染任何內容的元件 - 視覺輸出完全通過作用域插槽委託給消費元件。我們稱這種類型的元件為無渲染元件。

一個無渲染元件的例子可能是封裝當前鼠標位置跟蹤邏輯的元件:

<MouseTracker v-slot="{ x, y }">
Mouse is at: {{ x }}, {{ y }}
</MouseTracker>

Try it in the playground

雖然這是一種有趣的模式,但大多數可以通過無渲染元件實現的功能都可以通過 Composition API 以更高效的方式實現,無需額外的元件嵌套開銷。稍後,我們將看到如何將相同的鼠標跟蹤功能實現為可組合的

話雖如此,作用域插槽在我們需要同時封裝邏輯和組合視覺輸出時仍然非常有用,例如在 <FancyList> 範例中。

這篇意外的蠻實用的耶!雖然內容有點多~
大家有空要多練習唷~
原來原生Javascript也有Slot可以用 www


分享至
成為作者繼續創作的動力吧!
© 2024 vocus All rights reserved.