
再比如,下拉選單、時間選擇器或者自動填充屬性等自定義控制元件都是非常複雜的,需要考慮很多邊緣的複雜情況。雖然有很多庫很好的解決了這種複雜性,但是他們也帶來了不好的缺點,就是這類元件無法自定義樣式。
就拿下面的標籤輸入控制元件舉例:

這個元件擁有一些有趣的功能:
- 不允許你新增重複的標籤
- 不允許新增空標籤
- 自動去除標籤內容兩邊的空格
- 點選Enter鍵儲存標籤
- 點選x字元刪除標籤
如果你的專案中需要使用這樣一個元件,把這個作為一個庫引入,並且剝離這些邏輯肯定能夠節省很多時間和精力。
但是假如這個時候你需要一個不同樣式展現呢?
下面這個元件擁有和上面元件一樣的行為功能,但是佈局明顯不一樣:

通過組合css和配置選項,你可以嘗試在一個元件裡面都支援這些佈局,但顯然這不是很好的方法,萬一有一天你又需要另外一個佈局,你又得去改這個元件,破壞了元件的封閉性,容易引起其它問題。
針對以上情況,我們來介紹本文最重要的一個知識點。
作用域插槽(Scoped Slots)
在Vue.js中,slots是元件中的一個佔位符元素,會被從父元件/消費者中傳過來的內容替換。

常規slots就像是給元件傳遞了一段html文字,scoped slots就像是給元件傳遞了一個能夠接收資料並返回Html的回撥函式。
通過向子元件裡面slot元素增加props,將引數傳遞給父元件。父元件通過解構destructuring slot-scope
裡面接收的屬性資料來獲得這些引數。
這裡有一個為每一個list元素暴露scoped slot屬性的LinksList
元件,並且通過:link
prop將每一項的資料傳遞迴給父元素。

:link
prop 新增到LinksList元件中的slot元素,父元素元件現在能夠通過slot-scope
訪問的到這些資料並且在自己的slot模組裡面使用它。
插槽屬性的型別
你可以傳遞任何型別給slot,但是我發現使用以下3個型別的資料之一是最有用的。
資料(Data)
最簡單的slot prop型別就是資料型別:strings,numbers,boolean values,arrays,objects等。
在我們的links-list元件例子中,link
就是一個data prop型別的例子,它是一個擁有一些屬性的物件。


動作(Actions)
動作屬性是由子元件提供的一個函式,父元件可以通過呼叫這個函式來觸發子元件裡面某些行為。
舉個例子,我們可以給父元件傳遞一個bookmark
方法,這個方法用來為給定連結新增書籤。


繫結(Bindings)
Bindings是一系列屬性或者監聽事件的集合,通過使用v-bind
或者v-on
,繫結到特定的元素中。
當你想要封裝有關如何與給定的元素進行互動的細節時,這些非常有用。
舉個例子,我們提供了bookmarkButtonAttrs
繫結和bookmarkButtonEvents
繫結用來把這些細節移動到元件自身,而不是讓消費元件自己通過v-show
指令和@click
處理新增至書籤的按鈕邏輯。


Renderless Components
名稱解釋:Renderless Components,直譯為非渲染元件,我更喜歡叫函式式元件(借鑑於react中的叫法,以下統稱函式式元件)。
函式式元件是一個不渲染任何html文字的元件。
相反,它只管理狀態和行為,給父元件或者消費元件暴露一個作用域插槽,以便它們能自己控制該渲染的內容。
函式式元件能夠準確的渲染你給它傳入的內容,無需任何其它元素。

分離層現和行為
因為函式式元件只處理狀態和行為,它們不會做出任何有關設計和佈局的決定。
那就意味著如果你能找出一種方式將像我們的標籤輸入功能這樣有趣的行為從ui元件裡面剝離出來,你就能夠複用這個函式式元件去實現任何標籤輸入元件的佈局。
下面都是標籤輸入元件,但這次是由一個函式式元件支援。

函式式元件的基本結構
函式式元件僅僅暴露一個scoped slot,消費者可以在其中提供整個他們想要渲染的模組。
一個基本的函式式元件的骨架像下面這樣:

任何父元件/消費者都能夠在自己的模板中,通過解構slot-scope
中的exampleProp
去使用。

一個實際的使用案列
讓我們從頭開始構建一個標籤輸入控制元件的函式式版本。
我們首先要建立一個無插槽的空白的無渲染元件,

以及一個靜態的,沒有任何互動的父元件,然後將其傳遞到子元件的插槽中,

slot-scope
暴露給我們佈局的地方來完善這個元件。
標籤列表
首先,我們將靜態列表替換為動態列表。
這個標籤輸入元件是自定義表單控制元件,和這個原始例子一樣,這個tags應該在父元件中,並且通過v-model
繫結到元件中。
我們首先給函式式元件增加一個value屬性,並將其傳遞給一個名為tags的插槽。

v-model
指令,從slot-scope
中獲取到tags,然後使用v-for
指令來遍歷它們。

刪除標籤
下一步,當點選X按鈕,刪除一個標籤。
在函式式元件中,我們將會增加一個removeTag
的方法,並且將其作為一個slot屬性傳遞給父元素。

@click
事件,這個事件能夠在當前的標籤中呼叫removeTag
方法。

點選Enter鍵新增標籤
新增新標籤比前面兩個例子都要複雜些。
為了理解為什麼,我們先來看一下傳統的元件都是怎麼實現的。

v-model
將這個屬性繫結到input中。
一旦使用者點選enter鍵,只要這個標籤是合法的,我們就把它新增到list陣列中,然後清除input輸入的值。
這兒的問題就是我們怎樣通過scoped-slot傳遞v-model
繫結。
好吧,如果你深入瞭解過Vue,你應該知道v-model
其實就是一個語法糖,它負責將value特性繫結到一個名叫value的prop上,同時在其input事件被觸發時,將新的值通過自定義的input事件丟擲。

- 元件中新增一個
newTag
資料屬性 - 回傳一個繫結到
:value
的newTag
的繫結屬性 - 回傳一個繫結
@keydown.enter
用來新增標籤和繫結@input
用來更新標籤的事件繫結屬性


明確新增新標籤
在我們的當前佈局中,使用者通過在輸入元素中輸入以及敲擊enter鍵來完成新增一個新標籤的操作。但這也很容易想到,有些使用者希望能夠提供點選新增按鈕來新增標籤。
要實現這個很簡單,我們只需要給slot scope傳遞一個addTag
的方法的引用。

消費者只需要解構出它們實際需要的屬性即可,所以如果你提供了它們可能用不到的屬性,它們也沒有什麼成本。
執行Demo
這就是到目前為止我們建立的函式式元件:

換個佈局
現在我們已經有了一個標籤輸入控制元件的函式式元件,我們可以很容易的編寫我們想要的任何Html並將提供的插槽屬性應用到正確的位置來輕鬆實現替代佈局。
以下就是我們利用我們新的函式式元件從頭開始實現的堆疊式佈局。

建立自己的包裹式元件
看到這麼多的例子,你可能會想:“哇,當我需要另一種形式的標籤元件時,每次我都需要寫這麼多html”,是的,你說的對。
無論什麼時候你需要一個標籤輸入元件,你確實需要寫很多。


這有一個容易的解決方法:建立一個自己的包裹式元件!

<tags-input>
元件的樣子

更加瘋狂的是
一旦你意識到一個元件可以不用渲染任何內容而只負責提供資料,那麼通過元件建模的行為就沒了限制。
舉個列子,這裡有一個使用URL作為屬性,從這個URL獲取json資料並給父元件傳遞響應資料的fetch-data
元件:

結論
將一個元件拆分成一個檢視元件和一個函式式元件是非常有用的一種模式,可以使程式碼複用更容易,但並不是每次都值得這樣做。
如果有以下這類情況,可以考慮使用這種模式:
- 你打算構建一個庫,並且希望使用者可以自定義元件的外觀
- 在你的專案中有很多功能相似但佈局不一樣的元件
如果你正在研究一個在任何情況看來都相似的元件,那就不要走這條路了。這種情形下將所有你需要的寫在一個元件裡面可能會更好更簡單。
此外,檢視程式碼和業務邏輯分離只是一種降低程式碼耦合,進而增加程式碼健壯性的一種手段,其深層次的就是元件應該符合高內聚、低耦合的思想,其它符合這種思想的手段還有像控制反轉(IOC)、釋出訂閱模式等等,我覺得程式碼越往後寫越應該培養這種意識,否則簡簡單單的寫寫業務程式碼,以完成需求而寫程式碼,提升進步會比較慢。
文章翻譯沒有完全按照原文來翻譯,為了能夠更好理解我加了一些自己的理解,如果你喜歡我的文章,請點個贊表示鼓勵或者分享給你的朋友。