Vue 插槽 & 高複用元件
# 前言
元件是Vue插槽中最為關鍵的一個特性之一,而元件的API分三大模組
- Props: 允許外環境給元件傳遞資料,
- Events: 允許元件觸發外部環境的副作用 和
- 插槽: 允許內外環境作用於交叉,將額外的內容組合到元件中去。
正確理解了這三個,相信你能寫出很優美的元件
本文主要描述
- 對插槽的理解
- 匿名插槽
- 具名插槽
- 作用域插槽
- 編寫高複用元件的幾點思路
# 為什麼用插槽
元件的最大特性就是複用性,而用好插槽能大大提高元件的可複用能力。
元件的複用性常見情形如“在有相似功能的模組中,他們具有類似的UI介面,通過使用元件間的通訊機制傳遞資料,從而達到一套程式碼渲染不同資料的效果”。
然而這種利用元件間通訊的機制只能滿足在結構上相同,渲染資料不同的情形;假設兩個相似的頁面,他們只在某一模組有不同的UI效果,以上辦法就做不到了。可能你會想,使用 v-if
和 v-else
來特殊處理這兩個功能模組,不就解決了?很優秀,解決了,但不完美。極端一點,假設我們有一百個這種頁面,就需要寫一百個 v-if
、 v-else-if
、 v-else
來處理?那元件看起來將不再簡小精緻,維護起來也不容易。
而 插槽 “ SLOT
”就可以完美解決這個問題
# 什麼情況下使用插槽
顧名思義,插槽即往卡槽中插入一段功能塊。還是舉剛才的例子。如果有一百個基本相似,只有一個模組功能不同的頁面,而我們只想寫一個元件。可以將不同的那個模組單獨處理成一個卡片,在需要使用的時候將對應的卡片插入到元件中即可實現對應的完整的功能頁。而不是在元件中把所有的情形用 if-else
羅列出來
可能你會想,那我把一個元件分割成一片片的插槽,需要什麼拼接什麼,豈不是隻要一個元件就能完成所有的功能?思路上沒錯,但是需要明白的是,卡片是在父元件上代替子元件實現的功能,使用插槽無疑是在給父元件頁面增加規模,如果全都使用拼裝的方式,和不用元件又有什麼區別。因此,插槽並不是用的越多越好。
插槽是元件最大化利用的一種手段,而不是替代元件的策略,當然也不能替代元件。如果能在元件中實現的模組,或者只需要使用一次 v-else
, 或一次 v-else-if
, v-else
就能解決的問題,都建議直接在元件中實現。
# 準備工作
使用插槽前,需要先了解什麼是 編譯作用域 , 即
父元件模板的內容在父元件的作用域內編譯,子元件模板的內容在子元件的作用域內編譯
什麼意思?假設有如下案例
// 父元件 <template> <p>{{ greet }}</p> <child-component :data="myData"> {{ messages }} </child-component> </template>
// 元件child-component <template> <div> <p>{{ myName }}</p> <slot></slot> </div> </template>
在父元件作用域中參與編譯的內容有:(1) 父元件 P
標籤的 greet
。(2)【 重點上 】 變數 message
; (3) 變數 myData
;
在子元件中參與編譯的內容有:(1)子元件 p
標籤中的 myName
。(2) 【 重點下 】 子元件標籤中的 data
屬性
需要強調的是,【重點上】中的存在於父元件編譯作用於上的 message
部分也就是插槽內容,是不能訪問存在於子元件【重點下】中的 data
屬性的,如果需要訪問這部分內容,需要使用到 作用域插槽
功能
上面提到過一個觀點:“卡片是在父元件上代替子元件實現的功能,使用插槽無疑是在給父元件頁面增加規模”。從上面案例中也可以看出,子元件只提供了插槽 <slot>
,而具體什麼內容它並不管,都交給了父元件作用於中存在於 <child-component>
包含的那部分內容去分發。這部分內容,就是我們所說的卡片
# 單個插槽 (匿名插槽)
在沒有使用插槽前,我們在元件內部寫入的內容都會被拋棄,原因很簡單,在父元件渲染的時候,會使用子元件裡的內容來替換它在父元件的佔位。如果不想被丟棄,就需要在子元件中使用單個插槽來接收內容
單個插槽一般都是匿名的,當然也可以給他命名,命名的方式使用 slot="slotName"
// 父元件中定義卡片 <div> <h1>父元件</h1> <child-component> <p>卡片內容1</p> <p>卡片內容2</p> </child-component> </div>
// child-component元件中使用slot接收 <div> <h2>子元件</h2> <slot> 插槽預設內容 </slot> </div>
在案例中除了有卡片內容與插槽內容,我們還看到了在 <slot>
中定義的一段話,它是插槽標籤的預設內容,會在子元件編譯作用域內編譯,只有當宿主元素為空,且沒有相應的插入內容時才顯示。上面的案例我們可以得到如下結果:
// 渲染結果: <div> <h1>父元件</h1> <div> <h2>子元件</h2> <p>卡片內容1</p> <p>卡片內容2</p> </div> </div>
# 具名插槽
我們可以給插槽定義名字,使其成為具名插槽。在單個插槽中,會將父元件中所有的卡片(假設都沒有命名)按其在父元件中定義的順序都接收過來;
而 具名插槽則是接收指定的卡片 。這樣,我們就可以在不同位置定義多個插槽,分別用來接收不同的卡片內容。也可以增加一個匿名插槽,用來接收父元件編譯作用域中未被指定名稱的卡片內容(剩餘內容),從而達到卡片的最大化利用。
在父元件中,通過使用 slot = "slotName"
來給卡片內容命名,如下案例中,我們將內容分成了兩個卡片,一個卡片名為 header
, 另一個為 footer
。需要注意的是, 包含 slot
的標籤元素也會被插入到卡槽中。如案例中的 div
<div> <child-component> <div slot="header"> <h2>插槽標題</h2> </div> <div>沒被命名的“剩餘”內容一</div> <div slot="footer"> <p>版權所有,翻版我也沒辦法</p> </div> <div>沒有被命名的“剩餘”內容二</div> </child-component> </div>
卡片我們設定好了,接下來設定接收的卡槽
// child-component 中的內容 <div> <slot name="header"></slot> <div> <p>這裡是元件實現頁面相似的功能模組的地方</p> </div> // 定義預設的卡槽用來存放“剩餘”內容 <slot></slot> <slot name="footer"></slot> </div>
# 作用域插槽
作用域插槽( Scope slot
)是Vue中很重要的一個特性,可以使元件更加的通用,複用性更高。但因為它存在父子作用域的交織關係,使得元件難以理解。
再次回到 編譯作作用域 的內容,父元件模板的所有東西都會在父級作用域內編譯;子元件模板的所有東西都會在子級作用域內編譯。存在於父元件編譯作用域內的內容不能訪問子元件作用域中的變數,反之亦然。但是我們知道,插槽的內容其實是為了實現子元件的定製化設計,因此往往有部分資訊需要子元件中的資料來控制或渲染,這時就需要知道子元件會傳遞一些什麼資訊過來。
既然需要根據子元件的資訊來決定卡片的內容,我們需要將子元件中整個待定製模組的內容寄託給一個 slot
,讓它在父元件卡片上實現,然後將這個 slot
所需要的資料也傳遞出去。此時出現了一個待解決問題就是:需要將子元件編譯作用域中的資料讓其在父元件作用域中生效 。Vue的作用域插槽就是來解決這個問題的。
v2.1.0 版本使用(且必須用) template 對卡片內容進行統一包裝,並使用 slot-scope
(以前使用 scope
)屬性來接收子元件傳出的資料
為了更好的對比,回顧一下常規的 <todo-list>
如下
// 子元件中... <ul> <li v-for="todo in todos" :key="todo.id"> {{ todo.text }} </li> </ul>
如果在子元件中我們如此設計,那麼只能實現某一種列表形式的列表結構頁面,假設我們頁面每行需要多一個圖示,就無法複用這個元件了,怎麼辦?我們可以抽出行元素到父元件中進行定製化設計,誰需要單獨設計就抽出來,不需要的就是用預設結構
// 子元件中... <ul> <li v-for="item in todos" :key="item.id"> // 將這部分需要定製化的內容外傳,並把所需的資料以屬性形式外傳 // 這種屬性外傳的形式和 父元件給子元件傳遞資料的思路非常相似,因此父元件接收處也常被命名為props,當然命名可以隨意取。 <slot :todo="item">這裡是預設內容<slot> </li> </ul>
// 父元件 <div> <todo-list :todos="todos"> // 定義一個template來包裹卡片內容,並利用slot-scope屬性來接收子元件的資料 <template slot-scope="props" > <span v-if="props.todo.isComplete">✓</span> {{ props.todo.text }} </template> </todo-list> </div>
在父元件中的 slot-scope
中定義的變數是父元件編譯作用域中的臨時變數,用來存放從子元件中傳遞過來的 props
物件。該物件中,定義在子元件上的屬性作為 props
的鍵,如 <slot :todo="item"><slot>
中的 todo
; 子元件中屬性對應的值作為 props
的值,如 <slot :todo="item"><slot>
中的 item
。當然這裡的 item
是一個物件。
在 2.5.0+,slot-scope 不再限制在 <template> 元素上使用,而可以用在插槽內的任何元素或元件上。
在 2.5.0+ 版本里,上面的例子可以這麼寫
// 父元件 <div> <todo-list :todos="todos"> // 不需要再使用template來包裹卡片內容,直接將slot-scope定義在span上(這裡稍作改動假設文字在span中) <span slot-scope="props">{{ props.todo.text }}</span> </todo-list> </div>
# 利用結構賦值簡化程式碼
利用es6的解構賦值特性,可以使得程式碼結構更加清晰易懂,作用域插槽也變得更乾淨一些
<todo-list v-bind:todos="todos"> <template slot-scope="{ todo }"> <span v-if="todo.isComplete">✓</span> {{ todo.text }} </template> </todo-list>
# 如何編寫一個高複用的元件
Vue 作為一套構建使用者的漸進式框架,倡導使用簡單的API來實現響應式的資料來繫結和組合檢視元件。然而因為vue的語法自由,方案眾多,不同人解決問題的思路不一樣,寫出來的程式碼自然有差別,如果是多人開發,就容易造成規範不統一,自成一套的問題。
對於業務量較小的系統,元件的可複用性和規模編寫影響並不大,但隨著業務程式碼日益龐大,元件必將會越來越多,元件邏輯的耦合性也更加嚴重,容易出現維護困難,牽一髮而動全身的困惱。筆者查閱了相關資料書籍結合自身的理解,得出如下幾個要點。
0. 說明 - 元件職責
元件根據其用處可粗略分為兩類:一類是 通用元件 (可複用元件)即本章重點,一類是 業務元件 (幾乎為一次性元件)。Vue提倡將頁面劃分成不同的模組,將每一個模組封裝成一個元件。這種思路決定了不可能所有的元件都是通用元件,必然存在一些一次性的業務元件,封裝它們的目的是為了提高程式碼的可讀性和易維護性。
雖說有這兩類,但並沒有一條特別清晰的分界線,原因是Vue元件的編寫極具藝術性,通過Vue語法的巧妙利用,典型代表就是「作用域插槽」,理想情況下能將業務元件拆分成一個插槽的卡片內容,但這也存在難度。這也是為什麼稱Vue是漸進式框架的原因
可複用元件實現通用的功能(無關使用位置,使用場景的變化)
- UI 效果展示
- 與使用者的互動 (如點選事件)
- CSS特效如動畫效果
業務元件則實現偏向業務話的功能
- 獲取資料
- 和vuex相關的操作(不應該在通用元件中出現)
- 埋點功能
- 引用可複用元件
1. 業務無關
元件的命名應該和業務無關,而是根據功能命名。
假設有一個團隊列表,需要把每一項作為一個元件,你可能會想使用Team。這時,有另一個需求要求展示為每一個人員贈送的節日禮物列表,再使用這個Team元件顯然感覺不合適。
關於如何智慧的命名,給一個建議: 可以借用ElementUI等這類UI框架的規範,他們實質上也是對Vue元件的一些封裝,可以學習他們的做法。 舉個例子如 Item
, ListItem
, Cell
等命名
2. 資料無關
編寫的元件應該儘可能的無狀態,除非真實具有某些適普功能的特殊元件。應儘量不要在元件內部去獲取業務資料,以及任何與伺服器端打交道的操作,這將嚴重縮小元件的可用範圍。
3.名稱空間
可複用元件除了定義一個清晰的公開介面,還需要有名稱空間,避免與瀏覽器保留的標籤和其他元件發生衝突。特別是當專案引用外部UI或遷移到其他專案時,也能解決很多命名衝突問題。名稱空間建議使用專案名稱的縮寫。
當然,業務元件也建議有名稱空間
上下文無關
所謂上下文無關並不是說全無關,而是儘可能減少對外部環境的依賴。雖說Vue是拆分元件,拆分模組的思想,但並不是無意義拆分。並不希望把一個具有獨立功能的元件按照他的模組拆散,這樣不進增加了無意義的資料傳輸,還不利於上下文無關特性。
資料扁平化
傳遞資料時,不要將整個物件作為一個prop傳遞進來。很常見的一個現象就是
<child :data="resData"></child>
然後 resData
的結構為一個JS物件。這麼做不是不行,而是有一些弊端。
(1)元件的介面不清晰,甚至需要寫註釋才能看明白這組資料如何處理。
(2)props 校驗無法校驗物件內部的屬性型別
(3)當伺服器端返回的物件中帶有的key與元件介面不一致時,需要手動轉換或構建。
當然,這是一把雙刃劍,當需要渲染的資料欄位不多時,提倡使用扁平資料分格。如下
<child :title="resData.title" :describe="resData.describe" :author="resData.author"></child>
專案骨架
單元件不異過重,元件在功能獨立的前提下應該儘量簡單,越簡單的元件可複用性越強。當你實現元件的程式碼,不包括CSS,有好幾百行了(這個大小視業務而定),那麼就要考慮拆分成更小的元件。
當元件足夠簡單時,就可以在一個更大的業務元件中去自由組合這些元件,實現我們的業務功能。因此,理想情況下,元件的引用層級,只有兩級。業務元件引用通用元件。

而對於一個龐大的專案,必然會有更深層的元件巢狀,此時建議將業務層元件和通用元件分離

使用插槽將[業務元件]剝離成[通用元件]
插槽絕對是Vue中的利器。通過插槽我們不難將一個業務元件剝離出公用部分成為通用元件,通過 slot
再將所需要的業務內容插入對應插槽中。如下案例
// 元件two-col-layout <template> <ul slot="content" v-if="Lists.length"> <li v-for="item in Lists" :key="item.id"> <div class="l"> <slot name="left" :item="item">圖片區域</slot> </div> <div class="r"> <slot name="right" :item="item">詳情區域</slot> </div> </li> <slot name="after"></slot> </ul> </template>
案例中展示的是一個兩列布局的通用元件。其設定了左邊欄為圖片展示區域,右邊欄為詳情展示區域。但是關於這兩欄具體資訊如何展示,那是業務元件需要乾的事情。
- 案例中的元件與業務無關:他不關心頁面需要些什麼,詳情區域會放些什麼東西,有幾欄,而是將這些交給父元件實現。
- 與資料無關:他同樣不關心資料是什麼樣的,有些什麼欄位,欄位名是什麼,他只關心資料型別能通過Props驗證即可。畢竟這裡需要做
v-for
迴圈。 - 與上下文無關:告訴該元件一個數據名稱即可,它只做資料轉交工作
- 結構扁平:他將業務資訊交回給父元件完成,因此自己不需要做太多的子元件封裝,也就避免了多層元件巢狀
- 命名規範:名稱根據元件的功能命名,兩列布局
two-col-layout
,很容易看懂。
# 結束語
Vue為漸進式框架,上手簡單並不代表這門技術就簡單。經常複習官網和查閱相關書籍,會發現不同的東西。太多時候埋頭於寫業務程式碼,而忽略了對這門極具藝術的語言有較多的研究。多思考,虛心學,或許你會覺得,越學,不會的越多~那就對了