為什么提出這個復雜的問題?
在我們的應用程序中有一個頂欄,其中包含各種按鈕、一個搜索欄和其他一些控件。
它顯示的內容根據你所在的頁面略有差異,因此我們需要一種按頁配置它的方法。
為此,我們希望每個頁面都能配置頂欄。
看起來很簡單,但這里有一個問題:這個頂欄(我們稱之為 ActionBar)實際上是主布局骨架的一部分,它長成這樣:
<template> <div> <FullPageError /> <ActionBar /> <App /> </div> </template>
這里根據你所在的頁面 / 路徑動態注入 App。
ActionBar 有一些插槽,我們可以用它來作配置。但我們如何從 App 組件控制這些插槽呢?
定義問題
首先應該搞清楚我們究竟想要解決什么問題。
我們來看一個組件,它包含一個子組件和一個插槽:
// Parent.vue
<template> <div> <Child /> <slot /> </div> </template>
我們可以像這樣填充 Parent 的插槽:
// App.vue
<template> <Parent> <p>This content goes into the slot</p> </Parent> </template>
這里沒有什么太花哨的…
填充子組件的插槽很容易,這就是常見的插槽用法。
但有沒有辦法可以從 Child 組件內部控制進入 Parent 組件 slot 的內容呢?
更一般地說:
我們可以讓一個子組件來填充父組件的插槽嗎?
我們來看看我想出的第一個解決方案。
Props down,events up
看到這個問題,我的第一反應就是我們經常說的一句口頭禪:
Props down,events up
數據只能使用 props 才能通過組件樹向下流動。你只能靠發送 events 來回傳數據給樹。
也就是說如果我們需要讓子組件與父組件通信,就會使用事件。
所以我們會使用事件將內容傳遞到 ActionBar 插槽!
在每個應用程序組件中,我們都需要執行以下操作:
importSlotContentfrom'./SlotContent'; exportdefault{ name:'Application', created() { //Assoonasthis componentiscreated we'll emit our events this.$emit('slot-content', SlotContent); } }; {1}
我們將要放入插槽的內容打包成 SlotContent 組件(名稱不重要)。一旦創建了應用程序組件,我們就會發出 slot-content 事件,並傳遞我們想要使用的組件。
我們的骨架組件如下所示:
<template> <div> <FullPageError/> <ActionBar> <Component:is="slotContent"/> </ActionBar> <App@slot-content="component => slotContent = component"/> </div> </template>
它將偵聽該事件,並將 slotContent 設置為我們的 App 組件發送來的內容。然后我們使用內置 Component 動態渲染該組件。
通過事件傳遞組件感覺很奇怪,因為它並不是我們的應用程序中“發生”的事情。這只是應用程序的一種設計方式。
還好有一種方法可以不用這么多事件。
尋找其他選項
由於 Vue 組件就是 JavaScript 對象,我們可以添加想要的任何屬性。
我們可以將它作為字段添加到組件中,這樣就無需使用事件傳遞插槽內容了:
importSlotContentfrom'./SlotContent'; exportdefault{ name:'Application', slotContent: SlotContent, props: {/***/}, computed: {/***/}, };
我們得稍微改變一下骨架中訪問此組件的方式:
<template> <div> <FullPageError/> <ActionBar> <Component:is="slotContent"/> </ActionBar> <App/> </div> </template>
importAppfrom'./App'; importFullPageErrorfrom'./FullPageError'; importActionBarfrom'./ActionBar'; exportdefault{ name:'Scaffold', components: { App, FullPageError, ActionBar, } data() { return{ slotContent: App.slotContent, } }, };
這更像是靜態配置,更好看更整潔。
但這樣還是不對。
理想情況下,我們不會在代碼中混合使用范例,並且所有內容都會是聲明式的。
但在這里,我們不是將組件組合在一起,而是將它們作為 JavaScript 對象傳遞。
我們最好用正常的 Vue 方式讓想要的內容出現在插槽里。
考慮一下 portal
這里就可以用到 portal 了。
它們完全按你期望的那樣行事。你可以把任何內容從某處傳送到另一處。在本文的示例中,我們是從其他地方的 DOM 中的一個位置“傳送”元素過來。
無論組件樹是什么樣的,我們都能夠控制組件在 DOM 中渲染的位置。
例如,假設我們想要填充模態。但是我們的模態必須在頁面的根部渲染,這樣才能正確地覆蓋它。首先,我們將指定想要放在模態中的內容:
<template> <div> <!-- Other components --> <Portalto="modal"> Rendered in the modal. </Portal> </div> </template>
然后在我們的模態組件中放另一個 portal 來渲染該內容:
<template> <div> <h1>Modal</h1> <Portalfrom="modal"/> </div> </template>
這肯定是一種改進,因為現在我們實際上是在編寫 html 而不是簡單地傳遞對象。這種方法更具聲明性,並且更容易看到應用程序中發生了什么。
但有些情況下看到應用內發生了什么並不那么容易。
因為 portal 在幕后做了一些手腳來渲染不同位置的元素,所以它完全打破了 Vue 中渲染 DOM 的機制。看起來你在正常渲染元素,但它根本不能正常工作。這可能會帶來很多麻煩和陷阱。
這里還有一個大問題,我們稍后會講。
很顯然,至少在將組件添加到 $options 屬性時事情是不一樣的。
我認為還有更好的方法。
狀態提升
“狀態提升”是一個前端開發圈子所用的術語。
它的意思是你將狀態從子組件移動到父組件或祖父組件。你是順着組件樹向上移動。
這會對應用程序的體系結構產生深遠的影響。而對於我們的目的來說,它實際上開辟了一個完全不同的,更簡單的解決方案。
這里的“狀態”是我們試圖傳遞到 ActionBar 組件槽中的內容。
但是該狀態包含在 Page 組件里,我們無法將頁面特定的邏輯移動到布局組件中。我們的狀態必須保持在我們動態渲染的 Page 組件中。
所以我們必須提升整個 Page 組件才能提升狀態。
目前我們的 Page 組件是 Layout 組件的子組件:
<template> <div> <FullPageError /> <ActionBar /> <Page /> </div> </template>
我們得調換它們的關系才能提升它,讓 Layout 組件成為 Page 組件的子組件。我們的 Page 組件看起來像這樣:
<template> <Layout> <!-- Page-specific content --> </Layout> </template>
我們的 Layout 組件現在看起來像這樣,這里可以使用插槽來插入頁面內容:
<template> <div> <FullPageError /> <ActionBar /> <slot /> </div> </template>
但這並不能讓我們定制任何內容。我們必須在 Layout 組件中添加一些命名槽,以便傳入應該放入 ActionBar 的內容。
最簡單的方法是使用一個槽來完全替換 ActionBar 組件:
<template> <div> <FullPageError/> <slotname="actionbar"> <ActionBar/> </slot> <slot/> </div> </template>
這樣,如果你沒有指定“actionbar”插槽,我們將用默認的 ActionBar 組件。但你仍然可以使用自己自定義的 ActionBar 配置覆蓋此插槽:
<template> <Layout> <template#actionbar> <ActionBar> <!-- Custom content that goes into the action bar --> </ActionBar> </template> <!-- Page-specific content --> </Layout> </template>
對我來說這是一條理想的路徑,但它確實需要你重構頁面布局。這可能是一項艱巨的任務,具體取決於你的應用程序的構建方式。
如果你用不了這種方法,下一個備選方案應該是 2 號,也就是使用 $options 屬性。它是最干凈的方法,最容易被閱讀代碼的人理解。
廣州品牌設計公司https://www.houdianzi.com PPT模板下載大全https://redbox.wode007.com
還可以更簡單一些
前面我們定義問題時是用比較宏觀的方式來描述的:
我們可以讓子組件來填充父組件的插槽嗎?
但實際上這個問題與具體的 props 無關。更簡單來說是讓一個子組件控制在它自己的子樹之外渲染的內容。
把這個問題根據最常見的場景改寫一下:
組件控制在其子樹外渲染的內容的最佳方法是什么?
換成這個角度來審視我們之前的方案,就能得出一些有趣的新觀點。
將事件發送給父組件
因為我們的組件不能直接影響在它的子樹外發生的事情,所以我們要找這樣的一個組件;其子樹包含我們試圖控制的目標元素。
然后讓它為我們解決問題就行了。
靜態配置
我們只要向其他組件提供必要的信息就行了,不用主動要求其他組件代表我們做事。
Portal
前面這三種方法有一種共有模式,
所以可以做出如下斷言:
組件無法控制其子樹之外的東西。
(它的證明留給讀者練習)
所以這里的方法都是用某種手段讓另一個組件處理我們的需求,並控制我們真正感興趣的那個元素。
之所以 Portal 在這里表現更好,是因為它允許我們將所有這些通信邏輯封裝到單獨的組件中。
狀態提升
終於輪到真正不一樣的方案了,狀態提升是比之前那幾個方法更簡單、更強大的技術。
我們的主要障礙在於我們想要控制的東西是在子樹之外。
最簡單的解決方案:
將目標元素移動到子樹中,這樣我們就可以控制它了!
狀態提升——以及操縱該狀態的邏輯——讓我們可以擁有更大的子樹,並把我們的目標元素包含在該子樹中。
如果你能做到這一點,這就是解決這種問題和相關的一系列問題的最簡單方法。
記住,這並不一定要提升整個組件。你還可以重構應用程序,將一段邏輯移動到樹中更高的組件中。
這真的只是依賴注入而已
很熟悉軟件工程設計模式的讀者可能已經注意到,我們在這里所做的就是依賴注入——這是我們在軟件工程中已經使用了數十年的技術。
它的一個用途是用來制作易於配置的代碼。在我們的示例中,我們對每個 Page 所用的 Layout 組件給出了不一樣的配置。
當我們調換 Page 和 Layout 組件的關系時,這種做法就是所謂的控制反轉。
在基於組件的框架中,父組件控制子組件的行為(因為它在前者的子樹內),因此我們選擇讓 Page 控件控制 Layout 組件,而不是讓 Layout 組件控制 Page。
為了做到這一點,我們為 Layout 組件提供了插槽。
正如你所見,使用依賴注入可以使我們的代碼更加模塊化、更易於配置。