前言
在项目开发过程中,为了提高开发效率,我们通常会去封装一些方法、指令、插件和组件等,为了学习更多的封装技巧,我们可以通过学习别人的代码来提升自己。因此有了该系列文章。
本文通过阅读 element-ui 的源码,学习 loading 功能的实现。如有问题,望指出🙏。
源码入口
首先,我们从项目的 node_modules 中找到 element-ui 依赖,然后找到它未经打包过的入口文件
node_modules/element-ui/src/index.js(当然,也可以直接从github上克隆源码)
从源码中可以看出:
- 通过 install 方法,让 element-ui 支持
Vue.use(Element)的方式来注册所有方法和指令 v-loading指令 和this.$loading方法都是在 install 方法中注册到全局的
loading 目录
根据入口文件中 Loading 的引入路径,我们可以找到 Loading 所在目录
import Loading from '../packages/loading/index.js';
import directive from './src/directive';
import service from './src/index';
export default {
install(Vue) {
Vue.use(directive);
Vue.prototype.$loading = service;
},
directive,
service
};
可以看到,loading 支持四种方式:
- 通过插件 install 的方式注册,
- 通过 directive 指令的方式
- 通过 service 方法,且 service 被注入到vue原型上,就是平时常用的
this.$loading - 同时,loading 也支持通过 Loading.service 的方式来开启 loading
v-loading 实现
打开loading指定所在目录element-ui/packages/loading/src/directive.js
以下为源码的简化代码
import Vue from 'vue';
import Loading from './loading.vue';
// 将loading组件转为 vue 组件构造函数
const Mask = Vue.extend(Loading);
const loadingDirective = {};
loadingDirective.install = Vue => {
const toggleLoading = (el, binding) => {
// ...
}
// ...
// 注册 v-loading 指令
Vue.directive('loading', {
// 指令绑定到元素时调用
bind(el, binding) {
// 从 loading目标获取对应 html 属性
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');
// 获取需要显示 loading 的目标的 vue 实例
const vm = vnode.context;
// 创建一个 loading 组件的 vue 实例
const mask = new Mask({
el: document.createElement('div'),
data: {
// 获取 loading 的配置
// 优先用传给 loading 组件的属性,如果没有,从 loading 的目标元素上获取对应 html 属性
text: vm && vm[textExr] || textExr,
spinner: vm && vm[spinnerExr] || spinnerExr,
background: vm && vm[backgroundExr] || backgroundExr,
customClass: vm && vm[customClassExr] || customClassExr,
fullscreen: !!binding.modifiers.fullscreen
}
});
// 将 loading 的实例挂载到 el 元素上,让 toogleLoading 可以通过该实例来修改 loading 组件的属性
el.instance = mask;
el.mask = mask.$el;
el.maskStyle = {};
// 操作 loading 的挂载与移除
binding.value && toggleLoading(el, binding);
},
// 指令更新时调用
update(el, binding) {
// ...
},
// loading目标被销毁时调用
unbind(el, binding) {
// ...
},
}
}
可以看到,在指令方法中,我们通过 vnode.context 获取了 loading 目标的 vue 实例,然后通过 new Mask() 创建了一个 loading 的 vue 实例,并把 el 元素作为挂载元素,然后将loading实例挂载到 el 元素上。接下来,就是通过 toggleLoading 方法,将 loading 实例挂载到 loading 目标 el 上。
此处为 toggleLoading 简化后的代码
const toggleLoading = (el, binding) => {
if(binding.value) {
Vue.nexTick(() => {
// 通过 binding.modifiers 获取指令的修饰符 fullscreen,来判断是否需要全屏显示
if(binding.modifiers.fullscreen) {
// ...一些全屏loading样式设置
// 将 loading 组件挂载到 body 元素上
insertDom(document.body, el, binding)
} else {
// 非全屏 loading
// ...一些非全屏loading样式设置,并移除全屏样式
// 将 loading 组件挂载到 loading 目标元素上
insertDom(el, el, binding)
}
})
} else {
// ...
// 通过 loading 实例,修改 loading.vue 组件中的 visible 属性,来关闭 loading
el.instance.visible = false
// ...
}
}
// 将 loading 组件挂载到目标元素
const insertDom = (parent, el, binding) => {
// 通过 domVisible 来标识 loading 组件是否已挂载到目标元素
if (!el.domVisible && getStyle(el, 'display') !== 'none' && getStyle(el, 'visibility') !== 'hidden') {
// loading 还没挂载
// ...
// 将 domVisible 设为 true,并将 loading 组件挂载到目标元素
el.domVisible = true;
parent.appendChild(el.mask);
Vue.nextTick(() => {
if (el.instance.hiding) {
// 隐藏loading
el.instance.$emit('after-leave');
} else {
// 显示loading
el.instance.visible = true;
}
});
// 标识 loading 组件已挂载
el.domInserted = true;
} else if (el.domVisible && el.instance.hiding === true) {
// loading 已经挂载
// 显示 loading
el.instance.visible = true;
el.instance.hiding = false;
}
};
通过以上代码可以看出: 初始化时,toogleLoding 方法会先判断 loading 组件是否已挂载到目标元素,如果没有,则将 loading 组件挂载到目标元素上;如果已挂载,就通过 el.instance.visible 来控制 loading 组件的显示与隐藏。
loading 组件
element-ui/packages/loading/src/loading.vue
loading组件的实现相对简单,此处不做展开,以下为源码内容
<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>
以上为全文内容🙏。