組件基礎 {#components-basics}
組件允許我們將 UI 劃分為獨立的、可重用的部分,并且可以對每個部分進行單獨的思考。在實際應用中,組件常常被組織成層層嵌套的樹狀結構:
這和我們嵌套 HTML 元素的方式類似,Vue 實現了自己的組件模型,使我們可以在每個組件內封裝自定義內容與邏輯。Vue 同樣也能很好地配合原生 Web Component。如果你想知道 Vue 組件與原生 Web Components 之間的關系,可以閱讀此章節。
定義一個組件 {#defining-a-component}
當使用構建步驟時,我們一般會將 Vue 組件定義在一個單獨的 .vue
文件中,這被叫做單文件組件 (簡稱 SFC):
<script>
export default {data() {return {count: 0}}
}
</script><template><button @click="count++">You clicked me {{ count }} times.</button>
</template>
<script setup>
import { ref } from 'vue'const count = ref(0)
</script><template><button @click="count++">You clicked me {{ count }} times.</button>
</template>
當不使用構建步驟時,一個 Vue 組件以一個包含 Vue 特定選項的 JavaScript 對象來定義:
export default {data() {return {count: 0}},template: `<button @click="count++">You clicked me {{ count }} times.</button>`
}
import { ref } from 'vue'export default {setup() {const count = ref(0)return { count }},template: `<button @click="count++">You clicked me {{ count }} times.</button>`// 也可以針對一個 DOM 內聯模板:// template: '#my-template-element'
}
這里的模板是一個內聯的 JavaScript 字符串,Vue 將會在運行時編譯它。你也可以使用 ID 選擇器來指向一個元素 (通常是原生的 <template>
元素),Vue 將會使用其內容作為模板來源。
上面的例子中定義了一個組件,并在一個 .js
文件里默認導出了它自己,但你也可以通過具名導出在一個文件中導出多個組件。
使用組件 {#using-a-component}
:::tip
我們會在接下來的指引中使用 SFC 語法,無論你是否使用構建步驟,組件相關的概念都是相同的。示例一節中展示了兩種場景中的組件使用情況。
:::
要使用一個子組件,我們需要在父組件中導入它。假設我們把計數器組件放在了一個叫做 ButtonCounter.vue
的文件中,這個組件將會以默認導出的形式被暴露給外部。
<script>
import ButtonCounter from './ButtonCounter.vue'export default {components: {ButtonCounter}
}
</script><template><h1>Here is a child component!</h1><ButtonCounter />
</template>
若要將導入的組件暴露給模板,我們需要在 components
選項上注冊它。這個組件將會以其注冊時的名字作為模板中的標簽名。
<script setup>
import ButtonCounter from './ButtonCounter.vue'
</script><template><h1>Here is a child component!</h1><ButtonCounter />
</template>
通過 <script setup>
,導入的組件都在模板中直接可用。
當然,你也可以全局地注冊一個組件,使得它在當前應用中的任何組件上都可以使用,而不需要額外再導入。關于組件的全局注冊和局部注冊兩種方式的利弊,我們放在了組件注冊這一章節中專門討論。
組件可以被重用任意多次:
<h1>Here is a child component!</h1>
<ButtonCounter />
<ButtonCounter />
<ButtonCounter />
在演練場中嘗試一下
在演練場中嘗試一下
你會注意到,每當點擊這些按鈕時,每一個組件都維護著自己的狀態,是不同的 count
。這是因為每當你使用一個組件,就創建了一個新的實例。
在單文件組件中,推薦為子組件使用 PascalCase
的標簽名,以此來和原生的 HTML 元素作區分。雖然原生 HTML 標簽名是不區分大小寫的,但 Vue 單文件組件是可以在編譯中區分大小寫的。我們也可以使用 />
來關閉一個標簽。
如果你是直接在 DOM 中書寫模板 (例如原生 <template>
元素的內容),模板的編譯需要遵從瀏覽器中 HTML 的解析行為。在這種情況下,你應該需要使用 kebab-case
形式并顯式地關閉這些組件的標簽。
<!-- 如果是在 DOM 中書寫該模板 -->
<button-counter></button-counter>
<button-counter></button-counter>
<button-counter></button-counter>
請看 DOM 模板解析注意事項了解更多細節。
傳遞 props {#passing-props}
如果我們正在構建一個博客,我們可能需要一個表示博客文章的組件。我們希望所有的博客文章分享相同的視覺布局,但有不同的內容。要實現這樣的效果自然必須向組件中傳遞數據,例如每篇文章標題和內容,這就會使用到 props。
Props 是一種特別的 attributes,你可以在組件上聲明注冊。要傳遞給博客文章組件一個標題,我們必須在組件的 props 列表上聲明它。這里要用到 defineProps
宏:
<!-- BlogPost.vue -->
<script>
export default {props: ['title']
}
</script><template><h4>{{ title }}</h4>
</template>
當一個值被傳遞給 prop 時,它將成為該組件實例上的一個屬性。該屬性的值可以像其他組件屬性一樣,在模板和組件的 this
上下文中訪問。
<!-- BlogPost.vue -->
<script setup>
defineProps(['title'])
</script><template><h4>{{ title }}</h4>
</template>
defineProps
是一個僅 <script setup>
中可用的編譯宏命令,并不需要顯式地導入。聲明的 props 會自動暴露給模板。defineProps
會返回一個對象,其中包含了可以傳遞給組件的所有 props:
const props = defineProps(['title'])
console.log(props.title)
TypeScript 用戶請參考:為組件 props 標注類型
如果你沒有使用 <script setup>
,props 必須以 props
選項的方式聲明,props 對象會作為 setup()
函數的第一個參數被傳入:
export default {props: ['title'],setup(props) {console.log(props.title)}
}
一個組件可以有任意多的 props,默認情況下,所有 prop 都接受任意類型的值。
當一個 prop 被注冊后,可以像這樣以自定義 attribute 的形式傳遞數據給它:
<BlogPost title="My journey with Vue" />
<BlogPost title="Blogging with Vue" />
<BlogPost title="Why Vue is so fun" />
在實際應用中,我們可能在父組件中會有如下的一個博客文章數組:
export default {// ...data() {return {posts: [{ id: 1, title: 'My journey with Vue' },{ id: 2, title: 'Blogging with Vue' },{ id: 3, title: 'Why Vue is so fun' }]}}
}
const posts = ref([{ id: 1, title: 'My journey with Vue' },{ id: 2, title: 'Blogging with Vue' },{ id: 3, title: 'Why Vue is so fun' }
])
這種情況下,我們可以使用 v-for
來渲染它們:
<BlogPostv-for="post in posts":key="post.id":title="post.title"/>
在演練場中嘗試一下
在演練場中嘗試一下
留意我們是如何使用 v-bind
來傳遞動態 prop 值的。當事先不知道要渲染的確切內容時,這一點特別有用。
以上就是目前你需要了解的關于 props 的全部了。如果你看完本章節后還想知道更多細節,我們推薦你深入閱讀關于 props 的完整指引。
監聽事件 {#listening-to-events}
讓我們繼續關注我們的 <BlogPost>
組件。我們會發現有時候它需要與父組件進行交互。例如,要在此處實現無障礙訪問的需求,將博客文章的文字能夠放大,而頁面的其余部分仍使用默認字號。
在父組件中,我們可以添加一個 postFontSize
ref 來實現這個效果:
data() {return {posts: [/* ... */],postFontSize: 1}
}
const posts = ref([/* ... */
])const postFontSize = ref(1)
在模板中用它來控制所有博客文章的字體大小:
<div :style="{ fontSize: postFontSize + 'em' }"><BlogPostv-for="post in posts":key="post.id":title="post.title"/>
</div>
然后,給 <BlogPost>
組件添加一個按鈕:
<!-- BlogPost.vue, 省略了 <script> -->
<template><div class="blog-post"><h4>{{ title }}</h4><button>Enlarge text</button></div>
</template>
這個按鈕目前還沒有做任何事情,我們想要點擊這個按鈕來告訴父組件它應該放大所有博客文章的文字。要解決這個問題,組件實例提供了一個自定義事件系統。父組件可以通過 v-on
或 @
來選擇性地監聽子組件上拋的事件,就像監聽原生 DOM 事件那樣:
<BlogPost...@enlarge-text="postFontSize += 0.1"/>
子組件可以通過調用內置的 $emit
方法,通過傳入事件名稱來拋出一個事件:
<!-- BlogPost.vue, 省略了 <script> -->
<template><div class="blog-post"><h4>{{ title }}</h4><button @click="$emit('enlarge-text')">Enlarge text</button></div>
</template>
因為有了 @enlarge-text="postFontSize += 0.1"
的監聽,父組件會接收這一事件,從而更新 postFontSize
的值。
在演練場中嘗試一下
在演練場中嘗試一下
我們可以通過 defineEmits
宏來聲明需要拋出的事件:
<!-- BlogPost.vue -->
<script>
export default {props: ['title'],emits: ['enlarge-text']
}
</script>
<!-- BlogPost.vue -->
<script setup>
defineProps(['title'])
defineEmits(['enlarge-text'])
</script>
這聲明了一個組件可能觸發的所有事件,還可以對事件的參數進行驗證。同時,這還可以讓 Vue 避免將它們作為原生事件監聽器隱式地應用于子組件的根元素。
和 defineProps
類似,defineEmits
僅可用于 <script setup>
之中,并且不需要導入,它返回一個等同于 $emit
方法的 emit
函數。它可以被用于在組件的 <script setup>
中拋出事件,因為此處無法直接訪問 $emit
:
<script setup>
const emit = defineEmits(['enlarge-text'])emit('enlarge-text')
</script>
TypeScript 用戶請參考:為組件 emits 標注類型
如果你沒有在使用 <script setup>
,你可以通過 emits
選項定義組件會拋出的事件。你可以從 setup()
函數的第二個參數,即 setup 上下文對象上訪問到 emit
函數:
export default {emits: ['enlarge-text'],setup(props, ctx) {ctx.emit('enlarge-text')}
}
以上就是目前你需要了解的關于組件自定義事件的所有知識了。如果你看完本章節后還想知道更多細節,請深入閱讀組件事件章節。
通過插槽來分配內容 {#content-distribution-with-slots}
一些情況下我們會希望能和 HTML 元素一樣向組件中傳遞內容:
<AlertBox>Something bad happened.
</AlertBox>
我們期望能渲染成這樣:
:::danger This is an Error for Demo Purposes
Something bad happened.
:::
這可以通過 Vue 的自定義 <slot>
元素來實現:
<template><div class="alert-box"><strong>This is an Error for Demo Purposes</strong><slot /></div>
</template><style scoped>
.alert-box {/* ... */
}
</style>
如上所示,我們使用 <slot>
作為一個占位符,父組件傳遞進來的內容就會渲染在這里。
在演練場中嘗試一下
在演練場中嘗試一下
以上就是目前你需要了解的關于插槽的所有知識了。如果你看完本章節后還想知道更多細節,請深入閱讀組件插槽章節。
動態組件 {#dynamic-components}
有些場景會需要在兩個組件間來回切換,比如 Tab 界面:
在演練場中查看示例
在演練場中查看示例
上面的例子是通過 Vue 的 <component>
元素和特殊的 is
attribute 實現的:
<!-- currentTab 改變時組件也改變 -->
<component :is="currentTab"></component>
<!-- currentTab 改變時組件也改變 -->
<component :is="tabs[currentTab]"></component>
在上面的例子中,被傳給 :is
的值可以是以下幾種:
- 被注冊的組件名
- 導入的組件對象
你也可以使用 is
attribute 來創建一般的 HTML 元素。
當使用 <component :is="...">
來在多個組件間作切換時,被切換掉的組件會被卸載。我們可以通過 <KeepAlive>
組件強制被切換掉的組件仍然保持“存活”的狀態。
DOM 模板解析注意事項 {#dom-template-parsing-caveats}
如果你想在 DOM 中直接書寫 Vue 模板,Vue 則必須從 DOM 中獲取模板字符串。由于瀏覽器的原生 HTML 解析行為限制,有一些需要注意的事項。
:::tip
請注意下面討論只適用于直接在 DOM 中編寫模板的情況。如果你使用來自以下來源的字符串模板,就不需要顧慮這些限制了:
- 單文件組件
- 內聯模板字符串 (例如
template: '...'
) <script type="text/x-template">
:::
大小寫區分 {#case-insensitivity}
HTML 標簽和屬性名稱是不分大小寫的,所以瀏覽器會把任何大寫的字符解釋為小寫。這意味著當你使用 DOM 內的模板時,無論是 PascalCase 形式的組件名稱、camelCase 形式的 prop 名稱還是 v-on 的事件名稱,都需要轉換為相應等價的 kebab-case (短橫線連字符) 形式:
// JavaScript 中的 camelCase
const BlogPost = {props: ['postTitle'],emits: ['updatePost'],template: `<h3>{{ postTitle }}</h3>`
}
<!-- HTML 中的 kebab-case -->
<blog-post post-title="hello!" @update-post="onUpdatePost"></blog-post>
閉合標簽 {#self-closing-tags}
我們在上面的例子中已經使用過了閉合標簽 (self-closing tag):
<MyComponent />
這是因為 Vue 的模板解析器支持任意標簽使用 />
作為標簽關閉的標志。
然而在 DOM 模板中,我們必須顯式地寫出關閉標簽:
<my-component></my-component>
這是由于 HTML 只允許一小部分特殊的元素省略其關閉標簽,最常見的就是 <input>
和 <img>
。對于其他的元素來說,如果你省略了關閉標簽,原生的 HTML 解析器會認為開啟的標簽永遠沒有結束,用下面這個代碼片段舉例來說:
<my-component /> <!-- 我們想要在這里關閉標簽... -->
<span>hello</span>
將被解析為:
<my-component><span>hello</span>
</my-component> <!-- 但瀏覽器會在這里關閉標簽 -->
元素位置限制 {#element-placement-restrictions}
某些 HTML 元素對于放在其中的元素類型有限制,例如 <ul>
,<ol>
,<table>
和 <select>
,相應的,某些元素僅在放置于特定元素中時才會顯示,例如 <li>
,<tr>
和 <option>
。
這將導致在使用帶有此類限制元素的組件時出現問題。例如:
<table><blog-post-row></blog-post-row>
</table>
自定義的組件 <blog-post-row>
將作為無效的內容被忽略,因而在最終呈現的輸出中造成錯誤。我們可以使用特殊的 is
attribute 作為一種解決方案:
<table><tr is="vue:blog-post-row"></tr>
</table>
:::tip
當使用在原生 HTML 元素上時,is
的值必須加上前綴 vue:
才可以被解析為一個 Vue 組件。這一點是必要的,為了避免和原生的自定義內置元素相混淆。
:::
以上就是你需要了解的關于 DOM 模板解析的所有注意事項,同時也是 Vue 基礎部分的所有內容。祝賀你!雖然還有很多需要學習的,但你可以先暫停一下,去用 Vue 做一些有趣的東西,或者研究一些示例。
完成了本頁的閱讀后,回顧一下你剛才所學到的知識,如果還想知道更多細節,我們推薦你繼續閱讀關于組件的完整指引。
插槽 Slots {#slots}
此章節假設你已經看過了組件基礎。若你還不了解組件是什么,請先閱讀該章節。
插槽內容與出口 {#slot-content-and-outlet}
在之前的章節中,我們已經了解到組件能夠接收任意類型的 JavaScript 值作為 props,但組件要如何接收模板內容呢?在某些場景中,我們可能想要為子組件傳遞一些模板片段,讓子組件在它們的組件中渲染這些片段。
舉例來說,這里有一個 <FancyButton>
組件,可以像這樣使用:
<FancyButton>Click me! <!-- 插槽內容 -->
</FancyButton>
而 <FancyButton>
的模板是這樣的:
<button class="fancy-btn"><slot></slot> <!-- 插槽出口 -->
</button>
<slot>
元素是一個插槽出口 (slot outlet),標示了父元素提供的插槽內容 (slot content) 將在哪里被渲染。
最終渲染出的 DOM 是這樣:
<button class="fancy-btn">Click me!</button>
在演練場中嘗試一下
在演練場中嘗試一下
通過使用插槽,<FancyButton>
僅負責渲染外層的 <button>
(以及相應的樣式),而其內部的內容由父組件提供。
理解插槽的另一種方式是和下面的 JavaScript 函數作類比,其概念是類似的:
// 父元素傳入插槽內容
FancyButton('Click me!')// FancyButton 在自己的模板中渲染插槽內容
function FancyButton(slotContent) {return `<button class="fancy-btn">${slotContent}</button>`
}
插槽內容可以是任意合法的模板內容,不局限于文本。例如我們可以傳入多個元素,甚至是組件:
<FancyButton><span style="color:red">Click me!</span><AwesomeIcon name="plus" />
</FancyButton>
在演練場中嘗試一下
在演練場中嘗試一下
通過使用插槽,<FancyButton>
組件更加靈活和具有可復用性。現在組件可以用在不同的地方渲染各異的內容,但同時還保證都具有相同的樣式。
Vue 組件的插槽機制是受原生 Web Component <slot>
元素的啟發而誕生,同時還做了一些功能拓展,這些拓展的功能我們后面會學習到。
渲染作用域 {#render-scope}
插槽內容可以訪問到父組件的數據作用域,因為插槽內容本身是在父組件模板中定義的。舉例來說:
<span>{{ message }}</span>
<FancyButton>{{ message }}</FancyButton>
這里的兩個 {{ message }}
插值表達式渲染的內容都是一樣的。
插槽內容無法訪問子組件的數據。Vue 模板中的表達式只能訪問其定義時所處的作用域,這和 JavaScript 的詞法作用域規則是一致的。換言之:
父組件模板中的表達式只能訪問父組件的作用域;子組件模板中的表達式只能訪問子組件的作用域。
默認內容 {#fallback-content}
在外部沒有提供任何內容的情況下,可以為插槽指定默認內容。比如有這樣一個 <SubmitButton>
組件:
<button type="submit"><slot></slot>
</button>
如果我們想在父組件沒有提供任何插槽內容時在 <button>
內渲染“Submit”,只需要將“Submit”寫在 <slot>
標簽之間來作為默認內容:
<button type="submit"><slot>Submit <!-- 默認內容 --></slot>
</button>
現在,當我們在父組件中使用 <SubmitButton>
且沒有提供任何插槽內容時:
<SubmitButton />
“Submit”將會被作為默認內容渲染:
<button type="submit">Submit</button>
但如果我們提供了插槽內容:
<SubmitButton>Save</SubmitButton>
那么被顯式提供的內容會取代默認內容:
<button type="submit">Save</button>
在演練場中嘗試一下
在演練場中嘗試一下
具名插槽 {#named-slots}
有時在一個組件中包含多個插槽出口是很有用的。舉例來說,在一個 <BaseLayout>
組件中,有如下模板:
<div class="container"><header><!-- 標題內容放這里 --></header><main><!-- 主要內容放這里 --></main><footer><!-- 底部內容放這里 --></footer>
</div>
對于這種場景,<slot>
元素可以有一個特殊的 attribute name
,用來給各個插槽分配唯一的 ID,以確定每一處要渲染的內容:
<div class="container"><header><slot name="header"></slot></header><main><slot></slot></main><footer><slot name="footer"></slot></footer>
</div>
這類帶 name
的插槽被稱為具名插槽 (named slots)。沒有提供 name
的 <slot>
出口會隱式地命名為“default”。
在父組件中使用 <BaseLayout>
時,我們需要一種方式將多個插槽內容傳入到各自目標插槽的出口。此時就需要用到具名插槽了:
要為具名插槽傳入內容,我們需要使用一個含 v-slot
指令的 <template>
元素,并將目標插槽的名字傳給該指令:
<BaseLayout><template v-slot:header><!-- header 插槽的內容放這里 --></template>
</BaseLayout>
v-slot
有對應的簡寫 #
,因此 <template v-slot:header>
可以簡寫為 <template #header>
。其意思就是“將這部分模板片段傳入子組件的 header 插槽中”。
下面我們給出完整的、向 <BaseLayout>
傳遞插槽內容的代碼,指令均使用的是縮寫形式:
<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>
當一個組件同時接收默認插槽和具名插槽時,所有位于頂級的非 <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>
元素中的所有內容都將被傳遞到相應的插槽。最終渲染出的 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 函數來類比可能更有助于你來理解具名插槽:
// 傳入不同的內容給不同名字的插槽
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>`
}
動態插槽名 {#dynamic-slot-names}
動態指令參數在 v-slot
上也是有效的,即可以定義下面這樣的動態插槽名:
<base-layout><template v-slot:[dynamicSlotName]>...</template><!-- 縮寫為 --><template #[dynamicSlotName]>...</template>
</base-layout>
注意這里的表達式和動態指令參數受相同的語法限制。
作用域插槽 {#scoped-slots}
在上面的渲染作用域中我們討論到,插槽的內容無法訪問到子組件的狀態。
然而在某些場景下插槽的內容可能想要同時使用父組件域內和子組件域內的數據。要做到這一點,我們需要一種方法來讓子組件在渲染時將一部分數據提供給插槽。
我們也確實有辦法這么做!可以像對組件傳遞 props 那樣,向一個插槽的出口上傳遞 attributes:
<!-- <MyComponent> 的模板 -->
<div><slot :text="greetingMessage" :count="1"></slot>
</div>
當需要接收插槽 props 時,默認插槽和具名插槽的使用方式有一些小區別。下面我們將先展示默認插槽如何接受 props,通過子組件標簽上的 v-slot
指令,直接接收到了一個插槽 props 對象:
<MyComponent v-slot="slotProps">{{ slotProps.text }} {{ slotProps.count }}
</MyComponent>
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-Lq0fLQB4-1691855652867)(https://cn.vuejs.org/assets/scoped-slots.1c6d5876.svg)]
在演練場中嘗試一下
在演練場中嘗試一下
子組件傳入插槽的 props 作為了 v-slot
指令的值,可以在插槽內的表達式中訪問。
你可以將作用域插槽類比為一個傳入子組件的函數。子組件會將相應的 props 作為參數傳給它:
MyComponent({// 類比默認插槽,將其想成一個函數default: (slotProps) => {return `${slotProps.text} ${slotProps.count}`}
})function MyComponent(slots) {const greetingMessage = 'hello'return `<div>${// 在插槽函數調用時傳入 propsslots.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>
注意插槽上的 name
是一個 Vue 特別保留的 attribute,不會作為 props 傳遞給插槽。因此最終 headerProps
的結果是 { message: 'hello' }
。
如果你同時使用了具名插槽與默認插槽,則需要為默認插槽使用顯式的 <template>
標簽。嘗試直接為組件添加 v-slot
指令將導致編譯錯誤。這是為了避免因默認插槽的 props 的作用域而困惑。舉例:
<!-- 該模板無法編譯 -->
<template><MyComponent v-slot="{ message }"><p>{{ message }}</p><template #footer><!-- message 屬于默認插槽,此處不可用 --><p>{{ message }}</p></template></MyComponent>
</template>
為默認插槽使用顯式的 <template>
標簽有助于更清晰地指出 message
屬性在其他插槽中不可用:
<template><MyComponent><!-- 使用顯式的默認插槽 --><template #default="{ message }"><p>{{ message }}</p></template><template #footer><p>Here's some contact info</p></template></MyComponent>
</template>
高級列表組件示例 {#fancy-list-example}
你可能想問什么樣的場景才適合用到作用域插槽,這里我們來看一個 <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>
在演練場中嘗試一下
在演練場中嘗試一下
無渲染組件 {#renderless-components}
上面的 <FancyList>
案例同時封裝了可重用的邏輯 (數據獲取、分頁等) 和視圖輸出,但也將部分視圖輸出通過作用域插槽交給了消費者組件來管理。
如果我們將這個概念拓展一下,可以想象的是,一些組件可能只包括了邏輯而不需要自己渲染內容,視圖輸出通過作用域插槽全權交給了消費者組件。我們將這種類型的組件稱為無渲染組件。
這里有一個無渲染組件的例子,一個封裝了追蹤當前鼠標位置邏輯的組件:
<MouseTracker v-slot="{ x, y }">Mouse is at: {{ x }}, {{ y }}
</MouseTracker>
在演練場中嘗試一下
在演練場中嘗試一下
雖然這個模式很有趣,但大部分能用無渲染組件實現的功能都可以通過組合式 API 以另一種更高效的方式實現,并且還不會帶來額外組件嵌套的開銷。之后我們會在組合式函數一章中介紹如何更高效地實現追蹤鼠標位置的功能。
盡管如此,作用域插槽在需要同時封裝邏輯、組合視圖界面時還是很有用,就像上面的 <FancyList>
組件那樣。