在 Vue.js 中使用插槽
已發表: 2022-03-10在最近發布的 Vue 2.6 中,使用槽的語法變得更加簡潔。 對插槽的這種更改讓我對發現插槽的潛在力量重新產生了興趣,以便為我們基於 Vue 的項目提供可重用性、新功能和更清晰的可讀性。 插槽真正能夠做什麼?
如果您是 Vue 新手或者沒有看到 2.6 版的變化,請繼續閱讀。 了解插槽的最佳資源可能是 Vue 自己的文檔,但我會嘗試在這裡給出一個概要。
什麼是老虎機?
插槽是 Vue 組件的一種機制,它允許您以不同於嚴格的父子關係的方式組合組件。 插槽為您提供了將內容放置在新位置或使組件更通用的出口。 了解它們的最佳方式是親眼目睹它們的行動。 讓我們從一個簡單的例子開始:
// frame.vue <template> <div class="frame"> <slot></slot> </div> </template>
這個組件有一個包裝器div
。 讓我們假設div
在那裡圍繞其內容創建一個風格框架。 該組件可用於將框架包裹在您想要的任何內容周圍。 讓我們看看使用它的樣子。 這裡的frame
組件是指我們上面剛剛製作的組件。
// app.vue <template> <frame><img src="an-image.jpg"></frame> </template>
開始和結束frame
標籤之間的內容將被插入到slot
所在的frame
組件中,替換slot
標籤。 這是最基本的方法。 您還可以通過填寫以下內容來指定要進入插槽的默認內容:
// frame.vue <template> <div class="frame"> <slot>This is the default content if nothing gets specified to go here</slot> </div> </template>
所以現在如果我們像這樣使用它:
// app.vue <template> <frame /> </template>
“This is the default content if nothing gets specified to go here”的默認文本將顯示,但如果我們像以前一樣使用它,默認文本將被img
標籤覆蓋。
多個/命名插槽
您可以向一個組件添加多個插槽,但如果這樣做,除了其中一個之外,所有插槽都需要有名稱。 如果有一個沒有名稱,則它是默認插槽。 以下是創建多個插槽的方法:
// titled-frame.vue <template> <div class="frame"> <header><h2><slot name="header">Title</slot></h2></header> <slot>This is the default content if nothing gets specified to go here</slot> </div> </template>
我們保留了相同的默認插槽,但這次我們添加了一個名為header
的插槽,您可以在其中輸入標題。 你像這樣使用它:
// app.vue <template> <titled-frame> <template v-slot:header> <!-- The code below goes into the header slot --> My Image's Title </template> <!-- The code below goes into the default slot --> <img src="an-image.jpg"> </titled-frame> </template>
和之前一樣,如果我們想將內容添加到默認插槽中,只需將其直接放在titled-frame
組件中即可。 但是,要將內容添加到命名插槽,我們需要使用v-slot
指令將代碼包裝在template
標籤中。 您在v-slot
之後添加一個冒號 ( :
),然後寫下您希望將內容傳遞到的插槽的名稱。 請注意v-slot
是 Vue 2.6 的新版本,因此如果您使用的是舊版本,則需要閱讀有關已棄用插槽語法的文檔。
範圍插槽
您需要知道的另一件事是插槽可以將數據/函數向下傳遞給它們的子級。 為了證明這一點,我們需要一個完全不同的帶有插槽的示例組件,它比前一個更做作:讓我們通過創建一個將當前用戶的數據提供給其插槽的組件來從文檔中復制示例:
// current-user.vue <template> <span> <slot v-bind:user="user"> {{ user.lastName }} </slot> </span> </template> <script> export default { data () { return { user: ... } } } </script>
該組件有一個名為user
的屬性,其中包含有關用戶的詳細信息。 默認情況下,該組件顯示用戶的姓氏,但請注意,它使用v-bind
將用戶數據綁定到插槽。 有了這個,我們可以使用這個組件將用戶數據提供給它的後代:
// app.vue <template> <current-user> <template v-slot:default="slotProps">{{ slotProps.user.firstName }}</template> </current-user> </template>
為了訪問傳遞給插槽的數據,我們使用v-slot
指令的值指定範圍變量的名稱。
這裡有一些注意事項:
- 我們指定了
default
的名稱,儘管我們不需要為默認插槽指定名稱。 相反,我們可以只使用v-slot="slotProps"
。 - 您不需要使用
slotProps
作為名稱。 你可以隨心所欲地稱呼它。 - 如果您只使用默認插槽,則可以跳過該內部
template
標籤並將v-slot
指令直接放在current-user
標籤上。 - 您可以使用對象解構來創建對作用域槽數據的直接引用,而不是使用單個變量名。 換句話說,你可以使用
v-slot="{user}"
代替v-slot="slotProps"
然後你可以直接使用user
代替slotProps.user
。
考慮到這些註釋,上面的例子可以改寫成這樣:
// app.vue <template> <current-user v-slot="{user}"> {{ user.firstName }} </current-user> </template>
還有幾件事要記住:
- 您可以使用
v-bind
指令綁定多個值。 所以在這個例子中,我可以做的不僅僅是user
。 - 您也可以將函數傳遞給作用域插槽。 許多庫使用它來提供可重用的功能組件,稍後您將看到。
-
v-slot
的別名是#
。 所以不用寫v-slot:header="data"
,你可以寫#header="data"
。 當您不使用作用域插槽時,您也可以只指定#header
而不是v-slot:header
。 至於默認插槽,您需要在使用別名時指定default
名稱。 換句話說,您需要編寫#default="data"
而不是#="data"
。
您可以從文檔中了解更多的小點,但這應該足以幫助您理解我們在本文其餘部分所討論的內容。
你可以用老虎機做什麼?
插槽不是為單一目的而建造的,或者至少如果它們是,它們已經超越了最初的意圖,成為做許多不同事情的強大工具。
可重用模式
組件總是被設計成可以重用的,但是某些模式對於單個“普通”組件是不切實際的,因為你需要的props
數量來定制它可能過多或者你需要通過props
傳遞大部分內容和潛在的其他組件。 插槽可用於包含模式的“外部”部分,並允許將其他 HTML 和/或組件放置在其中以自定義“內部”部分,從而允許具有插槽的組件定義模式和注入到插槽是唯一的。
對於我們的第一個示例,讓我們從簡單的東西開始:一個按鈕。 假設您和您的團隊正在使用 Bootstrap*。 使用 Bootstrap,您的按鈕通常與基類 `btn` 和一個指定顏色的類綁定在一起,例如 `btn-primary`。 您還可以添加大小類,例如 `btn-lg`。
* 我既不鼓勵也不阻止你這樣做,我只是需要一些東西作為我的例子,而且它是眾所周知的。
現在讓我們假設,為簡單起見,您的應用程序/站點始終使用btn-primary
和btn-lg
。 你不想總是在你的按鈕上寫下所有三個類,或者你不相信一個菜鳥會記住這三個類。 在這種情況下,您可以創建一個自動包含所有這三個類的組件,但是如何允許自定義內容呢? prop
是不實用的,因為button
標籤中允許包含各種 HTML,所以我們應該使用 slot。
<!-- my-button.vue --> <template> <button class="btn btn-primary btn-lg"> <slot>Click Me!</slot> </button> </template>
現在我們可以在任何地方使用它,無論您想要什麼內容:
<!-- somewhere else, using my-button.vue --> <template> <my-button> <img src="/img/awesome-icon.jpg"> SMASH THIS BUTTON TO BECOME AWESOME FOR ONLY $500!!! </my-button> </template>
當然,您可以使用比按鈕更大的東西。 堅持使用 Bootstrap,讓我們看一下模式,或者至少是 HTML 部分; 我不會進入功能......但是。
<!-- my-modal.vue --> <template> <div class="modal" tabindex="-1" role="dialog"> <div class="modal-dialog" role="document"> <div class="modal-content"> <div class="modal-header"> <slot name="header"></slot> <button type="button" class="close" data-dismiss="modal" aria-label="Close"> <span aria-hidden="true">×</span> </button> </div> <div class="modal-body"> <slot name="body"></slot> </div> <div class="modal-footer"> <slot name="footer"></slot> </div> </div> </div> </div> </template>
現在,讓我們使用這個:

<!-- somewhere else, using my-modal.vue --> <template> <my-modal> <template #header><!-- using the shorthand for `v-slot` --> <h5>Awesome Interruption!</h5> </template> <template #body> <p>We interrupt your use of our application to let you know that this application is awesome and you should continue using it every day for the rest of your life!</p> </template> <template #footer> <em>Now back to your regularly scheduled app usage</em> </template> </my-modal> </template>
上述類型的插槽用例顯然非常有用,但它可以做的更多。
重用功能
Vue 組件不僅僅與 HTML 和 CSS 有關。 它們是用 JavaScript 構建的,因此它們也與功能有關。 插槽可用於創建功能一次並在多個地方使用它。 讓我們回到我們的模態示例並添加一個關閉模態的函數:
<!-- my-modal.vue --> <template> <div class="modal" tabindex="-1" role="dialog"> <div class="modal-dialog" role="document"> <div class="modal-content"> <div class="modal-header"> <slot name="header"></slot> <button type="button" class="close" data-dismiss="modal" aria-label="Close"> <span aria-hidden="true">×</span> </button> </div> <div class="modal-body"> <slot name="body"></slot> </div> <div class="modal-footer"> <!-- using `v-bind` shorthand to pass the `closeModal` method to the component that will be in this slot --> <slot name="footer" :closeModal="closeModal"></slot> </div> </div> </div> </div> </template> <script> export default { //... methods: { closeModal () { // Do what needs to be done to close the modal... and maybe remove it from the DOM } } } </script>
現在當你使用這個組件時,你可以在頁腳添加一個可以關閉模式的按鈕。 通常,在 Bootstrap 模態的情況下,您可以將data-dismiss="modal"
添加到按鈕,但我們希望將 Bootstrap 特定的東西從將插入此模態組件的組件中隱藏起來。 所以我們向他們傳遞了一個他們可以調用的函數,而他們對 Bootstrap 的參與並不知情:
<!-- somewhere else, using my-modal.vue --> <template> <my-modal> <template #header><!-- using the shorthand for `v-slot` --> <h5>Awesome Interruption!</h5> </template> <template #body> <p>We interrupt your use of our application to let you know that this application is awesome and you should continue using it every day for the rest of your life!</p> </template> <!-- pull in `closeModal` and use it in a button's click handler --> <template #footer="{closeModal}"> <button @click="closeModal"> Take me back to the app so I can be awesome </button> </template> </my-modal> </template>
無渲染組件
最後,您可以利用您所知道的使用插槽來傳遞可重用功能並剝離幾乎所有的 HTML 並只使用插槽。 這就是無渲染組件的本質:一個只提供功能而沒有任何 HTML 的組件。
使組件真正無渲染可能有點棘手,因為您需要編寫render
函數而不是使用模板來消除對根元素的需求,但這可能並不總是必要的。 讓我們看一個讓我們首先使用模板的簡單示例:
<template> <transition name="fade" v-bind="$attrs" v-on="$listeners"> <slot></slot> </transition> </template> <style> .fade-enter-active, .fade-leave-active { transition: opacity 0.3s; } .fade-enter, .fade-leave-to { opacity: 0; } </style>
這是一個奇怪的無渲染組件示例,因為它甚至沒有任何 JavaScript。 這主要是因為我們只是創建了一個內置無渲染函數的預配置可重用版本: transition
。
是的,Vue 有內置的無渲染組件。 這個特定示例取自 Cristi Jora 的一篇關於可重用過渡的文章,並展示了一種創建無渲染組件的簡單方法,該組件可以標準化整個應用程序中使用的過渡。 Cristi 的文章更深入,並展示了可重用過渡的一些更高級的變體,因此我建議您查看一下。
對於我們的另一個示例,我們將創建一個組件來處理在 Promise 的不同狀態期間顯示的內容的切換:掛起、成功解決和失敗。 這是一種常見的模式,雖然它不需要大量代碼,但如果不提取邏輯以實現可重用性,它可能會混淆很多組件。
<!-- promised.vue --> <template> <span> <slot name="rejected" v-if="error" :error="error"></slot> <slot name="resolved" v-else-if="resolved" :data="data"></slot> <slot name="pending" v-else></slot> </span> </template> <script> export default { props: { promise: Promise }, data: () => ({ resolved: false, data: null, error: null }), watch: { promise: { handler (promise) { this.resolved = false this.error = null if (!promise) { this.data = null return } promise.then(data => { this.data = data this.resolved = true }) .catch(err => { this.error = err this.resolved = true }) }, immediate: true } } } </script>
那麼這裡發生了什麼? 首先,請注意我們正在接收一個名為promise
的Promise
道具。 在watch
部分,我們觀察 Promise 的變化,當它發生變化時(或立即在組件創建時,感謝immediate
屬性)我們清除狀態,然後調用then
並catch
Promise,當它成功完成或更新狀態時失敗。
然後,在模板中,我們根據狀態顯示不同的插槽。 請注意,我們未能保持它真正的無渲染,因為我們需要一個根元素才能使用模板。 我們還將data
和error
傳遞給相關的插槽範圍。
這是一個使用它的例子:
<template> <div> <promised :promise="somePromise"> <template #resolved="{ data }"> Resolved: {{ data }} </template> <template #rejected="{ error }"> Rejected: {{ error }} </template> <template #pending> Working on it... </template> </promised> </div> </template> ...
我們將somePromise
傳遞給無渲染組件。 當我們等待它完成時,我們正在顯示“正在處理它……”,這要歸功於pending
的插槽。 如果成功,我們會顯示“Resolved:”和分辨率值。 如果失敗,我們會顯示“Rejected:”以及導致拒絕的錯誤。 現在我們不再需要在該組件中跟踪 Promise 的狀態,因為該部分已被拉出到它自己的可重用組件中。
那麼,我們可以做些什麼來處理promised.vue
中的 slot 周圍的span
呢? 要移除它,我們需要移除template
部分並向我們的組件添加一個render
函數:
render () { if (this.error) { return this.$scopedSlots['rejected']({error: this.error}) } if (this.resolved) { return this.$scopedSlots['resolved']({data: this.data}) } return this.$scopedSlots['pending']() }
這裡沒有什麼太棘手的事情。 我們只是使用一些if
塊來查找狀態,然後返回正確的作用域槽(通過this.$scopedSlots['SLOTNAME'](...)
)並將相關數據傳遞給槽作用域。 當您不使用模板時,您可以跳過使用.vue
文件擴展名,方法是將 JavaScript 從script
標籤中提取出來,然後將其插入.js
文件中。 在編譯這些 Vue 文件時,這應該會給您帶來非常輕微的性能提升。
這個例子是 vue-promised 的精簡版和稍微調整的版本,我建議不要使用上面的例子,因為它們涵蓋了一些潛在的陷阱。 還有很多其他很好的無渲染組件示例。 Baleada 是一個完整的庫,其中包含提供此類有用功能的無渲染組件。 還有 vue-virtual-scroller 用於根據屏幕上可見的內容控制列表項的呈現,或 PortalVue 用於將內容“傳送”到 DOM 的完全不同部分。
我出去了
Vue 的 slot 將基於組件的開發提升到了一個全新的水平,雖然我已經展示了很多可以使用 slot 的好方法,但還有更多的方式。 你能想到什麼好主意? 您認為老虎機可以通過哪些方式升級? 如果您有任何想法,請務必將您的想法帶給 Vue 團隊。 上帝保佑編碼愉快。