此章(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)發