Vue 元件庫實踐和設計

混元霹靂手發表於2017-08-08

現在前端的快速發展,已經讓元件這個模式變的格外重要。對於市面上的元件庫,雖然能滿足大部分的專案,但是一些小型細節方面和使用方面,或者UI庫存在的一些bug,會讓人很頭疼。

那我們應該如何面對解決這些問題。俗話說自己動手豐衣足食。有些元件不用刻意去造。應該考慮如何去打造一個快速,相容性好,功能齊全的元件庫。

  1. 先到github上和一些大公司開源的元件庫官網上去看看你所需元件庫的demo例子,Prop和event暴露出來了哪些介面。

  2. 貨比三家。別人分別用了哪些模式設計,哪種模式最簡便,更合理。

  3. 一般成熟的UI庫相容性是經過大量測試和使用者使用後沒有問題後的結果。省去這一部分,進行樣式的借鑑。

  4. 不要太急於進行元件的大量造輪子。因為一個人的戰鬥是有限的。根據需要和場景專案進行一個個定位,積少成多。

Crib-zk也是我個人目前針對自己專案的需求,額外進行的元件。雖然不能用於開源市場的使用,但是可以用於大家的學習。使用和學習是兩種模式。會使用不代表你懂,一旦有些需求不在開源專案元件的範圍之內。此時又不能清楚內部的原理,就會措手不及。接下來進行一步一步分析。

大綱:分析元件

  1. alert 外掛/元件

  2. backtop 元件

  3. sms-countDown元件(簡訊倒數計時)

  4. search 元件

  5. infinitscroll 元件

  6. actionSheet 元件

  7. accordion 元件(手風琴)

注意 : 看這篇文章最好結合我github上釋出的元件,進行比對式觀看。

github:github.com/ziksang/cri…

demo:http://39.108.49.237:3000/dist/

一、alert元件/外掛

如果在alert這種彈出式元件裡,首先要加一些背景layout的背景層動畫化,可以簡稱為dialog動化,進行一個包裹。

對於alert元件/外掛的區別使用性是在那裡?

一般來說,先會定義一個.vue檔案的alert模板。

enter image description here
enter image description here

<template>
    <div class="vux-alert">
        <x-dialog :value='alertShow' :isClose='false'>
            <div class="crib-confirm_hd" :style='[titleStyle]'>
                <strong class="crib-confirm_title">{{title}}</strong>
            </div>
            <div class="crib-confirm_bd">
                <slot>
                    <div v-html="content"></div>
                </slot>
            </div>
            <div class="crib-confirm_ft">
                <a href="javascript:;" class="crib-confirm_btn crib-confirm_btn_primary" @click="_onSubmit" :style='[buttonStyle]'>{{buttonText}}</a>
            </div>
        </x-dialog>
    </div>
</template>複製程式碼

仔細看上面模板。

  1. 我們發現唯一特別的是對content體中的定義了一個solt。這個slot就是元件模式和外掛模式的區分。如果我們想對slot裡面定義的是一個額外的展示模板或其它元件插入的話,此時只能用元件模式。

  2. 如果只是我們對content這個資料進填充的話,外掛模式也是最方便的。

props介面的暴露

  1. value 顯示訊息

  2. title (標題)

  3. content 內容最好支援html格式

  4. buttonText底部的按鈕文案

  5. titleStyle 標題樣式

  6. buttonStyle button樣式

event介面暴露

  1. onsubmit 點選時向外暴露事件

  2. onshow 顯示時向外暴露的顯示事件

  3. onhide 顯示時向暴露事件

 data() {
        return {
            alertShow: this.value
        }
    },
    watch: {
        value(val) {
            this.alertShow = val
        }
    },
    methods: {
        _onSubmit() {
            this.alertShow = false
            this.$emit('update:value', false)
            this.$emit('on-submit')
        }
    }複製程式碼

對於value這個值來說,可以用.sync來進行簡便的操作。不需要通過$emit來進行通知。在宣告元件的時候用$on去進行監聽事件,省去了開發者這一步的事。

外掛模式

首先要把原本的alert的.vue的模板給引處進來,然後用Vue.extend繼承一下。

  $vm = new Alert({
                el: document.createElement('div')
            })複製程式碼

我們自己要手動進行一個建立掛載點。

//此方法是用來把confirm上的prop屬性合併到呼叫時的引數上
const mergeOptions = function($vm,options) {
    //宣告一個預設的物件,就是comfirm上props屬性的default的值
    const defaults = {}
    //迴圈confirm屬性上的props值
    for (let i in $vm.$options.props){
       //不把value的值算上去,顯示改變通過watch或者改變data代理的屬性上去監聽
       if(i !== 'value'){
          defaults[i] = $vm.$options.props[i].default
       }
    }
    //把confrim元件原本的值和外掛傳入的options合併
    const _options = Object.assign({},defaults,options)
    //把confirm元件生成的實列物件再次替換成合並的屬性
    for(let i in _options) {
        $vm[i] = _options[i]
    }
}複製程式碼

同時要把value顯示操作的預設定義的屬性除外,進行自己定義後覆蓋預設屬性,進行顯示。

$watcher = $vm.$watch('alertShow', (val) => {
                    if (!val && options && options.onHide) {
                        options.onHide()
                    }
                })複製程式碼

同時對alertshow進行監聽,當點選submit的時候會自己動觸發事件,然後會改變alertshow的值,然後進行你所想要的操作。

二、backtop 元件

對於backtop元件的話,要理解幾點屬性。

  1. scrollTop

  2. offsetHeight

let offsetTop = this.scrollview == window ? document.body.scrollTop : this.scrollview.scrollTop
            let offsetHeight = this.scrollview == window ? document.body.offsetHeight : this.scrollview.offsetHeight複製程式碼

scrollTop 是距離頂部的高度。

offsetHeight 元素的高度包括邊框。

那如何去判斷什麼時候顯示返回頂部按鈕呢?

this.show = offsetTop >= offsetHeight / 2;

只要通過滾動的高度大於滾動元素的高度/2來進行一個適配是最好的。

對於如何進行那方面優化。

可以進行函式節流。節流是個什麼?因為進行滾動監聽的時候,scroll事件觸發的太頻繁了。這會影響到整個效能的問題。如果對於上下滾動也要頻繁監聽。用節點,不適用於防抖操作。

 throttle(func, wait) {
            var context, args;
            var previous = 0;

            return function () {
                var now = +new Date();
                context = this;
                args = arguments;
                if (now - previous > wait) {
                    func.apply(context, args);
                    previous = now;
                }
            }
        },複製程式碼

通過時間戳來進行對比,來進行函式節流。但是有一點需要注意,在節流的同時,不要節流的時節太長。因為mobile上面節流滾動的話,有一個自行滑動的時長。

const getScrollview = function (el) {
    //拿到當前節點
    let currentNode = el;
    //如果有節點,並且節點不等於html ,body 並且節點型別是元素節點
    while (currentNode && currentNode.tagName !== 'HTML' && currentNode.tagName !== 'BODY' && currentNode.nodeType === 1) {
       //拿到節點的overflowy的屬性
        let overflowY = document.defaultView.getComputedStyle(currentNode).overflowY;
        //如果此時屬性是scroll或者atuo 就返回此節點
        if (overflowY === 'scroll' || overflowY === 'auto') {
            return currentNode;
        }
        //否則就繼續向父節點上找
        currentNode = currentNode.parentNode;
    }
    //一但while語句為false的時候就直接返回window對像
    return window;
};


export {getScrollview}複製程式碼

在外層要進行一個包裹,通overflow屬性向來進行推測, 是全域性滾動還是window 下的滾動,通過while來進行判斷遞迴,來查詢所對應的元素 。

三、sms-countDown 簡訊倒數計時元件

enter image description here
enter image description here

對簡訊倒數計時的認知

對於簡訊倒數計時最重要的一點就是從父元件向sms元件通知倒數計時開始的一個prop引數,用start替代。

  watch : {
        start (value) {
          if(value === true){
            this.countDown()
          }
        }
      }複製程式碼

同時對start在內部進行監聽。一旦從外部傳入開始的時候,則內部進行倒數計時。

countDown () {
            this.myTime = 60;
            let time = setInterval(()=>{
                this.myTime --;
                if(this.myTime === 0){
                  this.$emit('update:start', false);
                  this.myTime = this.initText;
                  this.flag = true;
                  clearInterval(time)
                }
            },100)
        }複製程式碼

在這裡同樣要進行一個倒數計時停止之後的向外通知。還是用.sync的雙向繫結方法,用於簡便操作。

在對於第一次倒數計時和第二次倒數計時的時候,也要對文案這方面進行一個設定。

 firstCkText : {
            type : String,
            default : ''
          },
          secondCKText : {
            type : String,
            default : '重新獲取'
          },複製程式碼

第一次點選和第二次點選的按鈕,也是對主要的文案的一種設計,所以對文案的變化也是要很關注的。

四、search元件

對於search元件通常能想到哪些對應的功能和想法呢?比如首次進來的時候,要進行自動獲取Input的焦點。同時要向外面暴露是否要獲取Input獲點事的Prop :autoFocus。同時也要注意,一定要在Mounted的時候才能拿 到dom元素。

  mounted() {
        this.autoFocus && this.$refs.input.focus()
    }複製程式碼

一般想知道input裡面的value值是否改動的時候,通常都會用keydown或者是keyup事件。但是這裡不需要,可以時時把value的值給暴露出去,讓外層父元件可以去進行watch監聽來進行進所需要的事件操作。

watch: {
        inputValue (val) {
           if(val == ''){
               this.value = ""
           }
        },
        value: {
            handler(val, oldvalue) {
                //當值改變的時候,觸發事件
                this.$emit('update:inputValue', val)
                this.$emit('change-val')

            },
            immediate: true

        }
    },複製程式碼

同時這裡用到了.sync ,在頁面一載入的時候,立馬執行了。immediate使得value這個值立馬值行了監聽。

五、infinite-scroll 元件 (無限滾動元件)

無限滾動最關鍵的三個地方。第一滾動動底部觸發事件;第二如果有二次載入則顯示 loading;第三如何沒有二次載入則結束文案。

import { getScrollview } from '../../libs/getScrollview.js';複製程式碼

這個不用說,繼續尋找需要滾動範圍的元素 。

 data() {
        return {
            isLoading: false, //是否正在載入
            isDone: false,  //是否載入完畢
        }
    },複製程式碼

data裡面進行之前說的兩種模式的狀態進行定義。往下看,這一處定義之後對後面有什麼好處。

 this.$on('Load', () => {
                this.isLoading = false;
            });
            this.$on('loadDone', () => {
                    this.isLoading = false;
                    this.isDone = true;
            });複製程式碼

需要監聽兩個事件:

  1. 二次載入load事件。一旦進行二次載入的時候,馬上進行isloading等於false 防止重複載入。

  2. 通過loadDone對是否監聽完畢。如果載入完畢的話,同樣的關閉isloading 對isDone進行true的設定。

isloading和isDone分別對應的那個html 的template部分。

 <div class="list-donetip" v-show='!isLoading&& isDone'>
            <slot name='doneTip'>沒有更多資料了</slot>
        </div>

        <div class="list-loading" v-show='isLoading'>
            <slot name='loadingTip'>載入中...</slot>
        </div>複製程式碼

當isloading 為true的時候顯示“載入中... ”當isloading 為false的時候,isDone為true的時候才顯示 “沒有更多資料”,這也是一個標準的無限滾動。

什麼時候對isloading和isDone設定為true?

 scrollHandler() {
            if (this.isLoading || this.isDone) return;
            let baseHeight = this.scrollview == window ? document.body.offsetHeight : this.scrollview.offsetHeight
            let moreHeight = this.scrollview == window ? document.body.scrollHeight : this.scrollview.scrollHeight;
            let scrollTop = this.scrollview == window ? document.body.scrollTop : this.scrollview.scrollTop
            if (baseHeight + scrollTop + this.distance > moreHeight) {
                this.isLoading = true;

                this.onInfinite()
            }

            if (!this.scrollview) {
                console.warn('Can\'t find the scrollview!');
                return;
            }
        },複製程式碼

當滾動到底部的時候,對isloading進行為true的設定。外部元件可以呼叫onInfinite,進行ajax請求等操作。

在外部如何呼叫呢?

this.$refs.infinite.$emit('loadDone')複製程式碼

對元件進行 ref的設定,然後進行觸發loadDone或者load。

比對餓麼的元件,它使用的是指令的模式,內部實現還是太複雜。

如何進行一個優化

這裡就用到了函式防抖,同上面不用函式節流,用防抖。防抖跟節流的有什麼區別?防抖比較更節省效能。如果我們在設定的時間內一直滑動,則不會進行載入,只有滑動到指定的地方,則可以進行檢測,通過定時器來實現。

 debounce  (func, wait) {
            var timeout;
            return function () {
                var context = this;
                var args = arguments;

                clearTimeout(timeout)
                timeout = setTimeout(function () {
                    func.apply(context, args)
                }, wait);
            }
        },複製程式碼

六、actionSheet 元件

enter image description here
enter image description here

actionSheet 這裡亮點就是巧用了。call方法來改變了this的指向。這個有什麼好處?往下面看。

prpps : model 我通過Model這個資料進行遞,把文案的改變,點選後所執行的方法一併封裝到Model資料裡來進行操作。

如果在父元件引入這個元件的時候,看下面程式碼。

 model: [
                { name: this.name, method(name, index) { location.href = 'tel:110' } }
            ],複製程式碼

如果進行this.指向的話,指向的是父元件。這裡就不能直接在data裡面宣告瞭。如果是非同步的話,只有在ajax請求的非同步裡進行宣告,把值傳入,是如何做的呢?

 ff (item,index,method) {
             this.$emit('update:show', false)
             method.call(this.$parent,item,index)
        }複製程式碼

在這裡通過.call來把this.的指向到父元件,就能成功的方便的呼叫了。

七、accordion 手風琴元件

enter image description here
enter image description here

對accordion元件要進行定義兩個元件合併成一個元件的模式。

  1. 一個最外層的包裹元件。

  2. 第二個是每一個item的元件。

每說 accordion-item裡面的元件,通過

this.height = (this.show ? this.$refs.content.offsetHeight: 0) + 'px';複製程式碼

如果需要顯示的話,讓每一個item的元素來計算高度,展現出來。

通過_uid來進行 每個item的識別。

  methods : {
           open(uid) {
               this.$children.forEach (item => {
                   console.log(item._uid)
                   if(item._uid == uid){
                       item.show = !item.show
                   }else{
                       if(!this.repeat){
                           item.show = false
                           item.height = 0;
                       }
                   }
               })
           }
       }複製程式碼

能夠收起的是那個item元件,則向收起的那個item元件進行一個傳遞。本質上通過index找到子元件對應的項也可以實現。因為_uid是唯一的。這一步也是省了一些簡便的操作。

在這裡把一些突出的元件,來開闊我們的思想,來進行一其它元件的封裝 ,也可以基於這些元件對自己所需要的專案根據不同的需求來封裝。

最後,尤大說了一句話,我最喜歡的就是看別人程式碼。記住這句話,你的元件能寫的又快又好。

這是我最近在Gitchat上交流的一篇文章。如果大家對Vue感興趣的話,可以加入我的Vue討論微信群。有什麼問題可以在群裡交流。

相關文章