自建vue組件 air-ui (7) -- 建立指令組件

前言

經過 自建vue組件 air-ui (5) -- 建立第一個組件 Button自建vue組件 air-ui (6) -- 建立內置服務組件 咱們知道怎麼建立標籤組件和內置服務組件了,這一節咱們來說講怎麼建立指令組件。javascript

此次咱們作 loading 組件,仍是拿 element ui 的 loading 來參考, 它這個組件有點意思,有兩種調用方式:css

  1. 全局指令方式
  2. 全局方法的服務方式

這個其實就告訴咱們,只要你想,一個組件能夠有不一樣的表現方式,無論是標籤方式,仍是指令,或者服務方式。 咱們先看下目錄結構:html

components/
|    |--- loading/
|    |     |--- src/
|    |     |     |--- directive.js
|    |     |     |--- index.js
|    |     |     |--- loading.vue
|    |     |--- index.js
複製代碼

從目錄結構來看,應該很好理解。 .vue 結尾的是 dom 渲染, directive.js 是指令封裝邏輯, index.js 是服務的封裝邏輯。 根目錄下的 index.js 是導出的方式vue

loading 的服務方式

溫故而知新,咱們已經在上節知道了怎麼建立服務類型的組件,可是本節再溫習一下也沒有壞處。首先咱們看 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 的自定義指令有兩種方式:

  1. 註冊全局組件,好比:
// 註冊一個全局自定義指令 `v-focus`
Vue.directive('focus', {
  // 當被綁定的元素插入到 DOM 中時……
  inserted: function (el) {
    // 聚焦元素
    el.focus()
  }
})
複製代碼
  1. 在組件中註冊局部組件,組件中也接受一個 directives 的選項:
directives: {
  focus: {
    // 指令的定義
    inserted: function (el) {
      el.focus()
    }
  }
}
複製代碼

事實上,本節講的 loading, 就是全局註冊的指令,其實 element-ui 也有局部組件的,在 src/directives目錄中,就存放了一些局部自定義指令: mousewheelrepeat-click, 某些組件,好比 table 組件就會用到這些自定義的局部指令。

並且有如下幾個鉤子函數能夠提供(這些都是可選的):

  • bind:只調用一次,指令第一次綁定到元素時調用。在這裏能夠進行一次性的初始化設置。
  • inserted:被綁定元素插入父節點時調用 (僅保證父節點存在,但不必定已被插入文檔中)。
  • update:所在組件的 VNode 更新時調用,可是可能發生在其子 VNode 更新以前。指令的值可能發生了改變,也可能沒有。可是你能夠經過比較更新先後的值來忽略沒必要要的模板更新 (詳細的鉤子函數參數見下)。
  • componentUpdated:指令所在組件的 VNode 及其子 VNode 所有更新後調用。
  • unbind:只調用一次,指令與元素解綁時調用。

接下來咱們回到上面的代碼中,分析一下:

  1. 首先返回了 loadingDirective 這個對象,而這個對象只有一個 install 方法,這就說明了這個指令的初始化方式確定是用 Vue.use 的方式引用的。
  2. install 方法中,纔出現了註冊全局指令的定義 Vue.directive('loading', {..},接下來咱們簡單分析一下這幾個鉤子函數:
  3. bind 是第一次綁定到元素的時候,el 參數表示指令所綁定的元素,能夠用來直接操做 DOM。 邏輯就是獲取 el 的一些參數屬性,而後渲染出 loading dom 模板,而後再根據是否要全屏顯示的參數,來判斷是插入到 el 中,仍是 body 中。
  4. update 方法,其實就是在 loading 的過程當中,咱們容許對文案進行更新,最多見的就是加載進度條後面的百分比進度,就是一直在更新
  5. 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>
複製代碼

這樣就能夠看到效果了:

1

接下來點擊指令方式,能夠看到效果

1

點擊服務方式,也能夠看到效果,同時服務方式的定製化更高

1

總結

這樣子指令類型的組件的建立就完成了。 下節咱們講一下,怎麼部分引入組件


系列文章:

相關文章
相關標籤/搜索