此章節假設你(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)發