做自(zì)由與創造的先行者

插槽 Slots

Vue.js中文(wén)手冊

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

插槽内容與出口 ​

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

舉例來(lái)說,這(zhè)裏有一個 <FancyButton> 組件,可以像這(zhè)樣使用(yòng):

template

<FancyButton>

Click me! <!-- 插槽内容 -->

</FancyButton>

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

template

<button class="fancy-btn">

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

</button>

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

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

html

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

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

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

js

// 父元素傳入插槽内容

FancyButton('Click me!')

// FancyButton 在自(zì)己的模闆中渲染插槽内容

function FancyButton(slotContent) {

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

${slotContent}

</button>`

}

插槽内容可以是任意合法的模闆内容,不局限于文(wén)本。例如我們可以傳入多個元素,甚至是組件:

template

<FancyButton>

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

<AwesomeIcon name="plus" />

</FancyButton>

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

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

渲染作(zuò)用(yòng)域 ​

插槽内容可以訪問到(dào)父組件的數據作(zuò)用(yòng)域,因爲插槽内容本身是在父組件模闆中定義的。舉例來(lái)說:

template

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

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

這(zhè)裏的兩個 {{ message }} 插值表達式渲染的内容都是一樣的。

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

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

默認内容 ​

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

template

<button type="submit">

<slot></slot>

</button>

如果我們想在父組件沒有提供任何插槽内容時(shí)在 <button> 内渲染“Submit”,隻需要将“Submit”寫在 <slot> 标簽之間來(lái)作(zuò)爲默認内容:

template

<button type="submit">

<slot>

Submit <!-- 默認内容 -->

</slot>

</button>

現(xiàn)在,當我們在父組件中使用(yòng) <SubmitButton> 且沒有提供任何插槽内容時(shí):

template

<SubmitButton />

“Submit”将會(huì)被作(zuò)爲默認内容渲染:

html

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

但(dàn)如果我們提供了(le)插槽内容:

template

<SubmitButton>Save</SubmitButton>

那麽被顯式提供的内容會(huì)取代默認内容:

html

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

具名插槽 ​

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

template

<div class="container">

<header>

<!-- 标題内容放(fàng)這(zhè)裏 -->

</header>

<main>

<!-- 主要内容放(fàng)這(zhè)裏 -->

</main>

<footer>

<!-- 底部内容放(fàng)這(zhè)裏 -->

</footer>

</div>

對(duì)于這(zhè)種場景,<slot> 元素可以有一個特殊的 attribute name,用(yòng)來(lái)給各個插槽分配唯一的 ID,以确定每一處要渲染的内容:

template

<div class="container">

<header>

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

</header>

<main>

<slot></slot>

</main>

<footer>

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

</footer>

</div>

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

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

要爲具名插槽傳入内容,我們需要使用(yòng)一個含 v-slot 指令的 <template> 元素,并将目标插槽的名字傳給該指令:

template

<BaseLayout>

<template v-slot:header>

<!-- header 插槽的内容放(fàng)這(zhè)裏 -->

</template>

</BaseLayout>

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

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

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>

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

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>

現(xiàn)在 <template> 元素中的所有内容都将被傳遞到(dào)相應的插槽。最終渲染出的 HTML 如下(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>

使用(yòng) JavaScript 函數來(lái)類比可能(néng)更有助于你(nǐ)來(lái)理(lǐ)解具名插槽:

js

// 傳入不同的内容給不同名字的插槽

BaseLayout({

header: `...`,

default: `...`,

footer: `...`

})

// <BaseLayout> 渲染插槽内容到(dào)對(duì)應位置

function BaseLayout(slots) {

return `<div class="container">

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

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

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

</div>`

}

動态插槽名 ​

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

template

<base-layout>

<template v-slot:[dynamicSlotName]>

...

</template>

<!-- 縮寫爲 -->

<template #[dynamicSlotName]>

...

</template>

</base-layout>

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

作(zuò)用(yòng)域插槽 ​

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

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

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

template

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

<div>

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

</div>

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

template

<MyComponent v-slot="slotProps">

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

</MyComponent>

子組件傳入插槽的 props 作(zuò)爲了(le) v-slot 指令的值,可以在插槽内的表達式中訪問。

你(nǐ)可以将作(zuò)用(yòng)域插槽類比爲一個傳入子組件的函數。子組件會(huì)将相應的 props 作(zuò)爲參數傳給它:

js

MyComponent({

// 類比默認插槽,将其想成一個函數

default: (slotProps) => {

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

}

})

function MyComponent(slots) {

const greetingMessage = 'hello'

return `<div>${

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

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

}</div>`

}

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

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

template

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

{{ text }} {{ count }}

</MyComponent>

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

具名作(zuò)用(yòng)域插槽的工(gōng)作(zuò)方式也(yě)是類似的,插槽 props 可以作(zuò)爲 v-slot 指令的值被訪問到(dào):v-slot:name="slotProps"。當使用(yòng)縮寫時(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 是一個 Vue 特别保留的 attribute,不會(huì)作(zuò)爲 props 傳遞給插槽。因此最終 headerProps 的結果是 { message: 'hello' }。

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

template

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

<template>

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

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

<template #footer>

<!-- message 屬于默認插槽,此處不可用(yòng) -->

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

</template>

</MyComponent>

</template>

爲默認插槽使用(yòng)顯式的 <template> 标簽有助于更清晰地指出 message 屬性在其他(tā)插槽中不可用(yòng):

template

<template>

<MyComponent>

<!-- 使用(yòng)顯式的默認插槽 -->

<template #default="{ message }">

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

</template>

<template #footer>

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

</template>

</MyComponent>

</template>

高(gāo)級列表組件示例 ​

你(nǐ)可能(néng)想問什(shén)麽樣的場景才适合用(yòng)到(dào)作(zuò)用(yòng)域插槽,這(zhè)裏我們來(lái)看(kàn)一個 <FancyList> 組件的例子。它會(huì)渲染一個列表,并同時(shí)會(huì)封裝一些(xiē)加載遠端數據的邏輯、使用(yòng)數據進行列表渲染、或者是像分頁或無限滾動這(zhè)樣更進階的功能(néng)。然而我們希望它能(néng)夠保留足夠的靈活性,将對(duì)單個列表元素内容和(hé)樣式的控制權留給使用(yòng)它的父組件。我們期望的用(yòng)法可能(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> 之中,我們可以多次渲染 <slot> 并每次都提供不同的數據 (注意我們這(zhè)裏使用(yòng)了(le) v-bind 來(lái)傳遞插槽的 props):

template

<ul>

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

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

</li>

</ul>

無渲染組件 ​

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

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

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

template

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

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

</MouseTracker>

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

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

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