本文參考https://segmentfault.com/a/1190000018535744
loading/index.js
import directive from './src/directive'; import service from './src/index'; export default { // 這里為什么有個 install 呢 // 當你使用單組件單注冊的時候就會調用這里了 // 效果和下面一樣,掛載指令,掛載服務 install (Vue) { Vue.use(directive); Vue.prototype.$loading = service; }, // 就是上面的 Loading.directive directive, // 就是上面的 Loading.service service };
loading/src/loading.vue
<template> <transition name="el-loading-fade" @after-leave="handleAfterLeave"> <div v-show="visible" class="el-loading-mask" :style="{ backgroundColor: background || '' }" :class="[customClass, { 'is-fullscreen': fullscreen }]"> <div class="el-loading-spinner"> <svg v-if="!spinner" class="circular" viewBox="25 25 50 50"> <circle class="path" cx="50" cy="50" r="20" fill="none"/> </svg> <i v-else :class="spinner"></i> <p v-if="text" class="el-loading-text">{{ text }}</p> </div> </div> </transition> </template> <script> export default { data() { return { // 顯示在加載圖標下方的加載文案 string text: null, // 自定義加載圖標類名 string spinner: null, // 遮罩背景色 string background: null, // 同 v-loading 指令中的 fullscreen 修飾符 boolean fullscreen: true, // 是否顯示 visible: false, // customClass Loading 的自定義類名 string customClass: '' }; }, methods: { // loading結束后觸發 handleAfterLeave() { this.$emit('after-leave'); }, setText(text) { this.text = text; } } }; </script>
loading/src/directive.js
// v-loading 指令解析 import Vue from 'vue'; import Loading from './loading.vue'; import { addClass, removeClass, getStyle } from 'element-ui/src/utils/dom'; import { PopupManager } from 'element-ui/src/utils/popup'; import afterLeave from 'element-ui/src/utils/after-leave'; const Mask = Vue.extend(Loading); const loadingDirective = {}; // 還記得 Vue.use() 的使用方法么?若傳入的是對象,該對象需要一個 install 屬性 loadingDirective.install = Vue => { if (Vue.prototype.$isServer) return; // 這里處理顯示、消失 loading const toggleLoading = (el, binding) => { if (binding.value) { Vue.nextTick(() => { if (binding.modifiers.fullscreen) { el.originalPosition = getStyle(document.body, 'position'); el.originalOverflow = getStyle(document.body, 'overflow'); el.maskStyle.zIndex = PopupManager.nextZIndex(); addClass(el.mask, 'is-fullscreen'); insertDom(document.body, el, binding); } else { removeClass(el.mask, 'is-fullscreen'); if (binding.modifiers.body) { el.originalPosition = getStyle(document.body, 'position'); ['top', 'left'].forEach(property => { const scroll = property === 'top' ? 'scrollTop' : 'scrollLeft'; el.maskStyle[property] = el.getBoundingClientRect()[property] + document.body[scroll] + document.documentElement[scroll] - parseInt(getStyle(document.body, `margin-${property}`), 10) + 'px'; }); ['height', 'width'].forEach(property => { el.maskStyle[property] = el.getBoundingClientRect()[property] + 'px'; }); insertDom(document.body, el, binding); } else { el.originalPosition = getStyle(el, 'position'); insertDom(el, el, binding); } } }); } else { // 不然則將其設為不可見 // 從上往下讀我們是第一次看到 visible 屬性 // 別急,往下看,這個屬性可以其實就是單文件 loading.vue 里面的 // data() { return { visible: false } } afterLeave(el.instance, _ => { el.domVisible = false; const target = binding.modifiers.fullscreen || binding.modifiers.body ? document.body : el; removeClass(target, 'el-loading-parent--relative'); removeClass(target, 'el-loading-parent--hidden'); el.instance.hiding = false; }, 300, true); el.instance.visible = false; el.instance.hiding = true; } }; // 插入dom const insertDom = (parent, el, binding) => { if (!el.domVisible && getStyle(el, 'display') !== 'none' && getStyle(el, 'visibility') !== 'hidden') { Object.keys(el.maskStyle).forEach(property => { el.mask.style[property] = el.maskStyle[property]; }); if (el.originalPosition !== 'absolute' && el.originalPosition !== 'fixed') { addClass(parent, 'el-loading-parent--relative'); } if (binding.modifiers.fullscreen && binding.modifiers.lock) { addClass(parent, 'el-loading-parent--hidden'); } el.domVisible = true; // appendChild 添加的元素若為同一個,則不會重復添加 // 我們 el.mask 所指的為同一個 dom 元素 // 因為我們只在 bind 的時候給 el.mask 賦值 // 並且在組件存在期間,bind 只會調用一次 parent.appendChild(el.mask); Vue.nextTick(() => { if (el.instance.hiding) { el.instance.$emit('after-leave'); } else { // 將 loading 設為可見 el.instance.visible = true; } }); el.domInserted = true; } }; // 在此注冊 directive 指令 Vue.directive('loading', { bind: function (el, binding, vnode) { // 創建一個子組件,這里和 new Vue(options) 類似 // 返回一個組件實例 const textExr = el.getAttribute('element-loading-text'); const spinnerExr = el.getAttribute('element-loading-spinner'); const backgroundExr = el.getAttribute('element-loading-background'); const customClassExr = el.getAttribute('element-loading-custom-class'); const vm = vnode.context; const mask = new Mask({ el: document.createElement('div'), // 有些人看到這里會迷惑,為什么這個 data 不按照 Vue 官方建議傳函數進去呢? // 其實這里兩者皆可 // 稍微做一點延展好了,在 Vue 源碼里面,data 是延遲求值的 // 貼一點 Vue 源碼上來 // return function mergedInstanceDataFn() { // let instanceData = typeof childVal === 'function' // ? childVal.call(vm, vm) // : childVal; // let defaultData = typeof parentVal === 'function' // ? parentVal.call(vm, vm) // : parentVal; // if (instanceData) { // return mergeData(instanceData, defaultData) // } else { // return defaultData // } // } // instanceData 就是我們現在傳入的 data: {} // defaultData 就是我們 loading.vue 里面的 data() {} // 看了這段代碼應該就不難理解為什么可以傳對象進去了 data: { text: vm && vm[textExr] || textExr, spinner: vm && vm[spinnerExr] || spinnerExr, background: vm && vm[backgroundExr] || backgroundExr, customClass: vm && vm[customClassExr] || customClassExr, fullscreen: !!binding.modifiers.fullscreen } }); // 將創建的子類掛載到 el 上 // 在 directive 的文檔中建議 // 應該保證除了 el 之外其他參數(binding、vnode)都是只讀的 el.instance = mask; // 掛載 dom el.mask = mask.$el; el.maskStyle = {}; // 若 binding 的值為 truthy 運行 toogleLoading binding.value && toggleLoading(el, binding); }, update: function (el, binding) { el.instance.setText(el.getAttribute('element-loading-text')); if (binding.oldValue !== binding.value) { // 若舊不等於新值得時候(一般都是由 true 切換為 false 的時候) toggleLoading(el, binding); } }, unbind: function (el, binding) { if (el.domInserted) { // 移除dom el.mask && el.mask.parentNode && el.mask.parentNode.removeChild(el.mask); toggleLoading(el, { value: false, modifiers: binding.modifiers }); } el.instance && el.instance.$destroy(); } }); }; export default loadingDirective;
loading/src/index.js
// loading服務模式 import Vue from 'vue'; import loadingVue from './loading.vue'; import { addClass, removeClass, getStyle } from 'element-ui/src/utils/dom'; import { PopupManager } from 'element-ui/src/utils/popup'; import afterLeave from 'element-ui/src/utils/after-leave'; import merge from 'element-ui/src/utils/merge'; // 和指令模式一樣,創建實例構造器 const LoadingConstructor = Vue.extend(loadingVue); const defaults = { text: null, fullscreen: true, body: false, lock: false, customClass: '' }; // 定義變量,若使用的是全屏 loading 那就要保證全局的 loading 只有一個 let fullscreenLoading; LoadingConstructor.prototype.originalPosition = ''; LoadingConstructor.prototype.originalOverflow = ''; // 這里可以看到和指令模式不同的地方 // 在調用了 close 之后就會移除該元素並銷毀組件 LoadingConstructor.prototype.close = function () { if (this.fullscreen) { fullscreenLoading = undefined; } afterLeave(this, _ => { const target = this.fullscreen || this.body ? document.body : this.target; removeClass(target, 'el-loading-parent--relative'); removeClass(target, 'el-loading-parent--hidden'); if (this.$el && this.$el.parentNode) { this.$el.parentNode.removeChild(this.$el); } this.$destroy(); }, 300); this.visible = false; }; const addStyle = (options, parent, instance) => { let maskStyle = {}; if (options.fullscreen) { instance.originalPosition = getStyle(document.body, 'position'); instance.originalOverflow = getStyle(document.body, 'overflow'); maskStyle.zIndex = PopupManager.nextZIndex(); } else if (options.body) { instance.originalPosition = getStyle(document.body, 'position'); ['top', 'left'].forEach(property => { let scroll = property === 'top' ? 'scrollTop' : 'scrollLeft'; maskStyle[property] = options.target.getBoundingClientRect()[property] + document.body[scroll] + document.documentElement[scroll] + 'px'; }); ['height', 'width'].forEach(property => { maskStyle[property] = options.target.getBoundingClientRect()[property] + 'px'; }); } else { instance.originalPosition = getStyle(parent, 'position'); } Object.keys(maskStyle).forEach(property => { instance.$el.style[property] = maskStyle[property]; }); }; const Loading = (options = {}) => { if (Vue.prototype.$isServer) return; options = merge({}, defaults, options); // Loading 需要覆蓋的 DOM 節點。可傳入一個 DOM 對象或字符串;若傳入字符串,則會將其作為參數傳入 document.querySelector以獲取到對應 DOM 節點 if (typeof options.target === 'string') { options.target = document.querySelector(options.target); } options.target = options.target || document.body; if (options.target !== document.body) { options.fullscreen = false; } else { options.body = true; } // 若調用 loading 的時候傳入了 fullscreen 並且 fullscreenLoading 不為 falsy // fullscreenLoading 只會在下面賦值,並且指向了 loading 實例 if (options.fullscreen && fullscreenLoading) { return fullscreenLoading; } let parent = options.body ? document.body : options.target; // 這里就不用說了吧,和指令中是一樣的 let instance = new LoadingConstructor({ el: document.createElement('div'), data: options }); addStyle(options, parent, instance); if (instance.originalPosition !== 'absolute' && instance.originalPosition !== 'fixed') { addClass(parent, 'el-loading-parent--relative'); } if (options.fullscreen && options.lock) { addClass(parent, 'el-loading-parent--hidden'); } // 直接添加元素 parent.appendChild(instance.$el); Vue.nextTick(() => { instance.visible = true; }); // 若傳入了 fullscreen 參數,則將實例存儲 if (options.fullscreen) { // 需要注意的是,以服務的方式調用的全屏 Loading 是單例的:若在前一個全屏 Loading 關閉前再次調用全屏 Loading,並不會創建一個新的 Loading 實例,而是返回現有全屏 Loading 的實例 fullscreenLoading = instance; } return instance; }; export default Loading;