Vue 組件化實(shí)戰(zhàn)之——手動(dòng)封裝多項(xiàng)選擇器組件(一)

組件效果演示

多項(xiàng)選擇器是移動(dòng)端常見(jiàn)的通用組件,比如多級(jí)分類(lèi)、多級(jí)菜單的展示都離不開(kāi)它,它還可以進(jìn)一步擴(kuò)展為時(shí)間選擇器、地址選擇器等組件。其基本的效果演示如下圖:

vue-frame.gif

(圖片質(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ì)化需求

  1. 組件本身是一個(gè)受控的輸入框,允許自定義顯示內(nèi)容,點(diǎn)擊輸入框才會(huì)彈出多列選擇器,同時(shí)產(chǎn)生一個(gè)遮罩層。
  2. 多項(xiàng)選擇器每列顯示5個(gè)內(nèi)容,其中中間的內(nèi)容作為選中狀態(tài)列,這5個(gè)內(nèi)容還應(yīng)帶有樣式漸變的效果。
  3. 每一列均可上下滑動(dòng),但不能滑出邊界(滑出邊界會(huì)有回彈效果),同時(shí)滑動(dòng)結(jié)束后還應(yīng)該做滑動(dòng)校正(后面具體說(shuō)明)。
  4. 滑動(dòng)過(guò)程中和滑動(dòng)結(jié)束后需要觸發(fā)事件回調(diào),由外部響應(yīng)并重新刷新其他列的數(shù)據(jù)變化。
  5. 數(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日)
  6. 關(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ù)里邊,例如mountedupdated等。大量的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è)組件拆分如下:

組件拆分.png

  • 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,但這里更推薦使用ES6Map,那么同樣是存儲(chǔ)鍵值對(duì),ObjectMap有什么區(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í)慣這種模式的需要掌握這張很熟悉的圖片:

image

另一種方式估計(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)容吧!

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

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