开始前的准备
将 elementui 的 master 分支完整的 clone 下来,传送门
执行
npm run dev
下载好依赖,如果总是执行失败,一般都是 nodesass 为下载成功导致的,这时需要提前下载好 nodesass 然后再执行npm run dev:play
就好.成功之后打开localhost:8085
,代码定位,因为执行的是
npm run dev:play
,具体代码如下
"dev:play": "npm run build:file && cross-env NODE_ENV=development PLAY_ENV=true webpack-dev-server --config build/webpack.demo.js"
然后我们查找build/webpack.demo.js
,然后找到如下代码
entry: isProd ? {
docs: './examples/entry.js'
} : (isPlay ? './examples/play.js' : './examples/entry.js'),
很明显,我们的入口在./examples/play.js
,然后我们根据这个文件找到 examples/play 文件夹下的 vue 文件,那么我们的准备工作就算是完成了。
正式开始
文档分析
该组件有两种调用方式,一种是指令(通过 v-loading 去调用),一种是通过 Vue 实例方法调用(通过 this.$loading 去调用)
目录结构
loading 文件夹下面的 index 文件代码如下
import directive from './src/directive';
import service from './src/index';
export default {
install(Vue) {
Vue.use(directive);
Vue.prototype.$loading = service;
},
directive,
service
};
根据以上代码,我们就可以知道支持指令来调用的在./src/directive
文件中,支持添加 Vue 实例方法调用的在./src/index
文件中。
添加 Vue 实例方法
- 查看源文件
通过目录结构,我们已经知道两种方法调用的源文件其实就是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 {
text: null,
spinner: null,
background: null,
fullscreen: true,
visible: false,
customClass: ''
};
},
methods: {
handleAfterLeave() {
this.$emit('after-leave');
},
setText(text) {
this.text = text;
}
}
};
</script>
源码内容比较简单,里面就是 transition 动画包裹的一个绝对定位的盒子,里面装着默认的 svg 以及自定位文字,
接下来是扩展部分,内容在 src 文件夹下的 index.js 。因为其中有很大一部分代码在处理样式及层级关系,所以被我剔除掉了,下面只展示核心代码:
import loadingVue from './loading.vue';
const LoadingConstructor = Vue.extend(loadingVue);
const defaults = {
text: null,
fullscreen: true,
body: false,
lock: false,
customClass: ''
};
let fullscreenLoading;//用来保存弹窗实例
// 关闭弹窗的方法
function afterLeave(instance, callback, speed = 300, once = false) {
let called = false;
const afterLeaveCallback = function () {
if (called) return;
called = true;
if (callback) {
callback.apply(null, arguments);
}
};
setTimeout(() => {
afterLeaveCallback();
}, speed + 100);
};
LoadingConstructor.prototype.close = function () {
// 清除弹窗实例
if (this.fullscreen) {
fullscreenLoading = undefined;
}
afterLeave(this, _ => {
// 结束后删除节点
if (this.$el && this.$el.parentNode) {
this.$el.parentNode.removeChild(this.$el);
}
this.$destroy();
}, 300);
this.visible = false;
};
let service = (options = {}) => {
// 合并options,源码中用的是 Object.assign 的 profill
options = Object.assign({}, defaults, options);
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;
}
// 覆盖整个body的loading只能有一个
if (options.fullscreen && fullscreenLoading) {
return fullscreenLoading;
}
let instance = new LoadingConstructor({
el: document.createElement('div'),
data: options
});
// 如果没有 target ,那么弹窗将会直接挂载在body上
let parent = options.body ? document.body : options.target;
parent.appendChild(instance.$el);
Vue.nextTick(() => {
instance.visible = true;
});
// 将实例赋值给 fullscreenLoading
if (options.fullscreen) {
fullscreenLoading = instance;
}
return instance;
}
Vue.prototype.$loading = service
最后再验证一下
<div id="app">
<!-- 因为loading组件是绝对定位,所以在扩展时会在父节点加上 el-loading-parent--relative-->
<div id="loading" class="el-loading-parent--relative"></div>
</div>
实例化
new Vue({
el: '#app',
data: function () {
return {
visible: true
}
},
mounted() {
const loading = this.$loading({
lock: true,
text: 'Loading',
spinner: 'el-icon-loading',
background: 'rgba(0, 0, 0, 0.7)',
target: "#loading"
})
setTimeout(() => {
loading.close();
}, 2000);
}
})
效果图如下
芜湖,添加 Vue 实例方法部分内容完成
关于自定义指令,详细的前置信息还是官方文档最全,传送门
接下来就是 elementui 中的源码了,为了更方便阅读,我也是做了一定的简化
import loadingVue from './loading.vue';
const Mask = Vue.extend(loadingVue);
Vue.directive('loading', {
// 只调用一次 指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置
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');
const vm = vnode.context;
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 = {};
binding.value && toggleLoading(el, binding);
},
update: function (el, binding) {
el.instance.setText(el.getAttribute('element-loading-text'));
if (binding.oldValue !== binding.value) {
toggleLoading(el, binding);
}
},
unbind: function (el, binding) {
if (el.domInserted) {
el.mask &&
el.mask.parentNode &&
el.mask.parentNode.removeChild(el.mask);
toggleLoading(el, { value: false, modifiers: binding.modifiers });
}
el.instance && el.instance.$destroy();
}
});
// 判断 loading 组件挂载的el
const toggleLoading = (el, binding) => {
if (binding.value) {
Vue.nextTick(() => {
// 判断 fullscreen 如果有则绑在 body 上
if (binding.modifiers.fullscreen) {
insertDom(document.body, el, binding);
} else {
// 判断是否要绑定在 body 上
if (binding.modifiers.body) {
insertDom(document.body, el, binding);
} else {
insertDom(el, el, binding);
}
}
});
} else {
afterLeave(el.instance, _ => {
if (!el.instance.hiding) return;
el.domVisible = false;
el.instance.hiding = false;
}, 300, true);
el.instance.visible = false;
el.instance.hiding = true;
}
};
// 顾名思义 将DOM插入到文档中,并控制源码中 loading.vue 的显示和隐藏
const insertDom = (parent, el, binding) => {
// 判断 domVisible 以及 el 中样式是否消失或隐藏
if (!el.domVisible && el.style['display'] !== 'none' && el.style['visibility'] !== 'hidden') {
Object.keys(el.maskStyle).forEach(property => {
el.mask.style[property] = el.maskStyle[property];
});
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) {
el.instance.visible = true;
el.instance.hiding = false;
}
};
添加自定义指令部分内容完成,更多详情见github
最后,源码中是将以上的代码包裹在 loadingDirective 这个对象中,然后在 loadingDirective 上面添加 install 方法,最终在外层 index 文件中通过vue.use调用,简化代码如下
const loadingDirective = {};
loadingDirective.install = Vue => {
// 上面的代码
}
下面是源码的简略图
自此,loading 组件的源码分析全部结束