從零實現Vue的元件庫(七)- Message-Box 實現

Yzz發表於2018-12-28

模擬系統的訊息提示框而實現的一套模態對話方塊元件,用於訊息提示、確認訊息和提交內容。

概述:該元件的結構、原理與Toast 元件類似,所以這篇文章會減少元件開發的介紹,而增加一些Vue.extendVue生命週期相關原始碼的解讀。

主要有以下幾點:
  • Message-Box的基本功能實現;
  • Vue.extend$mount原理以及相應的優化點。

1. 例項

最終效果

程式碼

<!-- 基礎用法 -->
this.$confirm({
    title: "自定義提示",
    content: `<h1 style="color: red;">自定義HTML</h1>`,
    onConfirm: () => this.$message({
        content: "確定"
    }),
    onCancel: () => this.$message({
        type: "warn",
        content: "取消"
    })
});
this.$alert({
    title: "標題名稱",
    content: "這是一段內容",
    onConfirm: () =>
    this.$message({
        content: "確定"
    })
});
複製程式碼

例項地址:Hover-Tip 例項

程式碼地址:Github UI-Library

2. 原理

將Message-Box分為兩部分:

  • Message-Box元件,用於在頁面中顯示模態框,包含確認、取消、關閉三個操作;
  • Vue外掛的封裝,利用this.$alert({...})this.$confirm({...})在頁面中掛載 Message-Box 元件。

首先開發Message-Box元件,其基本template如下

<div class="mock" v-if="visible">
    <div :class="['message-box']">
        <!-- header -->
        <h5 class="message-box-header c-size-l">
            <span>{{ title }}</span>
            <fat-icon 
                v-if="showClose" 
                name="close" 
                class="close-btn" 
                @click.stop="close"
            />
        </h5>
        <!-- content -->
        <div 
            class="message-box-content c-size-m" 
            v-html="content"
        >
        </div>
        <!-- footer -->
        <div class="message-box-footer">
            <fat-button
                size="mini"
                v-if="cancelButtonText && type !== 'alert'"
                @click.stop="handleClick('cancel')"
            >{{ cancelButtonText }}</fat-button>
            <fat-button
                size="mini"
                type="success"
                v-if="confirmButtonText"
                @click.stop="handleClick('confirm')"
            >{{ confirmButtonText }}</fat-button>
        </div>
    </div>
</div>
複製程式碼

基本結構非常簡單,清晰的三段:

  • 最上層的visible狀態用於控制整個模態框的顯示、消失,header部分,包含title,以及關閉按鍵;
  • 中間content部分,包含傳入的content,為了支援HTML,所以採用v-html
  • 最底層footer部分,包含兩個按鈕,如果當前模態框的型別是alert則只有confirm,如果為confirm,則需要新增cancel

所涉及的datamethods如下

export default {
    data() {
        return {
            // 控制模態框的顯示
            visible: true
        }
    },
    watch: {
        visible(newValue) {
            if (!newValue) {
                // 過渡結束後登出元件
                this.$el.addEventListener('transitionend', this.destroyElement)
            }
        }
    },
    mounted() {
        document.body.appendChild(this.$el)
    },
    destroyed() {
        this.$el.parentNode.removeChild(this.$el)
    },
    methods: {
        destroyElement() {
            this.$destroy()
        },
        close() {
            // 關閉模態框
            this.visible = false
        },
        handleClick(type) {
            // 處理對應的點選事件
            this.$emit(type);
            this.close();
        }
    }
}
複製程式碼

可以看到在該元件的mounteddestroyed兩個生命週期中完成元件在頁面中的掛載與登出

mounted() {
    document.body.appendChild(this.$el)
},
destroyed() {
    this.$el.parentNode.removeChild(this.$el)
}
複製程式碼

其中登出的順序是:

  • 首先使元件的visible狀態為false,使它在頁面中消失,然後觸發模態框transition的過度動畫;
  • 之後監聽addEventListener('transitionend', this.destroyElement),如果過渡結束,就觸發對應的destroyElement觸發元件的生命週期destroyed,在元件中登出this.$el.parentNode.removeChild(this.$el)

相對於登出,掛載則要複雜的一些,由於這部分涉及到了封裝,所以一起梳理。

// 引入上述Message-Box元件
import messageBox from './messagebox.vue'
// 生成Message-Box對應的構造器
const Constructor = Vue.extend(messageBox)

function generateInstance(options, type = 'alert') {
    let instance = new Constructor({
        propsData: Object.assign(options, {
            type
        }),
    }).$mount(document.createElement('div'))
    ...
    return instance
}
複製程式碼

首先import模態框元件,然後利用Vue.extend建立一個Message-Box元件的構造器。

當呼叫this.$alert以及this.$confirm時利用new Constructor建立一個Message-Box元件,這時顯式呼叫vm.$mount()手動開啟編譯,此時會觸發元件的mounted生命週期

mounted() {
    document.body.appendChild(this.$el)
}
複製程式碼

完成在頁面中的掛載。

最後一步,利用Vue.use完成封裝,在Vue.prototype上新增$alert以及$confirm方法。

export default {
    install(Vue) {
        Vue.prototype.$alert = (options = {}) => generateInstance(options)
        Vue.prototype.$confirm = (options = {}) => generateInstance(options, 'confirm')
    }
}
複製程式碼

3. 原始碼

闡述下 Vue.extend 的原始碼,在 src/core/global-api/extend.js 中,比較關鍵的點在於:

  • 如何通過已有元件 object 生成對應構造器 constructor
  • 對於同一個元件的 constructor ,是否存在什麼優化。
Vue.extend = function (extendOptions: Object): Function {
    extendOptions = extendOptions || {}
    const Super = this
    const SuperId = Super.cid
    const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
    if (cachedCtors[SuperId]) {
        return cachedCtors[SuperId]
    }
    ...    
    const Sub = function VueComponent (options) {
        this._init(options)
    }
    Sub.prototype = Object.create(Super.prototype)
    Sub.prototype.constructor = Sub
    ...
    // cache constructor
    cachedCtors[SuperId] = Sub
    return Sub
}
複製程式碼

從原始碼中可以看出,Vue.extend 是一個變式的原型式繼承

function object(o) {
    function F() {}
    F.prototype = o
    return new F()
}
複製程式碼

臨時的建構函式 function Ffunction VueComponent,函式中 this._init 指向的是初始化Vue時候的 _init function

最終效果

在將 F.prototype 指向 Object.create(Super.prototype) ,這樣可以繼承 Vue 本身原型上的一些方法,最後 return constructor

之後 Vue 還做了快取處理,所以多次利用 Vue.extend 建立Message-Box、Toast、Message時,並不會影響效率/

if (cachedCtors[SuperId]) {
    return cachedCtors[SuperId]
}

// cache constructor
cachedCtors[SuperId] = Sub
複製程式碼
  1. 總結

深究了一下 Vue.extend 背後的原理,以及如何用它來是實現元件。

參考文章:

往期文章:

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

相關文章