從零實現Vue的元件庫(一)- Toast 實現

FatGe發表於2018-12-02

實現一個簡單的Toast外掛,方便遷移到不同的專案中,用來全域性提示、警告一些資訊。

概述: 在前端專案中,有時會需要通知、提示一些資訊給使用者,尤其是在後臺系統中,操作的正確與否,都需要給與使用者一些資訊。

1. 例項

在Vue元件的methods內,呼叫如下程式碼

this.$toast({
    content: "可自動關閉",
    autoClose: true
})
複製程式碼

在頁面的右側會出現一個Toast彈框,多次點選時,會依照順序進行顯示,並且Toast可自動關閉,具體效果如圖。

最終效果

例項地址:Toast 例項

程式碼地址:Github UI-Library

2. 原理

  • 程式碼結構

    將Toast外掛分為兩個部分:

    • Toast元件本身,包含本身的dom結構以及data,並在其生命週期完成在頁面中的掛載與銷燬;
    • 在元件外構建一層代理並提供相關方法用於呼叫、並控制多個Toast在頁面中顯示的順序。
  • Toast方法

    為了能夠通過this.$toast({...})呼叫Toast元件,須在Vue的prototype上新增一個方法,如下

    let instances = []
    let initIndex = 0
    Vue.prototype.$toast = (options = {}) => {
        /* 建立一個Toast元件的例項 */
        let instance = generateInstance(options)
        /* 將單個toast存入佇列中 */    
        instances.push(instance)
    }
    複製程式碼

    當呼叫該方法時,通過generateInstance建立一個Toast元件的例項,並將其放入instances,統一管理。

    /* 構造單個toast */
    const ToastConstructor = Vue.extend(Toast)
    const verticalOffset = 16
    function generateInstance(options) {
        // 利用ToastConstructor建立一個Toast的例項
        let instance = new ToastConstructor({
            propsData: options
        }).$mount(document.createElement('div'))
        if (typeof options.onClose === 'function') instance.onClose = options.onClose
        //計算instance verticalOffset
        let id = 'toast_' + initIndex++
        instance.id = id
        // 初始化Toast在空間中的垂直偏移量
        instance.verticalOffset = initVerticalOffset(instance.position)
        //監聽元件close
        instance.$once('toastClose', function () {
            const curInstance = this
            // 當Toast元件關閉時,重新計算垂直方向的偏移量
            updateVerticalOffset(curInstance)
            typeof curInstance.onClose === 'function' && curInstance.onClose()
        })
        return instance;
    }
    複製程式碼

    generateInstance函式中,首先利用ToastConstructor建構函式建立一個Toast元件的例項,並通過propsData傳入屬性值,同時通過$mount(document.createElement('div'))渲染該元件。

    ToastConstructor是通過Vue.extend創造Toast元件的建構函式,關於這部分的具體原理,可以參考 基於Vue構造器建立Form元件的通用解決方案

    initVerticalOffset函式計算Toast元件的初始偏移量

    /* 初始化每個toast物件在頁面中的垂直屬性 */
    function initVerticalOffset(position) {
        // 篩選同一方向的Toast元件
        let typeInstances = instances.filter(item => item.position === position)
        // 計算偏移量
        return typeInstances.reduce((sum, elem) => 
                    (elem.$el.offsetHeight + sum + verticalOffset), 
               verticalOffset)
    }
    複製程式碼

    之後當某個Toast關閉時,需要更新所有Toast例項在頁面中偏移量

    /* 更新每個toast物件在頁面中的垂直屬性 */
    function updateVerticalOffset(removeInstance) {
        let index = 0
        let removeHeight = removeInstance.verticalOffset
        // 相容ie 去掉find method
        for (; index < instances.length; index++) {
            if (instances[index].id === removeInstance.id) break;
        }
        instances.splice(index, 1);
    
        // 刪除關閉的Toast元件
        instances.splice(index, 1)
        // 更新在刪除位置之後的元件的位置
        for (; index < instances.length; ++index) {
            if (instances[index].position === removeInstance.position) {
                [instances[index].verticalOffset, removeHeight] = 
                [removeHeight, instances[index].verticalOffset]
            }
        }
    }
    複製程式碼

    以上完成了Toast元件的建立、如何在頁面中初始化、更新的位置。

  • Toast元件

    元件的功能比較簡單,只需要展示資訊,以及具備自動關閉、手動關閉兩個功能,屬性也要包括Toast的型別、位置、內容等。

    • 元件的生命週期
    let instance = new ToastConstructor({
        propsData: options
    }).$mount(document.createElement('div'))
    複製程式碼

    當Toast元件$mount呼叫時,會觸發mounted的生命週期

    mounted() {
        // 掛載Toast在頁面中
        document.body.appendChild(this.$el);
        // 需要自動關閉時,呼叫startTimer
        if (this.autoClose) this.startTimer();
    },
    beforeDestroy() {
        this.stopTimer();
        this.$el.removeEventListener("transitionend", this.destroyElement);
    },
    destroyed() {
        // 登出
        this.$el.parentNode.removeChild(this.$el);
    }
    複製程式碼
    • 自動關閉 需要自動時,就要在利用setTimeout完成該功能,並在登出時clearTimeout掉,防止洩露。
    startTimer() {
          if (this.duration > 0) {
            this.timer = setTimeout(() => {
                if (!this.closed) {
                    this.close();
                }
            }, this.duration);
          }
    },
    stopTimer() {
        if (this.timer) clearTimeout(this.timer);
    }
    複製程式碼

3. 使用

進一步將其封裝成Vue的外掛

export default {
    install (Vue) {
        Vue.prototype.$toast = (options = {}) => {...}
    }
}
複製程式碼

並且對所需要傳入的必需屬性,做處理異常處理

let requireProps = Object.keys(Toast.props)
                         .filter((elem) => (Toast.props[elem].required))
requireProps.forEach((elem) => {
    if (!options[elem]) throw `err: options lack ${elem} prop`
})
if ( options.type && !types.some(elem => elem === options.type) ) 
    throw `err: toast not exist ${options.type} type`
複製程式碼

4. 總結

通過封裝一個Toast外掛,提取不同業務之間公共的部分,減少業務的工作量。

往期文章:

原創宣告: 該文章為原創文章,轉載請註明出處。

相關文章