命令式組件Message、Dialog的主流寫法分析
這裏還是以element-ui爲例,那我們就看看裏面的Message。
它的dom結構什麼的就寫在node-modules/element-ui/packages/notification/src/main.vue裏面
<template> <transition name="el-notification-fade"> <div :class="['el-notification', customClass, horizontalClass]" v-show="visible" :style="positionStyle" @mouseenter="clearTimer()" @mouseleave="startTimer()" @click="click" role="alert" > <i class="el-notification__icon" :class="[ typeClass, iconClass ]" v-if="type || iconClass"> </i> <div class="el-notification__group" :class="{ 'is-with-icon': typeClass || iconClass }"> <h2 class="el-notification__title" v-text="title"></h2> <div class="el-notification__content" v-show="message"> <slot> <p v-if="!dangerouslyUseHTMLString">{{ message }}</p> <p v-else v-html="message"></p> </slot> </div> <div class="el-notification__closeBtn el-icon-close" v-if="showClose" @click.stop="close"></div> </div> </div> </transition> </template>
可以看出,這裏東西不多,只有一些對應的class、關閉按鈕外,就剩下title和message了,這裏看到用了slot,所以我們通常也只能將自己的自定義內容放入此插槽中。
js方便就是支持大家常用的點擊、關閉回調,裏面維護了一個計時器來控制關閉。
我們能將它作爲命令式只用的主要實現邏輯是在node-modules/element-ui/packages/notification/src/main.js裏面。
這裏的核心是Notification方法,這個包導出的也是它。
我們先看大家常用的 this.$message.success
方法是什麼
['success', 'warning', 'info', 'error'].forEach(type => { Notification[type] = options => { if (typeof options === 'string' || isVNode(options)) { options = { message: options }; } options.type = type; return Notification(options); }; });
通過這裏我們可以得知,原來平時使用的 this.$message.success
其實就是 Notification.success
,本質就是一個預先設定了 type
後再調用了 Notification
方法。
let instance; let instances = []; let seed = 1; const Notification = function(options) { if (Vue.prototype.$isServer) return; options = merge({}, options); const userOnClose = options.onClose; const id = 'notification_' + seed++; const position = options.position || 'top-right'; options.onClose = function() { Notification.close(id, userOnClose); }; instance = new NotificationConstructor({ data: options }); if (isVNode(options.message)) { instance.$slots.default = [options.message]; options.message = 'REPLACED_BY_VNODE'; } instance.id = id; instance.$mount(); document.body.appendChild(instance.$el); instance.visible = true; instance.dom = instance.$el; instance.dom.style.zIndex = PopupManager.nextZIndex(); let verticalOffset = options.offset || 0; instances.filter(item => item.position === position).forEach(item => { verticalOffset += item.$el.offsetHeight + 16; }); verticalOffset += 16; instance.verticalOffset = verticalOffset; instances.push(instance); return instance; };
上面的 NotificationConstructor
就是
import Main from './main.vue'; const NotificationConstructor = Vue.extend(Main);
使用Vue構造函數生成組件實例 instance
,這和我們默認用vue-cli生成vue項目的main.js是不是很像。
import App from './app.vue'; new Vue({ render: h => h(App), router, store: new Vuex.Store(store), }).$mount("#app");
唯一的區別是我們在 mounted
的時候沒有傳參數,這意味着我們需要自己將生成的dom插入目標位置,這也更符合我們的預期。
instances
數組是用來存儲多個組件實例的,在垂直方向如果有多個組件實例的話,會給它們直接加入16px的偏移量避免它們重疊在一起。
這裏還給每個實例更新了 verticalOffset
屬性的值來記錄垂直方向的偏移量,方便 positionStyle
變更,更新組件的位置。
我們同樣可以看到,在移除組件的時候,需要同步變更 instances
數組裏面該 instance
後面的實例對應的style樣式。
Notification.close = function(id, userOnClose) { let index = -1; const len = instances.length; const instance = instances.filter((instance, i) => { if (instance.id === id) { index = i; return true; } return false; })[0]; if (!instance) return; if (typeof userOnClose === 'function') { userOnClose(instance); } instances.splice(index, 1); if (len <= 1) return; const position = instance.position; const removedHeight = instance.dom.offsetHeight; for (let i = index; i < len - 1 ; i++) { if (instances[i].position === position) { instances[i].dom.style[instance.verticalProperty] = parseInt(instances[i].dom.style[instance.verticalProperty], 10) - removedHeight - 16 + 'px'; } } };
順便提一下, PopupManager
是element-ui裏面公共的通過z-index來確保最新創建的dom永遠保持在最上方不被舊的元素遮擋的機制,實現原理也很簡單。
鑑於message傳參的options.message支持傳入vNode,所以會有針對vNode相關的判斷和處理邏輯。
簡單總結一下,使用Vue構造函數這種方法是常用的命令式組件的套路,類似的還有Dialog(有的組件庫叫Modal)、Notice等。