經過 自建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>
複製代碼
這樣就能夠看到效果了:
接下來點擊指令方式,能夠看到效果
點擊服務方式,也能夠看到效果,同時服務方式的定製化更高
這樣子指令類型的組件的建立就完成了。 下節咱們講一下,怎麼部分引入組件
系列文章: