組件效果演示
多項(xiàng)選擇器是移動(dòng)端常見(jiàn)的通用組件,比如多級(jí)分類(lèi)、多級(jí)菜單的展示都離不開(kāi)它,它還可以進(jìn)一步擴(kuò)展為時(shí)間選擇器、地址選擇器等組件。其基本的效果演示如下圖:
(圖片質(zhì)量不清晰,并不是樣式問(wèn)題哦)
需求分析
簡(jiǎn)單對(duì)這種效果做一個(gè)需求分析吧。
表面需求
先從直觀的表面來(lái)看這個(gè)需求
- 多列展示
可以展示多個(gè)層級(jí)的列 - 支持列變化
例如在第一列數(shù)據(jù)滑動(dòng)選定后,會(huì)影響其他列的數(shù)據(jù)變化。 - 組件不重疊
即同個(gè)頁(yè)面中顯示的多個(gè)組件數(shù)據(jù)隔離。 - 支持回顯
即選中的內(nèi)容可以實(shí)時(shí)顯示在輸入框中,并且是可以自定義的。
重新打開(kāi)選擇器后,能夠默認(rèn)滑動(dòng)到上一次選中的位置。
細(xì)化需求
- 組件本身是一個(gè)受控的輸入框,允許自定義顯示內(nèi)容,點(diǎn)擊輸入框才會(huì)彈出多列選擇器,同時(shí)產(chǎn)生一個(gè)遮罩層。
- 多項(xiàng)選擇器每列顯示5個(gè)內(nèi)容,其中中間的內(nèi)容作為選中狀態(tài)列,這5個(gè)內(nèi)容還應(yīng)帶有樣式漸變的效果。
- 每一列均可上下滑動(dòng),但不能滑出邊界(滑出邊界會(huì)有回彈效果),同時(shí)滑動(dòng)結(jié)束后還應(yīng)該做滑動(dòng)校正(后面具體說(shuō)明)。
- 滑動(dòng)過(guò)程中和滑動(dòng)結(jié)束后需要觸發(fā)事件回調(diào),由外部響應(yīng)并重新刷新其他列的數(shù)據(jù)變化。
- 數(shù)據(jù)變化時(shí)應(yīng)保持其原有的相對(duì)位置不變,但當(dāng)當(dāng)前位置超出變化后的長(zhǎng)度邊界值時(shí),應(yīng)自動(dòng)調(diào)整回彈到第一個(gè)或最后一個(gè)選中。(例如上圖中,先選中12月31日,滑動(dòng)回11月時(shí),沒(méi)有31日,自動(dòng)回彈到30日)
- 關(guān)閉選擇器后,觸發(fā)事件回調(diào),能夠讓外部收集到該表單的數(shù)據(jù)信息。重新打開(kāi)選擇器后,能夠默認(rèn)滑動(dòng)到上一次選中的位置。
組件參數(shù)
設(shè)計(jì)組件好比設(shè)計(jì)一個(gè)函數(shù),組件參數(shù)其實(shí)就是外部應(yīng)該傳入什么數(shù)據(jù)提供組件渲染,在vue
中就對(duì)應(yīng)于props
屬性。
對(duì)于這種組件,往往數(shù)據(jù)來(lái)源都是后端返回,我們希望設(shè)計(jì)得更加友好調(diào)用,即后端返回的數(shù)據(jù)直接可以作為參數(shù)傳入組件,而無(wú)需另做轉(zhuǎn)換。
雖然后端返回的數(shù)據(jù)格式千變?nèi)f化,但對(duì)于組件內(nèi)部的渲染來(lái)說(shuō),其實(shí)只需關(guān)注這個(gè)數(shù)據(jù)的key
值和用于顯示的label
值,因此還需兩個(gè)參數(shù)確定即可。
同時(shí)為了支持回顯操作,需要由外部告知每次彈出選擇器時(shí)需要滾動(dòng)到的位置,這個(gè)參數(shù)也為了方便外部使用,傳遞的應(yīng)該是數(shù)據(jù)的key
值,而不是需要外部再次轉(zhuǎn)換后的index
最后還需要一個(gè)開(kāi)關(guān)控制組件的顯示和隱藏,雖然組件內(nèi)部本身也有做控制,但仍然提供一個(gè)接口給外部響應(yīng)式開(kāi)關(guān)。(也就是說(shuō)需要雙向綁定該變量)
綜上,組件的參數(shù)大體包含這么幾個(gè):
- isShow: 顯示隱藏開(kāi)關(guān)
Boolean
- columns: 接收數(shù)據(jù)
Array
數(shù)組里每個(gè)元素仍然是數(shù)組,每個(gè)數(shù)組代表每一列數(shù)據(jù)(如5列選擇器就有5個(gè)數(shù)組),每一列數(shù)組里的元素才是真正可展示的鍵值對(duì)信息(Object
)。
這一點(diǎn)可參考微信小程序的設(shè)計(jì) - keyField: 接收的columns中,用于表示id的key
String
- labelField: 接收的columns中,用于展示的key
String
- defaultCurrent: 外部告知每次彈出選擇器時(shí)需要滾動(dòng)到的數(shù)據(jù)位置。
String
組件拆分
雖然對(duì)外顯示的是一個(gè)組件,但組件的內(nèi)部實(shí)現(xiàn)往往需要細(xì)化出多個(gè)組件,組件拆分本身就是個(gè)具有實(shí)踐性的哲學(xué)問(wèn)題,不同的學(xué)者都有不同的見(jiàn)解。但在這里我想從另一個(gè)維度做探討,即利用組件的生命周期來(lái)拆分。
在探討這個(gè)維度之前,我想先介紹vue
中一個(gè)比較常見(jiàn)的API,即this.$nextTick
,相信大家并不陌生,因?yàn)樵趘ue中,數(shù)據(jù)的變化并不會(huì)立即觸發(fā)視圖的渲染,但很多時(shí)候我們?nèi)孕枰@取數(shù)據(jù)變化后與視圖相關(guān)的數(shù)據(jù),比如在這個(gè)組件中,每一列的高度、相對(duì)頂部的位置,這些數(shù)據(jù)都必須保證在視圖渲染完成后才能正確獲取的,因此視圖元素信息的獲取通常會(huì)放在this.$nextTick
回調(diào)里面,這樣就能確保回調(diào)能在視圖渲染完成后執(zhí)行。
因此,在你的組件邏輯中如果有大量使用this.$nextTick
的情況,那么這個(gè)組件就一定有再拆分的可能。
為什么這么說(shuō)?其實(shí)也很好理解,父組件的數(shù)據(jù)變化同樣會(huì)帶給子組件的視圖刷新,父組件的視圖更新完成子組件必定也更新完成,因此在原有的this.$nextTick
回調(diào)邏輯必然可以放在子組件的帶過(guò)去式的生命周期的鉤子函數(shù)里邊,例如mounted
,updated
等。大量的this.$nextTick
在一個(gè)組件上邏輯肯定是模糊不好維護(hù)的,拆分出一個(gè)子組件會(huì)使邏輯更加的清晰。
試想一下,如果把所有的邏輯都放在一個(gè)組件實(shí)現(xiàn),當(dāng)點(diǎn)擊輸入框彈出選擇器的時(shí)候,會(huì)有一個(gè)變量控制它從隱藏到顯示,有經(jīng)驗(yàn)的開(kāi)發(fā)者都知道,在這個(gè)過(guò)程中父組件的數(shù)據(jù)肯定還沒(méi)到,這樣的顯示勢(shì)必會(huì)帶來(lái)一次空數(shù)據(jù)渲染,而此時(shí)想希望在mounted
鉤子里直接獲取每一列的高度、位置等信息,肯定是錯(cuò)的,即使在this.$nextTick
下也未見(jiàn)得可行。因此如果能再細(xì)化出一個(gè)子組件,就多了一個(gè)(甚至是多個(gè))mounted
,利用這個(gè)mounted
,我們就可以獲取渲染后的視圖信息。
依據(jù)這個(gè)思路,我們將這個(gè)組件拆分如下:
- MutiPicker: 看似是一個(gè)大容器,其實(shí)它本質(zhì)不過(guò)是一個(gè)受控的輸入框
- PickerContainer :即演示圖里所看到的白色部分,也是整個(gè)組件最核心的部分
- PickerColumn:就是每一列的內(nèi)容了。
在MultiPicker和PickerContainer之間還插了一層Drawer抽屜動(dòng)畫(huà)組件,僅動(dòng)畫(huà)展示用途。
同時(shí)為了通用性,在PickerColumn中還抽象了一層Scroller劃窗組件,以便擴(kuò)展更多組件的需要。
技術(shù)實(shí)現(xiàn)
基礎(chǔ)依賴(lài)
- vue2.6.x : 標(biāo)題已經(jīng)說(shuō)明了,我們要用vue的H5版實(shí)現(xiàn),而之所以要2.6版本以上,因?yàn)橛行┬绿匦允窃谶@個(gè)組件中不得不用的。
- better-scroll/core ^2.0.0-beta.2 : better-scroll是一個(gè)比較好用的滾動(dòng)插件,使用它可以簡(jiǎn)化很多很底層細(xì)節(jié)的邏輯,當(dāng)然由于差價(jià)本身通配性很強(qiáng),我們需要把它定制化并vue化。
- node-sass ^4.9.0 : 是一個(gè)css樣式的預(yù)處理器,這里不會(huì)涉及太多的css,但是有些設(shè)計(jì)思路離不開(kāi)css的巧妙運(yùn)用。
組件數(shù)據(jù)定義
如何將數(shù)據(jù)展示出來(lái)?數(shù)據(jù)結(jié)構(gòu)的選擇就很重要。試想一下,外圍傳入的columns好比這樣:
[
[{id: 1000, name: ‘北京’, isTest: true},{id: 2000, name: ‘上海’, isTest: true},{id: 3000, name: ‘廣州’, isTest: true}],
[{id: 3001, name: ‘天河區(qū)’, isTest: true}, {id: 3002, name: ‘海珠區(qū)’, isTest: true}, {id: 3001, name: ‘從化區(qū)’, isTest: true}]
]
這樣的結(jié)構(gòu),通過(guò)keyField和LabelField,我們自然可以知道用id和name來(lái)展示,這看似沒(méi)什么問(wèn)題,但是問(wèn)題在于,當(dāng)我選擇完成后,如何告知外圍我選中的數(shù)據(jù)。如果還是這種結(jié)構(gòu),那么通常返回的是雙index模式,這類(lèi)似于微信小程序的設(shè)計(jì),比如選擇廣州海珠區(qū),告知外圍的是[2,1]
。這其實(shí)很不友好,對(duì)外圍來(lái)說(shuō)還需要做一次轉(zhuǎn)換,才能拿到想要的數(shù)據(jù),因此是否可以改進(jìn)一下,能否直接告訴外圍的數(shù)據(jù)就是[{id: 3000, name: ‘廣州’, isTest: true}, {id: 3002, name: ‘海珠區(qū)’, isTest: true}]
,這種帶完整數(shù)據(jù)結(jié)構(gòu)的呢?
另外,為了支持回顯,外圍需要傳入defaultCurrent
給組件默認(rèn)展示,而如果是雙index模式,外圍通常還需要做一次轉(zhuǎn)換才能知道[2,1]
。為了友好組件設(shè)計(jì),能否讓外圍直接傳入默認(rèn)需要展示的id值,如[3000, 3002]
即可呢?
若外圍不想轉(zhuǎn)換,必然要在組件內(nèi)轉(zhuǎn)換,核心在于我們并不需要數(shù)組這種index對(duì)數(shù)據(jù)的映射關(guān)系,而我們迫切需要的是id對(duì)數(shù)據(jù)的映射關(guān)系,這樣就能能直接通過(guò)id取得數(shù)據(jù),對(duì)此很容易聯(lián)想到ES5
的鍵值對(duì)Object
,但這里更推薦使用ES6
的Map
,那么同樣是存儲(chǔ)鍵值對(duì),Object
和Map
有什么區(qū)別呢?
Object 好比是HashMap,順序添加鍵值對(duì)時(shí),默認(rèn)是根據(jù)鍵的內(nèi)部哈希值排序的。
Map 好比是LinkedMap,順序添加鍵值對(duì),其順序會(huì)和原數(shù)組的順序一樣。
但是在Vue中是否能支持Map的遍歷呢?慶幸的是Vue2.6以上的版本支持v-for
對(duì)實(shí)現(xiàn)Symbol.iterator
的數(shù)據(jù)結(jié)構(gòu)的遍歷,這其中就包括了Map。即使不支持動(dòng)態(tài)響應(yīng)式,但對(duì)于展示渲染已經(jīng)滿(mǎn)足了。
因此最后的結(jié)論就是,我們要將外圍傳入的二維數(shù)組結(jié)構(gòu),轉(zhuǎn)換成數(shù)組的Map,即類(lèi)似于[Map, Map, Map]
,每個(gè)Map鍵就是keyField值,值就是labelField值。
Drawer抽屜動(dòng)畫(huà)組件
上面的核心部分說(shuō)了那么多,是時(shí)候開(kāi)始實(shí)踐了,這里從最簡(jiǎn)單的組件開(kāi)始,那就是動(dòng)畫(huà)。這個(gè)組件唯一的一個(gè)動(dòng)畫(huà)那就是彈出動(dòng)畫(huà),而彈出動(dòng)畫(huà)不過(guò)是抽屜效果的一個(gè)子集。
Vue提供的動(dòng)畫(huà)主要有兩種,簡(jiǎn)單來(lái)說(shuō)就是問(wèn)要CSS實(shí)現(xiàn)還是js實(shí)現(xiàn),答案也很簡(jiǎn)單,能不用js就不用js,絕大多數(shù)特效用css已經(jīng)足夠。
而靠css實(shí)現(xiàn)的動(dòng)畫(huà),vue又提供了兩種模式,這兩種模式的區(qū)別主要是看你對(duì)動(dòng)畫(huà)的理解,姑且稱(chēng)其中一種為點(diǎn)態(tài)式,習(xí)慣這種模式的需要掌握這張很熟悉的圖片:
另一種方式估計(jì)也猜到了,那就是過(guò)程式,它是通過(guò)css3動(dòng)畫(huà)實(shí)現(xiàn)的,使用這種方式的,通常是把動(dòng)畫(huà)看成一個(gè)整體,研究的是整條動(dòng)畫(huà)曲線(xiàn)的變化,而不是一個(gè)點(diǎn)狀態(tài)到另一個(gè)點(diǎn)狀態(tài)。
大多數(shù)情況下,兩種實(shí)現(xiàn)是相通的,而css3的動(dòng)畫(huà)實(shí)現(xiàn)也許適用范圍更廣一些,但關(guān)鍵如何實(shí)現(xiàn),還得看個(gè)人對(duì)動(dòng)畫(huà)的理解,個(gè)人對(duì)比這兩種方式,點(diǎn)態(tài)式相對(duì)比較抽象,過(guò)程式比較直觀一些。
點(diǎn)態(tài)式的動(dòng)畫(huà)實(shí)現(xiàn)方式:
<template>
<transition name="drawer">
<slot></slot>
</transition>
</template>
.drawer-enter, .drawer-leave-to {
transform: translateY(100%)
}
.drawer-enter-active, .drawer-leave-active {
transition: transform .5s;
}
過(guò)程式的動(dòng)畫(huà)實(shí)現(xiàn)方式:
@keyframes drawer-in {
0% {
transform: translateY(100%);
}
100% {
transform: translateY(0);
}
}
.drawer-enter-active {
animation: drawer-in .5s;
}
.drawer-leave-active {
animation: drawer-in .5s reverse;
}
MutiPicker 中間組件
從圖上畫(huà)MutiPicker好像是一個(gè)大容器,實(shí)際上它只是一個(gè)小小的輸入框,準(zhǔn)確的說(shuō),是一個(gè)受控的輸入框,只有當(dāng)它被點(diǎn)擊的時(shí)候,整個(gè)容器的面貌才顯示得出來(lái)。
根據(jù)組件參數(shù)的設(shè)定,我們先將這個(gè)頂層組件的props
定義出來(lái):
props: {
isShow: {
type: Boolean,
default: false,
},
columns: {
validator: arr => Array.isArray(arr) && arr.reduce((a, b, i) => a && Array.isArray(b), true),
default: () => [[]],
},
keyField: {
type: String,
required: true,
},
labelField: {
type: String,
required: true,
},
defaultCurrent: {
type: Array,
default: () => [],
},
},
組件參數(shù)的雙向綁定
這里問(wèn)題就來(lái)了,外圍可以通過(guò)isShow參數(shù)控制picker的顯示隱藏,但是輸入框作為組件內(nèi)部,也需要去控制isShow變量顯示隱藏,那怎么讓通過(guò)props
綁定來(lái)的參數(shù)實(shí)現(xiàn)雙向綁定呢?vue2.6為此專(zhuān)門(mén)提供了新語(yǔ)法.sync
,只要外圍采用這種方式綁定<multi-picker :is-show.sync="isShow" />
,組件內(nèi)部派發(fā)update:isShow
事件,就可以間接"改變"props
定義的isShow
變量,實(shí)現(xiàn)雙向綁定了。
而這種語(yǔ)法,和2.6之前版本的v-model
有什么區(qū)別呢?
.sync 更適用于實(shí)現(xiàn)自定義組件之間的雙向綁定,它派發(fā)的事件具有自定義性
v-model 更適用于表單組件之間的雙向綁定,它通常派發(fā)的是表單相關(guān)的事件
新版slot插槽
需求說(shuō)到,這個(gè)表單組件允許自定義展示內(nèi)容,那么就需要通過(guò)插槽對(duì)外提供視圖自定義顯示,當(dāng)然除了視圖之外,外圍更需要的是拿到這個(gè)組件選中的數(shù)據(jù),那么如何通過(guò)插槽的方式向外傳遞數(shù)據(jù)呢?vue2.6對(duì)插槽的語(yǔ)法進(jìn)行了改造,其實(shí)主要的變化在于使用v-slot
指令代替之前放在template
標(biāo)簽上的scope
,也就是說(shuō),2.6版本后,獨(dú)占默認(rèn)插槽就可以不用template
標(biāo)簽了。
因此我們最初版的MutiPicker可以長(zhǎng)這樣
<div id="picker" @click.stop="onShow">
<slot :currents="currents"></slot>
<div class="mask" v-if="isShow"></div>
<div class="test-tmp" v-if="isShow">這里將會(huì)是個(gè)picker-container</div>
</div>
props: {
isShow: {
type: Boolean,
default: false,
},
// ....
}
data() {
return {
currents: [],
};
},
methods: {
onShow() {
this.$emit('update:isShow', true);
},
}
#picker {
width: 100%;
height: 100%;
}
.mask {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: 1;
background-color: rgba(55, 55, 55, .5);
}
順便提一下,這里使用v-if
來(lái)控制遮罩層和pickerContainer的顯示和隱藏,而不是v-show
,目的是為了滿(mǎn)足多個(gè)MultiPicker同時(shí)使用時(shí)的數(shù)據(jù)隔離。
對(duì)于外圍來(lái)說(shuō),可以通過(guò)這種方式做數(shù)據(jù)交互:
<multi-picker :is-show.sync="isShow" v-slot="{ currents }">
<span>{{ viewShow(currents) }}</span>
</multi-picker>
總結(jié)
本文主要提出了需求并做分析,并對(duì)整體組件做了初步的設(shè)計(jì),包括組件參數(shù)、組件拆分、數(shù)據(jù)結(jié)構(gòu)定義,其中富含不一樣的設(shè)計(jì)思想。最后初步做了組件技術(shù)實(shí)現(xiàn),同時(shí)還介紹一些了vue2.6新特性,包括動(dòng)畫(huà)設(shè)計(jì)、雙向數(shù)據(jù)綁定、插槽參數(shù)等諸多內(nèi)容,當(dāng)然主要在思想設(shè)計(jì)方面占了比較多的篇幅。
下期還將出一篇完結(jié)這個(gè)組件的最核心部分picker-container
,敬請(qǐng)期待更精彩的內(nèi)容吧!