实现基础模板
- 路径 packages/loading/src/loading.vue
- 这个正常一个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 {
text: null,
spinner: null,
background: null,
fullscreen: true,
visible: false,
customClass: ''
};
},
methods: {
handleAfterLeave() {
this.$emit('after-leave');
},
setText(text) {
this.text = text;
}
}
};
</script>
实现服务方式调用
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)
// 这里大部分是基础模板的data里的变量,lock判断是否是需要锁定,body判断是否是body节点
// 默认的配置
const defaults = {
text: null,
fullscreen: true,
body: false,
lock: false,
customClass: ''
}
let fullscreenLoading
// 初始化position和overflow属性为空
LoadingConstructor.prototype.originalPosition = ''
LoadingConstructor.prototype.originalOverflow = ''
// 这里先不看, 看下方 查看步骤 1
LoadingConstructor.prototype.close = function () {
// this代表 组件内部data, 全屏选项的关闭,会清空fullscreenLoading
if (this.fullscreen) {
fullscreenLoading = undefined
}
// 组件移除后需要移除target身上的class,和新增dom节点(loading)
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')
// 调用弹出层管理器,获取递增的zindex值
maskStyle.zIndex = PopupManager.nextZIndex()
} else if (options.body) {
instance.originalPosition = getStyle(document.body, 'position')
// 根据父级滚动的 scrollTop 和 scrollLeft 计算自己应该定位的值,保证自己可以覆盖当前可视区
['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]
})
}
// 步骤 1
// 这个方法被抛出去,被上一层的index.js引用,命名为service,同时 绑定到Vue的原型上,install里面Vue.prototype.$loading = service
// 所以这里options是我们传递过的配置选项
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
// 判断target是不是body,是的话认为是body为true,不是的话,就取消全屏
if (options.target !== document.body) {
options.fullscreen = false
} else {
options.body = true
}
// 判断全屏选项下 fullscreenLoading是否已经初始化过了,如果有,直接返回
if (options.fullscreen && fullscreenLoading) {
return fullscreenLoading
}
let parent = options.body ? document.body : options.target
// 这里new 一个新的组件,data会混合基础组件的data,el会体现在组件的对象的$el属性上
let instance = new LoadingConstructor({
el: document.createElement('div'),
data: options
})
addStyle(options, parent, instance)
// 根据父级定位信息 决定是否需要增加relative属性
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)
// dom节点插入到父级,操作完成后,显示loading
Vue.nextTick(() => {
instance.visible = true
})
// 如果是全屏,fullscreenLoading对象赋值,因为全屏,是绑定到body下面,所以父级是固定不变,所以上面判断如果全屏,可以直接返回相同的loading实例,多次调用会使用同一个loading实例
if (options.fullscreen) {
fullscreenLoading = instance
}
return instance
}
export default 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'
// loading组件 继承基础组件
const Mask = Vue.extend(Loading)
const loadingDirective = {}
// install 方便被引用的时候 vue.use
loadingDirective.install = Vue => {
// 是否已经注册过该指令了
if (Vue.prototype.$isServer) return
// 暂缓查看步骤1
const toggleLoading = (el, binding) => {
if (binding.value) {
// v-loading绑定是true的时候,等组件渲染完毕
Vue.nextTick(() => {
// 判断修饰符是不是全屏
if (binding.modifiers.fullscreen) {
// 获取父级样式信息
el.originalPosition = getStyle(document.body, 'position')
el.originalOverflow = getStyle(document.body, 'overflow')
// 调用弹出层管理器,获取递增的zindex值
el.maskStyle.zIndex = PopupManager.nextZIndex()
addClass(el.mask, 'is-fullscreen')
insertDom(document.body, el, binding)
} else {
removeClass(el.mask, 'is-fullscreen')
// 判断修饰符是否挂载到body上
// 计算位置信息,和大小信息
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 {
afterLeave(el.instance, _ => {
if (!el.instance.hiding) return
el.domVisible = false
// 父组件,如果是 fullscreen,body修饰符,就是body,否则就是指令挂载本身
// 移除样式, el.instance.hiding设置为false,防止多次卸载
const target = binding.modifiers.fullscreen || binding.modifiers.body
? document.body
: el
removeClass(target, 'el-loading-parent--relative')
removeClass(target, 'el-loading-parent--hidden')
// 设置hiding为false,代表已经移除该节点
el.instance.hiding = false
}, 300, true)
el.instance.visible = false
el.instance.hiding = true
}
}
const insertDom = (parent, el, binding) => {
// 指令绑定的本身的 domVisible 要是false,防止重复插入, 同时本身不能是隐藏的
if (!el.domVisible && getStyle(el, 'display') !== 'none' && getStyle(el, 'visibility') !== 'hidden') {
// 设置蒙层的样式,位置和大小
Object.keys(el.maskStyle).forEach(property => {
el.mask.style[property] = el.maskStyle[property]
})
// 根据父级定位信息 决定父级是否需要增加relative属性
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
parent.appendChild(el.mask)
Vue.nextTick(() => {
if (el.instance.hiding) {
el.instance.$emit('after-leave')
} else {
el.instance.visible = true
}
})
// 节点已经被插入
el.domInserted = true
} else if (el.domVisible && el.instance.hiding === true) {
// 如果 节点被隐藏了,同时hiding是true,说明已经插入了,不再重复插入,直接调用就可以了
el.instance.visible = true
el.instance.hiding = false
}
}
// 步骤1, 注册指令loading ,就是我们使用的时候 v-loading
Vue.directive('loading', {
// el 指令所绑定的元素,可以用来直接操作 DOM
/**
* binding:一个对象,包含以下 property:
* name:指令名,不包括 v- 前缀。 loading
* value:指令的绑定值,例如:v-loading="1 > 2" 中,绑定值为 false。
* oldValue:指令绑定的前一个值,仅在 update 和 componentUpdated 钩子中可用。无论值是否改变都可用。
* expression:字符串形式的指令表达式。例如 v-loading="1 > 2" 中,表达式为 "1 > 2"。
* arg:传给指令的参数,可选。例如 v-loading:foo 中,参数为 "foo"。
* modifiers:一个包含修饰符的对象。例如:v-loading.fullscreen.lock="fullscreenLoading" 中,修饰符对象为 { fullscreen: true, lock: true }。
*/
bind: function(el, binding, vnode) {
// 获取自定义的样式,文案
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')
// context: Component | void
const vm = vnode.context
// 新增一个loading蒙层,挂载到一个新生成的div节点上
// binding.modifiers.fullscreen 判断修饰符有没有全屏
const mask = new Mask({
el: document.createElement('div'),
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.instance = mask
el.mask = mask.$el
el.maskStyle = {}
// 如果是指令绑定的值true就执行toggleLoading
binding.value && toggleLoading(el, binding)
},
// 所在组件的 VNode 更新时调用
update: function(el, binding) {
// 重新设置文案
el.instance.setText(el.getAttribute('element-loading-text'))
// 新老值不一样的时候,才调用,防止重复调用
if (binding.oldValue !== binding.value) {
toggleLoading(el, binding)
}
},
// 只调用一次,指令与元素解绑时调用。
unbind: function(el, binding) {
// 实例被插入过,同时有mask和mask父节点,就移除mask节点。
// 调用toggleLoading方法,走afterLeave,做移除class等清理工作
if (el.domInserted) {
el.mask &&
el.mask.parentNode &&
el.mask.parentNode.removeChild(el.mask)
toggleLoading(el, { value: false, modifiers: binding.modifiers })
}
// el实例存在的话手动调用摧毁
el.instance && el.instance.$destroy()
}
})
}
export default loadingDirective