做(zuò)自(zì)由與創造的先行(xíng)者

插槽 Slots

Vue.js中文手冊

此章(zhāng)節假設你已經看(kàn)過了(le)組件基礎。若你還不了(le)解組件是什麽,請(qǐng)先閱讀該章(zhāng)節。

插槽內(nèi)容與出口 ​

在之前的章(zhāng)節中,我們已經了(le)解到組件能(néng)夠接收任意類型的 JavaScript 值作(zuò)為(wèi) props,但(dàn)組件要(yào)如(rú)何接收模闆內(nèi)容呢?在某些場景中,我們可(kě)能(néng)想要(yào)為(wèi)子組件傳遞一(yī)些模闆片段,讓子組件在它們的組件中渲染這(zhè)些片段。

舉例來說,這(zhè)裏有一(yī)個 <FancyButton> 組件,可(kě)以像這(zhè)樣使用:

template

<FancyButton>

Click me! <!-- 插槽內(nèi)容 -->

</FancyButton>

而 <FancyButton> 的模闆是這(zhè)樣的:

template

<button class="fancy-btn">

<slot></slot> <!-- 插槽出口 -->

</button>

<slot> 元素是一(yī)個插槽出口 (slot outlet),标示了(le)父元素提供的插槽內(nèi)容 (slot content) 将在哪裏被渲染。

最終渲染出的 DOM 是這(zhè)樣:

html

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

通(tōng)過使用插槽,<FancyButton> 僅負責渲染外(wài)層的 <button> (以及相應的樣式),而其內(nèi)部的內(nèi)容由父組件提供。

理解插槽的另一(yī)種方式是和(hé)下(xià)面的 JavaScript 函數(shù)作(zuò)類比,其概念是類似的:

js

// 父元素傳入插槽內(nèi)容

FancyButton('Click me!')

// FancyButton 在自(zì)己的模闆中渲染插槽內(nèi)容

function FancyButton(slotContent) {

return `<button class="fancy-btn">

${slotContent}

</button>`

}

插槽內(nèi)容可(kě)以是任意合法的模闆內(nèi)容,不局限于文本。例如(rú)我們可(kě)以傳入多個元素,甚至是組件:

template

<FancyButton>

<span style="color:red">Click me!</span>

<AwesomeIcon name="plus" />

</FancyButton>

通(tōng)過使用插槽,<FancyButton> 組件更加靈活和(hé)具有可(kě)複用性。現在組件可(kě)以用在不同的地(dì)方渲染各異的內(nèi)容,但(dàn)同時(shí)還保證都(dōu)具有相同的樣式。

Vue 組件的插槽機制是受原生 Web Component <slot> 元素的啓發而誕生,同時(shí)還做(zuò)了(le)一(yī)些功能(néng)拓展,這(zhè)些拓展的功能(néng)我們後面會學習到。

渲染作(zuò)用域 ​

插槽內(nèi)容可(kě)以訪問(wèn)到父組件的數(shù)據作(zuò)用域,因為(wèi)插槽內(nèi)容本身是在父組件模闆中定義的。舉例來說:

template

<span>{{ message }}</span>

<FancyButton>{{ message }}</FancyButton>

這(zhè)裏的兩個 {{ message }} 插值表達式渲染的內(nèi)容都(dōu)是一(yī)樣的。

插槽內(nèi)容無法訪問(wèn)子組件的數(shù)據。Vue 模闆中的表達式隻能(néng)訪問(wèn)其定義時(shí)所處的作(zuò)用域,這(zhè)和(hé) JavaScript 的詞法作(zuò)用域規則是一(yī)緻的。換言之:

父組件模闆中的表達式隻能(néng)訪問(wèn)父組件的作(zuò)用域;子組件模闆中的表達式隻能(néng)訪問(wèn)子組件的作(zuò)用域。

默認內(nèi)容 ​

在外(wài)部沒有提供任何內(nèi)容的情況下(xià),可(kě)以為(wèi)插槽指定默認內(nèi)容。比如(rú)有這(zhè)樣一(yī)個 <SubmitButton> 組件:

template

<button type="submit">

<slot></slot>

</button>

如(rú)果我們想在父組件沒有提供任何插槽內(nèi)容時(shí)在 <button> 內(nèi)渲染“Submit”,隻需要(yào)将“Submit”寫在 <slot> 标簽之間(jiān)來作(zuò)為(wèi)默認內(nèi)容:

template

<button type="submit">

<slot>

Submit <!-- 默認內(nèi)容 -->

</slot>

</button>

現在,當我們在父組件中使用 <SubmitButton> 且沒有提供任何插槽內(nèi)容時(shí):

template

<SubmitButton />

“Submit”将會被作(zuò)為(wèi)默認內(nèi)容渲染:

html

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

但(dàn)如(rú)果我們提供了(le)插槽內(nèi)容:

template

<SubmitButton>Save</SubmitButton>

那(nà)麽被顯式提供的內(nèi)容會取代默認內(nèi)容:

html

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

具名插槽 ​

有時(shí)在一(yī)個組件中包含多個插槽出口是很(hěn)有用的。舉例來說,在一(yī)個 <BaseLayout> 組件中,有如(rú)下(xià)模闆:

template

<div class="container">

<header>

<!-- 标題內(nèi)容放這(zhè)裏 -->

</header>

<main>

<!-- 主要(yào)內(nèi)容放這(zhè)裏 -->

</main>

<footer>

<!-- 底部內(nèi)容放這(zhè)裏 -->

</footer>

</div>

對于這(zhè)種場景,<slot> 元素可(kě)以有一(yī)個特殊的 attribute name,用來給各個插槽分配唯一(yī)的 ID,以确定每一(yī)處要(yào)渲染的內(nèi)容:

template

<div class="container">

<header>

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

</header>

<main>

<slot></slot>

</main>

<footer>

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

</footer>

</div>

這(zhè)類帶 name 的插槽被稱為(wèi)具名插槽 (named slots)。沒有提供 name 的 <slot> 出口會隐式地(dì)命名為(wèi)“default”。

在父組件中使用 <BaseLayout> 時(shí),我們需要(yào)一(yī)種方式将多個插槽內(nèi)容傳入到各自(zì)目标插槽的出口。此時(shí)就需要(yào)用到具名插槽了(le):

要(yào)為(wèi)具名插槽傳入內(nèi)容,我們需要(yào)使用一(yī)個含 v-slot 指令的 <template> 元素,并将目标插槽的名字傳給該指令:

template

<BaseLayout>

<template v-slot:header>

<!-- header 插槽的內(nèi)容放這(zhè)裏 -->

</template>

</BaseLayout>

v-slot 有對應的簡寫 #,因此 <template v-slot:header> 可(kě)以簡寫為(wèi) <template #header>。其意思就是“将這(zhè)部分模闆片段傳入子組件的 header 插槽中”。

下(xià)面我們給出完整的、向 <BaseLayout> 傳遞插槽內(nèi)容的代碼,指令均使用的是縮寫形式:

template

<BaseLayout>

<template #header>

<h1>Here might be a page title</h1>

</template>

<template #default>

<p>A paragraph for the main content.</p>

<p>And another one.</p>

</template>

<template #footer>

<p>Here's some contact info</p>

</template>

</BaseLayout>

當一(yī)個組件同時(shí)接收默認插槽和(hé)具名插槽時(shí),所有位于頂級的非 <template> 節點都(dōu)被隐式地(dì)視(shì)為(wèi)默認插槽的內(nèi)容。所以上(shàng)面也可(kě)以寫成:

template

<BaseLayout>

<template #header>

<h1>Here might be a page title</h1>

</template>

<!-- 隐式的默認插槽 -->

<p>A paragraph for the main content.</p>

<p>And another one.</p>

<template #footer>

<p>Here's some contact info</p>

</template>

</BaseLayout>

現在 <template> 元素中的所有內(nèi)容都(dōu)将被傳遞到相應的插槽。最終渲染出的 HTML 如(rú)下(xià):

html

<div class="container">

<header>

<h1>Here might be a page title</h1>

</header>

<main>

<p>A paragraph for the main content.</p>

<p>And another one.</p>

</main>

<footer>

<p>Here's some contact info</p>

</footer>

</div>

使用 JavaScript 函數(shù)來類比可(kě)能(néng)更有助于你來理解具名插槽:

js

// 傳入不同的內(nèi)容給不同名字的插槽

BaseLayout({

header: `...`,

default: `...`,

footer: `...`

})

// <BaseLayout> 渲染插槽內(nèi)容到對應位置

function BaseLayout(slots) {

return `<div class="container">

<header>${slots.header}</header>

<main>${slots.default}</main>

<footer>${slots.footer}</footer>

</div>`

}

動态插槽名 ​

動态指令參數(shù)在 v-slot 上(shàng)也是有效的,即可(kě)以定義下(xià)面這(zhè)樣的動态插槽名:

template

<base-layout>

<template v-slot:[dynamicSlotName]>

...

</template>

<!-- 縮寫為(wèi) -->

<template #[dynamicSlotName]>

...

</template>

</base-layout>

注意這(zhè)裏的表達式和(hé)動态指令參數(shù)受相同的語法限制。

作(zuò)用域插槽 ​

在上(shàng)面的渲染作(zuò)用域中我們讨論到,插槽的內(nèi)容無法訪問(wèn)到子組件的狀态。

然而在某些場景下(xià)插槽的內(nèi)容可(kě)能(néng)想要(yào)同時(shí)使用父組件域內(nèi)和(hé)子組件域內(nèi)的數(shù)據。要(yào)做(zuò)到這(zhè)一(yī)點,我們需要(yào)一(yī)種方法來讓子組件在渲染時(shí)将一(yī)部分數(shù)據提供給插槽。

我們也确實有辦法這(zhè)麽做(zuò)!可(kě)以像對組件傳遞 props 那(nà)樣,向一(yī)個插槽的出口上(shàng)傳遞 attributes:

template

<!-- <MyComponent> 的模闆 -->

<div>

<slot :text="greetingMessage" :count="1"></slot>

</div>

當需要(yào)接收插槽 props 時(shí),默認插槽和(hé)具名插槽的使用方式有一(yī)些小區(qū)别。下(xià)面我們将先展示默認插槽如(rú)何接受 props,通(tōng)過子組件标簽上(shàng)的 v-slot 指令,直接接收到了(le)一(yī)個插槽 props 對象:

template

<MyComponent v-slot="slotProps">

{{ slotProps.text }} {{ slotProps.count }}

</MyComponent>

子組件傳入插槽的 props 作(zuò)為(wèi)了(le) v-slot 指令的值,可(kě)以在插槽內(nèi)的表達式中訪問(wèn)。

你可(kě)以将作(zuò)用域插槽類比為(wèi)一(yī)個傳入子組件的函數(shù)。子組件會将相應的 props 作(zuò)為(wèi)參數(shù)傳給它:

js

MyComponent({

// 類比默認插槽,将其想成一(yī)個函數(shù)

default: (slotProps) => {

return `${slotProps.text} ${slotProps.count}`

}

})

function MyComponent(slots) {

const greetingMessage = 'hello'

return `<div>${

// 在插槽函數(shù)調用時(shí)傳入 props

slots.default({ text: greetingMessage, count: 1 })

}</div>`

}

實際上(shàng),這(zhè)已經和(hé)作(zuò)用域插槽的最終代碼編譯結果、以及手動編寫渲染函數(shù)時(shí)使用作(zuò)用域插槽的方式非常類似了(le)。

v-slot="slotProps" 可(kě)以類比這(zhè)裏的函數(shù)簽名,和(hé)函數(shù)的參數(shù)類似,我們也可(kě)以在 v-slot 中使用解構:

template

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

{{ text }} {{ count }}

</MyComponent>

具名作(zuò)用域插槽 ​

具名作(zuò)用域插槽的工(gōng)作(zuò)方式也是類似的,插槽 props 可(kě)以作(zuò)為(wèi) v-slot 指令的值被訪問(wèn)到:v-slot:name="slotProps"。當使用縮寫時(shí)是這(zhè)樣:

template

<MyComponent>

<template #header="headerProps">

{{ headerProps }}

</template>

<template #default="defaultProps">

{{ defaultProps }}

</template>

<template #footer="footerProps">

{{ footerProps }}

</template>

</MyComponent>

向具名插槽中傳入 props:

template

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

注意插槽上(shàng)的 name 是一(yī)個 Vue 特别保留的 attribute,不會作(zuò)為(wèi) props 傳遞給插槽。因此最終 headerProps 的結果是 { message: 'hello' }。

如(rú)果你混用了(le)具名插槽與默認插槽,則需要(yào)為(wèi)默認插槽使用顯式的 <template> 标簽。嘗試直接為(wèi)組件添加 v-slot 指令将導緻編譯錯誤。這(zhè)是為(wèi)了(le)避免因默認插槽的 props 的作(zuò)用域而困惑。舉例:

template

<!-- 該模闆無法編譯 -->

<template>

<MyComponent v-slot="{ message }">

<p>{{ message }}</p>

<template #footer>

<!-- message 屬于默認插槽,此處不可(kě)用 -->

<p>{{ message }}</p>

</template>

</MyComponent>

</template>

為(wèi)默認插槽使用顯式的 <template> 标簽有助于更清晰地(dì)指出 message 屬性在其他(tā)插槽中不可(kě)用:

template

<template>

<MyComponent>

<!-- 使用顯式的默認插槽 -->

<template #default="{ message }">

<p>{{ message }}</p>

</template>

<template #footer>

<p>Here's some contact info</p>

</template>

</MyComponent>

</template>

高級列表組件示例 ​

你可(kě)能(néng)想問(wèn)什麽樣的場景才适合用到作(zuò)用域插槽,這(zhè)裏我們來看(kàn)一(yī)個 <FancyList> 組件的例子。它會渲染一(yī)個列表,并同時(shí)會封裝一(yī)些加載遠端數(shù)據的邏輯、使用數(shù)據進行(xíng)列表渲染、或者是像分頁或無限滾動這(zhè)樣更進階的功能(néng)。然而我們希望它能(néng)夠保留足夠的靈活性,将對單個列表元素內(nèi)容和(hé)樣式的控制權留給使用它的父組件。我們期望的用法可(kě)能(néng)是這(zhè)樣的:

template

<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> 之中,我們可(kě)以多次渲染 <slot> 并每次都(dōu)提供不同的數(shù)據 (注意我們這(zhè)裏使用了(le) v-bind 來傳遞插槽的 props):

template

<ul>

<li v-for="item in items">

<slot name="item" v-bind="item"></slot>

</li>

</ul>

無渲染組件 ​

上(shàng)面的 <FancyList> 案例同時(shí)封裝了(le)可(kě)重用的邏輯 (數(shù)據獲取、分頁等) 和(hé)視(shì)圖輸出,但(dàn)也将部分視(shì)圖輸出通(tōng)過作(zuò)用域插槽交給了(le)消費(fèi)者組件來管理。

如(rú)果我們将這(zhè)個概念拓展一(yī)下(xià),可(kě)以想象的是,一(yī)些組件可(kě)能(néng)隻包括了(le)邏輯而不需要(yào)自(zì)己渲染內(nèi)容,視(shì)圖輸出通(tōng)過作(zuò)用域插槽全權交給了(le)消費(fèi)者組件。我們将這(zhè)種類型的組件稱為(wèi)無渲染組件。

這(zhè)裏有一(yī)個無渲染組件的例子,一(yī)個封裝了(le)追蹤當前鼠标位置邏輯的組件:

template

<MouseTracker v-slot="{ x, y }">

Mouse is at: {{ x }}, {{ y }}

</MouseTracker>

雖然這(zhè)個模式很(hěn)有趣,但(dàn)大部分能(néng)用無渲染組件實現的功能(néng)都(dōu)可(kě)以通(tōng)過組合式 API 以另一(yī)種更高效的方式實現,并且還不會帶來額外(wài)組件嵌套的開(kāi)銷。之後我們會在組合式函數(shù)一(yī)章(zhāng)中介紹如(rú)何更高效地(dì)實現追蹤鼠标位置的功能(néng)。

盡管如(rú)此,作(zuò)用域插槽在需要(yào)同時(shí)封裝邏輯、組合視(shì)圖界面時(shí)還是很(hěn)有用,就像上(shàng)面的 <FancyList> 組件那(nà)樣。

網站建設開(kāi)發|APP設計開(kāi)發|小程序建設開(kāi)發
下(xià)一(yī)篇:異步組件
上(shàng)一(yī)篇:透傳 Attributes