简介
本文将介绍组件的服务实现原理,耐心读完,相信会对您有所帮助。 为了更换的理解本文,请优先阅读下前文Loading指令实现。
更多组件分析详见 👉 📚 Element UI 源码剖析组件总览 。
本专栏的 gitbook
版本地址已经发布 📚《learning element-ui》 ,内容同步更新中!
服务方式
前文中介绍了服务方式的使用方法Loading.service(options)
,文件packages\loading\src\index.js
实现了该功能,其中很多的逻辑实现跟全文指令实现相同。
源码实现
源码简介后结构如下:
// packages\loading\src\index.js
// ...
// 返回一个扩展实例构造器(使用基础 Vue 构造器,创建一个“子类”)
const LoadingConstructor = Vue.extend(loadingVue);
// 默认配置
const defaults = {
text: null, // 加载图标下方的加载文案
fullscreen: true, // 默认全屏遮罩
body: false, // 遮罩插入至 DOM 中的 body 上
lock: false, // 全屏模式下锁定屏幕的滚动
customClass: '' // 自定义类名
};
// 全屏遮罩实例缓存
let fullscreenLoading;
// 添加 originalPosition originalOverflow属性
LoadingConstructor.prototype.originalPosition = '';
LoadingConstructor.prototype.originalOverflow = '';
// 添加 close() 用于移除该元素并销毁组件
LoadingConstructor.prototype.close = function() {
// ...
};
// 为loading组件以及插入父节点 添加样式
const addStyle = (options, parent, instance) => {
// ...
};
// 返回组件实例
const Loading = (options = {}) => {
// ...
return instance;
};
export default Loading;
使用Vue.extend
返回一个 loding 组件扩展实例构造器,用于实例的创建。同时为构造器对象添加了 originalPosition
、 originalOverflow
属性和 close()
方法,方便实例的使用。
Loading()
组件使用service
方法就是导出的 Loading
,用于返回组件实例 ,options
参数配置项具体见 官方文档 options。
const Loading = (options = {}) => {
// Vue 实例是否运行于服务器
if (Vue.prototype.$isServer) return;
// 更新组件配置项
options = merge({}, defaults, options);
// Loading 需要覆盖的 DOM 节点
if (typeof options.target === 'string') {
options.target = document.querySelector(options.target);
}
options.target = options.target || document.body;
// 遮罩插入节点不是 body 时,fullscreen 设置无效
if (options.target !== document.body) {
options.fullscreen = false;
} else {
options.body = true;
}
// 返回全屏遮罩实例
if (options.fullscreen && fullscreenLoading) {
return fullscreenLoading;
}
// 获取 loading 组件将要插入的父节点
let parent = options.body ? document.body : options.target;
// 创建组件实例
let instance = new LoadingConstructor({
el: document.createElement('div'),
data: options
});
addStyle(options, parent, instance);
// 根据父节点Position属性 决定父级是否需要增加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');
}
// 将loading实例附加到指定父节点的子节点列表的末尾
parent.appendChild(instance.$el);
Vue.nextTick(() => {
// 显示组件
instance.visible = true;
});
if (options.fullscreen) {
// 全屏时 缓存组件实例
fullscreenLoading = instance;
}
return instance;
};
根据初始配置项 const defaults = { fullscreen: true }
,可知使用服务方法时默认全屏加载,默认插入节点就是 body
元素,所以 参数target
默认值处理会有 document.body
。
target
可传入一个 DOM 对象或字符串;若传入字符串,则会将其作为参数传入 document.querySelector
以获取到对应 DOM 节点。
if (typeof options.target === 'string') {
options.target = document.querySelector(options.target);
}
options.target = options.target || document.body;
对于全屏加载模式,target
必须指定body
元素;否则认为时区域加载。
// 遮罩插入节点不是 body 时,fullscreen 设置无效
if (options.target !== document.body) {
options.fullscreen = false;
} else {
options.body = true;
}
前文介绍服务方式使用时,提到Loading 是单例的:若在前一个全屏 Loading 关闭前再次调用全屏 Loading,并不会创建一个新的 Loading 实例,而是返回现有全屏 Loading 的实例。
const loadingInstance1 = this.$loading({ fullscreen: true });
const loadingInstance2 = this.$loading({ fullscreen: true });
console.log(loadingInstance1 === loadingInstance2); // true
实现逻辑如下,定了变量fullscreenLoading
缓存全屏加载的遮罩组件实例,首次调用service
方法返回 实例 instance
,随后若是再次调用 service
创建全屏组件,代码就直接返回 fullscreenLoading
,这也是为什么前面提到的两个实例是同一个。
// 全屏遮罩实例缓存
let fullscreenLoading;
const Loading = (options = {}) => {
// ...
// 返回全屏遮罩实例
if (options.fullscreen && fullscreenLoading) {
return fullscreenLoading;
}
// ...
if (options.fullscreen) {
// 全屏时 缓存组件实例
fullscreenLoading = instance;
}
return instance;
};
close()
方法 close
用于移除该组件实例的元素并销毁该实例 。
LoadingConstructor.prototype.close = function() {
if (this.fullscreen) {
fullscreenLoading = undefined;
}
afterLeave(this, _ => {
console.log('afterLeave');
// 判断父节点是否为 body
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;
console.log('visible afterLeave');
};
addStyle()
方法addStyle
用于loading组件DOM操作(元素插入、样式更新)。
const addStyle = (options, parent, instance) => {
// 遮罩样式
let maskStyle = {};
if (options.fullscreen) { // 全屏/整页模式
// 获取body元素position属性值
instance.originalPosition = getStyle(document.body, 'position');
// 获取body元素overflow属性值
instance.originalOverflow = getStyle(document.body, 'overflow');
// 设置loading的DOM堆叠层级 z-index
maskStyle.zIndex = PopupManager.nextZIndex();
} else if (options.body) {
// 区域加载非全屏/整页模式 指定组件插入节点为 body
instance.originalPosition = getStyle(document.body, 'position');
// 计算遮罩的left、top
// 因为是区域加载 偏移量要基于绑定元素的left、top值
['top', 'left'].forEach(property => {
let scroll = property === 'top' ? 'scrollTop' : 'scrollLeft';
maskStyle[property] = options.target.getBoundingClientRect()[property] +
document.body[scroll] +
document.documentElement[scroll] +
'px';
});
// 计算遮罩的 width 、height
['height', 'width'].forEach(property => {
maskStyle[property] = options.target.getBoundingClientRect()[property] + 'px';
});
} else {
// 绑定元素的父节点 position 属性
instance.originalPosition = getStyle(parent, 'position');
}
// 遮罩组件样式绑定
Object.keys(maskStyle).forEach(property => {
instance.$el.style[property] = maskStyle[property];
});
};
样式实现
组件样式源码 packages\theme-chalk\src\loading.scss
使用混合指令 b
、when
、m
嵌套生成组件样式。
@include b(loading-parent) {
// 生成 .el-loading-parent--relative
@include m(relative) {
// ...
}
// 生成 .el-loading-parent--hidden
@include m(hidden) {
// ...
}
}
// 生成 .el-loading-mask
@include b(loading-mask) {
// ...
// 生成 .el-loading-mask.is-fullscreen
@include when(fullscreen) {
// ...
// 生成 .el-loading-mask.is-fullscreen .el-loading-spinner
.el-loading-spinner {
// ...
// 生成.el-loading-mask.is-fullscreen .el-loading-spinner .circular
.circular {
// ...
}
}
}
}
// 生成 .el-loading-spinner
@include b(loading-spinner) {
// ...
// 生成 .el-loading-spinner .el-loading-text
.el-loading-text {
// ...
}
// 生成 .el-loading-spinner .circular
.circular {
// ...
animation: loading-rotate 2s linear infinite;
}
// 生成 .el-loading-spinner .path
.path {
// ...
animation: loading-dash 1.5s ease-in-out infinite;
}
// 生成 .el-loading-spinner i
i {
// ...
}
}
// 生成 .el-loading-fade-enter,.el-loading-fade-leave-active
.el-loading-fade-enter,
.el-loading-fade-leave-active {
// ...
}
// 关键帧定义 loading-rotate
@keyframes loading-rotate {
// ...
}
// 关键帧定义 loading-dash
@keyframes loading-dash {
// ...
}
📚参考&关联阅读
Loading指令实现
'Vue-extend',vuejs
关注专栏
此文章已收录到专栏中 👇,可以直接关注。