Vue 插槽 & 高復(fù)用組件

2019-3-4更新】Vue 2.6+修改了部分語法,對(duì)插槽的使用有了較多的更新。在本文中筆者在相應(yīng)位置給出了更新提示和新的推薦用法


# 前言

? 組件是Vue插槽中最為關(guān)鍵的一個(gè)特性之一,而插槽是組件的一大亮點(diǎn)。本文主要描述

  • 對(duì)插槽的理解
  • 匿名、具名、作用域插槽
  • 編寫高復(fù)用組件的幾點(diǎn)思路
    ?

# 為什么用插槽

? 組件的最大特性就是復(fù)用性,而用好插槽能大大提高組件的可復(fù)用能力。

? 組件的復(fù)用性常見情形如在有相似功能的模塊中,他們具有類似的UI界面,通過使用組件間的通信機(jī)制傳遞數(shù)據(jù),從而達(dá)到一套代碼渲染不同數(shù)據(jù)的效果

? 然而這種利用組件間通信的機(jī)制只能滿足在結(jié)構(gòu)上相同,渲染數(shù)據(jù)不同的情形;假設(shè)兩個(gè)相似的頁面,他們只在某一模塊有不同的UI效果,以上辦法就做不到了。可能你會(huì)想,使用 v-ifv-else來特殊處理這兩個(gè)功能模塊,不就解決了?很優(yōu)秀,解決了,但不完美。極端一點(diǎn),假設(shè)我們有一百個(gè)這種頁面,就需要寫一百個(gè)v-ifv-else-ifv-else來處理?那組件看起來將不再簡小精致,維護(hù)起來也不容易。

? 而 插槽 “SLOT”就可以完美解決這個(gè)問題
?

# 什么情況下使用插槽

? 顧名思義,插槽即往卡槽中插入一段功能塊。還是舉剛才的例子。如果有一百個(gè)基本相似,只有一個(gè)模塊功能不同的頁面,而我們只想寫一個(gè)組件。可以將不同的那個(gè)模塊單獨(dú)處理成一個(gè)卡片,在需要使用的時(shí)候?qū)?duì)應(yīng)的卡片插入到組件中即可實(shí)現(xiàn)對(duì)應(yīng)的完整的功能頁。而不是在組件中把所有的情形用if-else羅列出來

? 可能你會(huì)想,那我把一個(gè)組件分割成一片片的插槽,需要什么拼接什么,豈不是只要一個(gè)組件就能完成所有的功能?思路上沒錯(cuò),但是需要明白的是,卡片是在父組件上代替子組件實(shí)現(xiàn)的功能,使用插槽無疑是在給父組件頁面增加規(guī)模,如果全都使用拼裝的方式,和不用組件又有什么區(qū)別。因此,插槽并不是用的越多越好

? 插槽是組件最大化利用的一種手段,而不是替代組件的策略,當(dāng)然也不能替代組件。如果能在組件中實(shí)現(xiàn)的模塊,或者只需要使用一次v-else, 或一次v-else-ifv-else就能解決的問題,都建議直接在組件中實(shí)現(xiàn)。
?

# 準(zhǔn)備工作

? 使用插槽前,需要先了解什么是編譯作用域, 即

父組件模板的內(nèi)容在父組件的作用域內(nèi)編譯,子組件模板的內(nèi)容在子組件的作用域內(nèi)編譯

? 什么意思?假設(shè)有如下案例

// 父組件
<template>
  <p>{{ greet }}</p>
  <child-component :data="myData">
    {{ messages }}      // Vue 2.6+ 將該行定義為“后備內(nèi)容”
  </child-component>
</template>
// 組件 child-component
<template>
  <div>
    <p>{{ myName }}</p>
    <slot></slot>
  </div>
</template>

? 在父組件作用域中參與編譯的內(nèi)容有:(1) 父組件P標(biāo)簽的greet。(2)【變量 message; (3) 變量myData
? 在子組件中參與編譯的內(nèi)容有:(1)子組件 p 標(biāo)簽中的myName。(2) 【子組件<child-component>中的data特性

? 需要強(qiáng)調(diào)的是,【上】中的存在于父組件編譯作用于上的message部分也就是插槽的后備內(nèi)容,是存在于父組件作用域內(nèi),該部分是不能訪問存在于子組件作用域【下】中的data特性的,如果需要訪問這部分內(nèi)容,需要使用到作用域插槽功能

? 上面提到過一個(gè)觀點(diǎn):卡片是在父組件上代替子組件實(shí)現(xiàn)的功能,使用插槽無疑是在給父組件頁面增加規(guī)模。從上面案例中也可以看出,子組件只提供了插槽<slot>,而具體什么內(nèi)容它并不管,都交給了父組件作用于中存在于<child-component>包含的那部分內(nèi)容去分發(fā)。這部分內(nèi)容,就是我們所說的卡片
?

# 單個(gè)插槽 (匿名插槽)

? 在沒有使用插槽前,組件內(nèi)部寫入的后備內(nèi)容都會(huì)被拋棄,原因很簡單,在父組件渲染的時(shí)候,會(huì)使用子組件里的內(nèi)容來替換它在父組件的占位。如果不想被丟棄,就需要在子組件中使用單個(gè)插槽來接收內(nèi)容

? 單個(gè)插槽一般都是匿名的,當(dāng)然也可以給他命名,默認(rèn)未命名情況下,Vue2.6+版本默認(rèn)為v-slot:default或簡寫#default

// 父組件中定義卡片
<div>
    <h1>父組件</h1>
    <child-component>
        <p>卡片內(nèi)容1</p>
        <p>卡片內(nèi)容2</p>
    </child-component>
</div>
// child-component組件中使用slot接收
<div>
    <h2>子組件</h2>
    <slot>
        插槽默認(rèn)內(nèi)容
    </slot>
</div>

在案例中除了有卡片內(nèi)容與插槽內(nèi)容,我們還看到了在<slot>中定義的一段話,它是插槽標(biāo)簽的默認(rèn)內(nèi)容,會(huì)在子組件編譯作用域內(nèi)編譯,只有當(dāng)宿主元素為空,且沒有相應(yīng)的插入內(nèi)容時(shí)才顯示。上面的案例我們可以得到如下結(jié)果:

// 渲染結(jié)果:
<div>
    <h1>父組件</h1>
    <div>
        <h2>子組件</h2>
        <p>卡片內(nèi)容1</p>
        <p>卡片內(nèi)容2</p>
    </div>
</div>

?

# 具名插槽 (Vue2.6+有更新)

? 我們可以給插槽定義名字,使其成為具名插槽。在單個(gè)插槽中,會(huì)將父組件中所有的卡片(假設(shè)都沒有命名)按其在父組件中定義的順序都接收過來;

? 而具名插槽則是接收指定的卡片。這樣,我們就可以在不同位置定義多個(gè)插槽,分別用來接收不同的卡片內(nèi)容。也可以增加一個(gè)匿名插槽,用來接收父組件編譯作用域中未被指定名稱的卡片內(nèi)容(剩余內(nèi)容)。

Vue2.6+版本中,要求對(duì)所有的具名插槽的v-slot都添加在一個(gè)<template>上,除非當(dāng)被提供的內(nèi)容只有默認(rèn)插槽時(shí)才能直接用在組件上。另外只有默認(rèn)插槽時(shí)可以省略v-slot:default中的default。注意這兩種情況都只適用于只有默認(rèn)插槽的情況下,一般都不建議使用

? 在父組件中,通過使用【Vue2.5用法:即將廢棄slot = "slotName"Vue2.6+用法v-slot:name或簡寫#name來給卡片內(nèi)容命名,如下案例中,我們將內(nèi)容分成了兩個(gè)卡片,一個(gè)卡片名為header, 另一個(gè)為footer。需要注意的是,包含slot的標(biāo)簽元素也會(huì)被插入到卡槽中。如案例中的div標(biāo)簽

<div>
  <child-component>
    <template v-slot:header>   // 這里使用插槽語法全稱方式
      <div>
        <h2>插槽標(biāo)題</h2>
      </div>

    <div>沒被命名的“剩余”內(nèi)容一</div>

    <template #footer>  // 這里使用插槽語法簡寫方式
      <div #footer>
        <p>版權(quán)所有,翻版我也沒辦法</p>
      </div>
    </template>

    <div>沒有被命名的“剩余”內(nèi)容二</div>
  </child-component>
</div>

? 強(qiáng)烈建議將“剩余”內(nèi)容寫在一起,并使用<template>包裹起來,規(guī)范的話再加入#default。卡片我們?cè)O(shè)定好了,接下來設(shè)定接收的插槽

// child-component 中的內(nèi)容
<div>
  <slot name="header"></slot>

  <div>
    <p>這里是組件實(shí)現(xiàn)頁面相似的功能模塊的地方</p>
  </div>

  // 定義默認(rèn)的卡槽用來存放“剩余”內(nèi)容
  <slot></slot>

  <slot name="footer"></slot>
</div>

?

# 作用域插槽

? 讓插槽內(nèi)容能夠訪問子組件中才有的數(shù)據(jù)是很有用的,作用域插槽(Scope slot)就是這么一個(gè)特性,它可以使組件更加的通用,復(fù)用性更高。但因?yàn)樗嬖诟缸幼饔糜虻慕豢楆P(guān)系,使得組件難以理解。

v2.1.0 版本使用(且必須用) <template> 對(duì)卡片內(nèi)容進(jìn)行統(tǒng)一包裝,并使用slot-scope(以前使用scope)屬性來接收子組件傳出的數(shù)據(jù)。v2.5.0做了修改,可以將slot-scope用在任意標(biāo)簽上,v2.6+之后,又做了一次更新,使用v-slot:slotName="slotProps"形式

為了更好的體現(xiàn)作用于插槽的強(qiáng)大,回顧一下常規(guī)的<todo-list>如下

// 子組件中...
<ul>
  <li v-for="todo in filteredTodos" :key="todo.id">
    {{ todo.text }}
  </li>
</ul>

如果在子組件中我們?nèi)绱嗽O(shè)計(jì),將直接限制todo-list就這一種顯示形式,假如需要一個(gè)ICON,就無法實(shí)現(xiàn)了。我們可以將每個(gè) todo 作為父級(jí)組件的插槽,以此通過父級(jí)組件對(duì)其進(jìn)行控制,然后將 todo 作為一個(gè)插槽 prop 進(jìn)行綁定

<!-- 子組件中  -->
<ul>
  <li v-for="todo in filteredTodos" :key="todo.id">
    <!--
      這種屬性外傳的形式和 父組件給子組件傳遞數(shù)據(jù)的思路非常相似,
      因此父組件接收處也常被命名為slotProps,當(dāng)然命名可以隨意取。
    -->
    <slot :todo="todo">
      這里是后備內(nèi)容
    <slot>
  </li>
</ul>
<!--父組件中  -->
<div>
  <todo-list :todos="todos">
    <!--
    我們?yōu)槊總€(gè) todo 準(zhǔn)備了一個(gè)插槽,
    將 `todo` 對(duì)象作為一個(gè)插槽的 prop 傳入。
    -->
    <!-- Vue2.5及之前用法: <template slot-scope="slotProps" > -->
    <template v-slot:default="slotProps" >   // Vue 2.6+ 指定了來源于哪個(gè)插槽內(nèi)容
      {{ slotProps.todo.text }}
    </template>
  </todo-list>
</div>

在 2.5.0,slot-scope 不限制在 <template> 上使用而可以在任意元素上使用;而Vue 2.6+引入了插槽名,不再建議用在元素上。如果只有默認(rèn)插槽,可以簡寫v-slot:default成為v-slot,或者成為#default。但如果存在任意別的具名插槽,則不再用第一種簡寫。

在Vue 2.6+中,如果只有默認(rèn)插槽,可以這么寫

<!-- 父組件 -->
<div>
  <todo-list v-slot="slotProp">  
    <span>{{ slotProp.todo.text }}</span>
  </todo-list>
</div>

? 如果存在別的插槽,則不能混用,注意,這里也可以使用v-slot的簡寫

<div>
  <todo-list> 
    <template #default="slotProps">
      <span>{{ slotProp.todo.text }}</span>
    </template>
    <template #other="otherSlotProps">
      <span>{{ otherSlotProps.todo.text }}</span>
    </template>
  </todo-list>
</div>

?

# 解構(gòu)插槽prop

? 作用域插槽利用v-bind將想屬性綁定到插槽的特性中,這些特性會(huì)被處在父組件中綁定給slotProp(假設(shè)為該名字)收集,其內(nèi)部工作原理是將你的插槽內(nèi)容包括在一個(gè)傳入單個(gè)參數(shù)的函數(shù)里function(slotProp){ },這意味著v-slot 的值實(shí)際上可以是任何能夠作為函數(shù)定義中的參數(shù)的 JavaScript 表達(dá)式,也就是說我們可以使用ES6的解構(gòu)賦值。

<todo-list :todo="todo">
  <template v-slot="{ todo }">  // 如果簡寫掉了:default,則不能簡寫#,即v-slot 和 #default 二選一
    <span v-if="todo.isComplete">?</span>
    {{ todo.text }}
  </template>
</todo-list>

?

# 插槽變量

? 動(dòng)態(tài)指令參數(shù)也可以用在v-slot上,來定義動(dòng)態(tài)的插槽名

<base-layout>
  <template v-slot:[dynamicSlotName]>
    ...
  </template>
</base-layout>

?

# 如何編寫一個(gè)高復(fù)用的組件

? Vue 作為一套構(gòu)建用戶的漸進(jìn)式框架,倡導(dǎo)使用簡單的API來實(shí)現(xiàn)響應(yīng)式的數(shù)據(jù)來綁定和組合視圖組件。然而因?yàn)関ue的語法自由,方案眾多,不同人解決問題的思路不一樣,寫出來的代碼自然有差別,如果是多人開發(fā),就容易造成規(guī)范不統(tǒng)一,自成一套的問題。

? 對(duì)于業(yè)務(wù)量較小的系統(tǒng),組件的可復(fù)用性和規(guī)模編寫影響并不大,但隨著業(yè)務(wù)代碼日益龐大,組件必將會(huì)越來越多,組件邏輯的耦合性也更加嚴(yán)重,容易出現(xiàn)維護(hù)困難,牽一發(fā)而動(dòng)全身的困惱。筆者查閱了相關(guān)資料書籍結(jié)合自身的理解,得出如下幾個(gè)要點(diǎn)。

0. 說明 - 組件職責(zé)

? 組件根據(jù)其用處可粗略分為兩類:一類是通用組件(可復(fù)用組件)即本章重點(diǎn),一類是業(yè)務(wù)組件(幾乎為一次性組件)。Vue提倡將頁面劃分成不同的模塊,將每一個(gè)模塊封裝成一個(gè)組件。這種思路決定了不可能所有的組件都是通用組件,必然存在一些一次性的業(yè)務(wù)組件,封裝它們的目的是為了提高代碼的可讀性和易維護(hù)性。

? 雖說有這兩類,但并沒有一條特別清晰的分界線,原因是Vue組件的編寫極具藝術(shù)性,通過Vue語法的巧妙利用,典型代表就是「作用域插槽」,理想情況下能將業(yè)務(wù)組件拆分成一個(gè)插槽的卡片內(nèi)容,但這也存在難度。這也是為什么稱Vue是漸進(jìn)式框架的原因

可復(fù)用組件實(shí)現(xiàn)通用的功能(無關(guān)使用位置,使用場景的變化)

  • UI 效果展示
  • 與用戶的交互 (如點(diǎn)擊事件)
  • CSS特效如動(dòng)畫效果

業(yè)務(wù)組件則實(shí)現(xiàn)偏向業(yè)務(wù)話的功能

  • 獲取數(shù)據(jù)
  • 和vuex相關(guān)的操作(不應(yīng)該在通用組件中出現(xiàn))
  • 埋點(diǎn)功能
  • 引用可復(fù)用組件
1. 業(yè)務(wù)無關(guān)

? 組件的命名應(yīng)該和業(yè)務(wù)無關(guān),而是根據(jù)功能命名。

? 假設(shè)有一個(gè)團(tuán)隊(duì)列表,需要把每一項(xiàng)作為一個(gè)組件,你可能會(huì)想使用Team。這時(shí),有另一個(gè)需求要求展示為每一個(gè)人員贈(zèng)送的節(jié)日禮物列表,再使用這個(gè)Team組件顯然感覺不合適。

? 關(guān)于如何智慧的命名,給一個(gè)建議: 可以借用ElementUI等這類UI框架的規(guī)范,他們實(shí)質(zhì)上也是對(duì)Vue組件的一些封裝,可以學(xué)習(xí)他們的做法。 舉個(gè)例子如 ItemListItemCell等命名

2. 數(shù)據(jù)無關(guān)

? 編寫的組件應(yīng)該盡可能的無狀態(tài),除非真實(shí)具有某些適普功能的特殊組件。應(yīng)盡量不要在組件內(nèi)部去獲取業(yè)務(wù)數(shù)據(jù),以及任何與服務(wù)器端打交道的操作,這將嚴(yán)重縮小組件的可用范圍。

3.命名空間

?可復(fù)用組件除了定義一個(gè)清晰的公開接口,還需要有命名空間,避免與瀏覽器保留的標(biāo)簽和其他組件發(fā)生沖突。特別是當(dāng)項(xiàng)目引用外部UI或遷移到其他項(xiàng)目時(shí),也能解決很多命名沖突問題。命名空間建議使用項(xiàng)目名稱的縮寫。

? 當(dāng)然,業(yè)務(wù)組件也建議有命名空間

上下文無關(guān)

? 所謂上下文無關(guān)并不是說全無關(guān),而是盡可能減少對(duì)外部環(huán)境的依賴。雖說Vue是拆分組件,拆分模塊的思想,但并不是無意義拆分。并不希望把一個(gè)具有獨(dú)立功能的組件按照他的模塊拆散,這樣不進(jìn)增加了無意義的數(shù)據(jù)傳輸,還不利于上下文無關(guān)特性。

數(shù)據(jù)扁平化

? 傳遞數(shù)據(jù)時(shí),不要將整個(gè)對(duì)象作為一個(gè)prop傳遞進(jìn)來。很常見的一個(gè)現(xiàn)象就是

<child :data="resData"></child>

? 然后resData的結(jié)構(gòu)為一個(gè)JS對(duì)象。這么做不是不行,而是有一些弊端。
(1)組件的接口不清晰,甚至需要寫注釋才能看明白這組數(shù)據(jù)如何處理。
(2)props 校驗(yàn)無法校驗(yàn)對(duì)象內(nèi)部的屬性類型
(3)當(dāng)服務(wù)器端返回的對(duì)象中帶有的key與組件接口不一致時(shí),需要手動(dòng)轉(zhuǎn)換或構(gòu)建。
當(dāng)然,這是一把雙刃劍,當(dāng)需要渲染的數(shù)據(jù)字段不多時(shí),提倡使用扁平數(shù)據(jù)分格。如下

<child :title="resData.title" :describe="resData.describe" :author="resData.author"></child>
項(xiàng)目骨架

? 單組件不異過重,組件在功能獨(dú)立的前提下應(yīng)該盡量簡單,越簡單的組件可復(fù)用性越強(qiáng)。當(dāng)你實(shí)現(xiàn)組件的代碼,不包括CSS,有好幾百行了(這個(gè)大小視業(yè)務(wù)而定),那么就要考慮拆分成更小的組件。

? 當(dāng)組件足夠簡單時(shí),就可以在一個(gè)更大的業(yè)務(wù)組件中去自由組合這些組件,實(shí)現(xiàn)我們的業(yè)務(wù)功能。因此,理想情況下,組件的引用層級(jí),只有兩級(jí)。業(yè)務(wù)組件引用通用組件。

? 而對(duì)于一個(gè)龐大的項(xiàng)目,必然會(huì)有更深層的組件嵌套,此時(shí)建議將業(yè)務(wù)層組件和通用組件分離


使用插槽將[業(yè)務(wù)組件]剝離成[通用組件]

? 插槽絕對(duì)是Vue中的利器。通過插槽我們不難將一個(gè)業(yè)務(wù)組件剝離出公用部分成為通用組件,通過slot再將所需要的業(yè)務(wù)內(nèi)容插入對(duì)應(yīng)插槽中。如下案例

// 組件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">圖片區(qū)域</slot>
        </div>
        <div class="r">
          <slot name="right" :item="item">詳情區(qū)域</slot>
        </div>
      </li>
      <slot name="after"></slot>
    </ul>
</template>

? 案例中展示的是一個(gè)兩列布局的通用組件。其設(shè)置了左邊欄為圖片展示區(qū)域,右邊欄為詳情展示區(qū)域。但是關(guān)于這兩欄具體信息如何展示,那是業(yè)務(wù)組件需要干的事情。

  1. 案例中的組件與業(yè)務(wù)無關(guān):他不關(guān)心頁面需要些什么,詳情區(qū)域會(huì)放些什么東西,有幾欄,而是將這些交給父組件實(shí)現(xiàn)。
  2. 與數(shù)據(jù)無關(guān):他同樣不關(guān)心數(shù)據(jù)是什么樣的,有些什么字段,字段名是什么,他只關(guān)心數(shù)據(jù)類型能通過Props驗(yàn)證即可。畢竟這里需要做v-for循環(huán)。
  3. 與上下文無關(guān):告訴該組件一個(gè)數(shù)據(jù)名稱即可,它只做數(shù)據(jù)轉(zhuǎn)交工作
  4. 結(jié)構(gòu)扁平:他將業(yè)務(wù)信息交回給父組件完成,因此自己不需要做太多的子組件封裝,也就避免了多層組件嵌套
  5. 命名規(guī)范:名稱根據(jù)組件的功能命名,兩列布局two-col-layout,很容易看懂。
    ?

# 結(jié)束語

? Vue為漸進(jìn)式框架,上手簡單并不代表這門技術(shù)就簡單。經(jīng)常復(fù)習(xí)官網(wǎng)和查閱相關(guān)書籍,會(huì)發(fā)現(xiàn)不同的東西。太多時(shí)候埋頭于寫業(yè)務(wù)代碼,而忽略了對(duì)這門極具藝術(shù)的語言有較多的研究。多思考,虛心學(xué),或許你會(huì)覺得,越學(xué),不會(huì)的越多~那就對(duì)了

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • 組件(Component)是Vue.js最核心的功能,也是整個(gè)架構(gòu)設(shè)計(jì)最精彩的地方,當(dāng)然也是最難掌握的。...
    六個(gè)周閱讀 5,653評(píng)論 0 32
  • 什么是組件? 組件 (Component) 是 Vue.js 最強(qiáng)大的功能之一。組件可以擴(kuò)展 HTML 元素,封裝...
    youins閱讀 9,533評(píng)論 0 13
  • 本文章是我最近在公司的一場內(nèi)部分享的內(nèi)容。我有個(gè)習(xí)慣就是每次分享都會(huì)先將要分享的內(nèi)容寫成文章。所以這個(gè)文集也是用來...
    Awey閱讀 9,477評(píng)論 4 67
  • 此文基于官方文檔,里面部分例子有改動(dòng),加上了一些自己的理解 什么是組件? 組件(Component)是 Vue.j...
    陸志均閱讀 3,868評(píng)論 5 14
  • Vue是一款高度封裝的、開箱即用的、一棧式的前端框架,既可以結(jié)合webpack進(jìn)行編譯式前端開發(fā),也適用基于gul...
    Hebborn_hb閱讀 1,109評(píng)論 0 31