经过 自建vue组件 air-ui (5) -- 建立第一个组件 Button 和 自建vue组件 air-ui (6) -- 建立内置服务组件 咱们知道怎么建立标签组件和内置服务组件了,这一节咱们来说讲怎么建立指令组件。javascript
此次咱们作 loading
组件,仍是拿 element ui 的 loading 来参考, 它这个组件有点意思,有两种调用方式:css
这个其实就告诉咱们,只要你想,一个组件能够有不一样的表现方式,无论是标签方式,仍是指令,或者服务方式。 咱们先看下目录结构:html
components/
| |--- loading/
| | |--- src/
| | | |--- directive.js
| | | |--- index.js
| | | |--- loading.vue
| | |--- index.js
复制代码
从目录结构来看,应该很好理解。 .vue
结尾的是 dom 渲染, directive.js
是指令封装逻辑, index.js
是服务的封装逻辑。 根目录下的 index.js
是导出的方式vue
温故而知新,咱们已经在上节知道了怎么建立服务类型的组件,可是本节再温习一下也没有坏处。首先咱们看 loading.vue
这个 vue 组件的代码:java
<template>
<transition name="air-loading-fade" @after-leave="handleAfterLeave"> <div v-show="visible" class="air-loading-mask" :style="{ backgroundColor: background || '' }" :class="[customClass, { 'is-fullscreen': fullscreen }]"> <div class="air-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="air-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>
复制代码
逻辑很简单,就是一个 div,而后里面根据参数设置不一样的样式和类, 我这边不细说,接下来看 src/index.js
:node
import Vue from 'vue';
import loadingVue from './loading.vue';
import { addClass, removeClass, getStyle } from '../../../../src/utils/dom';
import { PopupManager } from '../../../../src/utils/popup';
import afterLeave from '../../../../src/utils/after-leave';
import merge from '../../../../src/utils/merge';
const LoadingConstructor = Vue.extend(loadingVue);
const defaults = {
text: null,
fullscreen: true,
body: false,
lock: false,
customClass: ''
};
let fullscreenLoading;
LoadingConstructor.prototype.originalPosition = '';
LoadingConstructor.prototype.originalOverflow = '';
LoadingConstructor.prototype.close = function() {
if (this.fullscreen) {
fullscreenLoading = undefined;
}
afterLeave(this, _ => {
const target = this.fullscreen || this.body
? document.body
: this.target;
removeClass(target, 'air-loading-parent--relative');
removeClass(target, 'air-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);
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;
}
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, 'air-loading-parent--relative');
}
if (options.fullscreen && options.lock) {
addClass(parent, 'air-loading-parent--hidden');
}
parent.appendChild(instance.$el);
Vue.nextTick(() => {
instance.visible = true;
});
if (options.fullscreen) {
fullscreenLoading = instance;
}
return instance;
};
export default Loading;
复制代码
逻辑跟上节的 notification 同样,也是先用Vue.extend(loadingVue)
生成一个构建函数,而后在 Loading
函数对象中,经过工厂方式去实例化这个对象,最后再添加到 body 或者 target 元素中,最后返回这个实例化的对象,固然中间有许多样式的处理,包括当前是否要全屏之类的。element-ui
这种服务的挂载是:bash
import service from './components/loading/src/index';
Vue.prototype.$loading = service;
复制代码
调用也是同样的:app
const loading = this.$loading({
lock: true,
text: 'Loading',
spinner: 'air-icon-loading',
background: 'rgba(0, 0, 0, 0.7)'
});
setTimeout(() => {
loading.close();
}, 2000);
复制代码
接下来咱们看下指令方式的逻辑: src/directive.js
:dom
import Vue from 'vue';
import Loading from './loading.vue';
import { addClass, removeClass, getStyle } from '../../../../src/utils/dom';
import { PopupManager } from '../../../../src/utils/popup';
import afterLeave from '../../../../src/utils/after-leave';
const Mask = Vue.extend(Loading);
const loadingDirective = {};
loadingDirective.install = Vue => {
if (Vue.prototype.$isServer) return;
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 {
afterLeave(el.instance, _ => {
if (!el.instance.hiding) return;
el.domVisible = false;
const target = binding.modifiers.fullscreen || binding.modifiers.body
? document.body
: el;
removeClass(target, 'air-loading-parent--relative');
removeClass(target, 'air-loading-parent--hidden');
el.instance.hiding = false;
}, 300, true);
el.instance.visible = false;
el.instance.hiding = true;
}
};
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, 'air-loading-parent--relative');
}
if (binding.modifiers.fullscreen && binding.modifiers.lock) {
addClass(parent, 'air-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) {
el.instance.visible = true;
el.instance.hiding = false;
}
};
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();
}
});
};
export default loadingDirective;
复制代码
这时候要说一下 vue 是怎么建立指令的,具体能够看文档自定义指令, 文档写的很是清楚了,我这边不打算详细讲太多,稍微提一下, vue 的自定义指令有两种方式:
// 注册一个全局自定义指令 `v-focus`
Vue.directive('focus', {
// 当被绑定的元素插入到 DOM 中时……
inserted: function (el) {
// 聚焦元素
el.focus()
}
})
复制代码
directives
的选项:directives: {
focus: {
// 指令的定义
inserted: function (el) {
el.focus()
}
}
}
复制代码
事实上,本节讲的 loading
, 就是全局注册的指令,其实 element-ui
也有局部组件的,在 src/directives
目录中,就存放了一些局部自定义指令: mousewheel
和 repeat-click
, 某些组件,好比 table
组件就会用到这些自定义的局部指令。
并且有如下几个钩子函数能够提供(这些都是可选的):
接下来咱们回到上面的代码中,分析一下:
loadingDirective
这个对象,而这个对象只有一个 install
方法,这就说明了这个指令的初始化方式确定是用 Vue.use
的方式引用的。install
方法中,才出现了注册全局指令的定义 Vue.directive('loading', {..}
,接下来咱们简单分析一下这几个钩子函数:bind
是第一次绑定到元素的时候,el
参数表示指令所绑定的元素,能够用来直接操做 DOM。 逻辑就是获取 el
的一些参数属性,而后渲染出 loading dom 模板,而后再根据是否要全屏显示的参数,来判断是插入到 el
中,仍是 body 中。update
方法,其实就是在 loading 的过程当中,咱们容许对文案进行更新,最多见的就是加载进度条后面的百分比进度,就是一直在更新unbind
就是移除绑定的操做调用的方式,就是相似于这样子:
import directive from './components/loading/src/directive';
Vue.use(directive);
复制代码
既然 loading
有全局指令的方式和内置服务(全局方法)的方式,并且初始化的方式不同,因此就统一为用 use
来调用, src/components/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
};
复制代码
导出的时候,有包含 install
方法,而后在 install
方法里面,针对两种方式进行初始化,因此在 main.js
的调用,就是:
import Loading from './components/loading'
Vue.use(Loading)
复制代码
这样就能够了,两种方式均可以被初始化。固然最好的方式仍是写在 src/components/index.js
里面:
...
import Loading from './loading'
...
const install = function (Vue) {
...
// Vue.use(Loading);
// 能够像上面那样用 use 直接两种都初始化,也能够像下面这一种,分开初始化
Vue.use(Loading.directive);
Vue.prototype.$loading = Loading.service;
...
}
export default {
install
}
复制代码
接下来咱们在 home.vue
进行测试, 在 template
加上这个:
<air-button type="primary" @click="openFullScreen1" v-loading.fullscreen.lock="fullscreenLoading">
指令方式
</air-button>
<air-button type="primary" @click="openFullScreen2">
服务方式
</air-button>
复制代码
表示两种方式的调用方式, 而后在 script
里面加上对应的参数和方法:
<script>
export default {
data () {
return {
...
fullscreenLoading: false
}
},
methods: {
...
openFullScreen1() {
this.fullscreenLoading = true;
setTimeout(() => {
this.fullscreenLoading = false;
}, 2000);
},
openFullScreen2() {
const loading = this.$loading({
lock: true,
text: 'Loading',
spinner: 'air-icon-loading',
background: 'rgba(0, 0, 0, 0.7)'
});
setTimeout(() => {
loading.close();
}, 2000);
}
}
}
</script>
复制代码
这样就能够看到效果了:
接下来点击指令方式,能够看到效果
点击服务方式,也能够看到效果,同时服务方式的定制化更高
这样子指令类型的组件的建立就完成了。 下节咱们讲一下,怎么部分引入组件
系列文章: