天天看點

Vue元件的三種調用方式

最近在寫 fj-service-system

的時候,遇到了一些問題。那就是我有些元件,比如Dialog、Message這樣的元件,是引入三方元件庫,比如element-ui這樣的,還是自己實作一個?雖然它們有按需引入的功能,但是整體風格和我的整個系統不搭。于是就可以考慮自己手動實作這些簡單的元件了。

通常我們看Vue的一些文章的時候,我們能看到的通常是講Vue單檔案元件化開發頁面的。單一元件開發的文章相對就較少了。我在做

項目的時候,發現其實單一元件開發也是很有意思的。可以寫寫記錄下來。因為寫的不是什麼ui架構,是以也隻是一個記錄,沒有github倉庫,權且看代碼吧。

  • v-model或者.sync顯式控制元件顯示隐藏
  • 通過js代碼調用
  • 通過Vue指令調用

在寫元件的時候很多寫法、靈感來自于

element-ui

,感謝。

Dialog

我習慣把這個東西叫做對話框,實際上還有叫做modal(彈窗)元件的

叫法

。其實就是在頁面裡,彈出一個小視窗,這個小視窗裡的内容可以定制。通常可以用來做登入功能的對話框。

Vue元件的三種調用方式

這種元件就很适合通過v-model或者.sync的方式來顯式的控制出現和消失。它可以直接寫在頁面裡,然後通過data去控制——這也是最符合Vue的設計思路的元件。

為此我們可以寫一個元件就叫做Dialog.vue

<template>
  <div class="dialog">
    <div class="dialog__wrapper" v-if="visble" @clcik="closeModal">
      <div class="dialog">
        <div class="dialog__header">
          <div class="dialog__title">{{ title }}</div>
        </div>
        <div class="dialog__body">
          <slot></slot>
        </div>
        <div class="dialog__footer">
          <slot name="footer"></slot>
        </div>
      </div>
    </div>
    <div class="modal" v-show="visible"></div>
  </div>
</template>

<script>
  export default {
    name: 'dialog',
    props: {
      title: String,
      visible: {
        type: Boolean,
        default: false
      }
    },
    methods: {
      close() {
        this.$emit('update:visible', false) // 傳遞關閉事件
      },
      closeModal(e) {
        if (this.visible) {
          document.querySelector('.dialog').contains(e.target) ? '' : this.close(); // 判斷點選的落點在不在dialog對話框内,如果在對話框外就調用this.close()方法關閉對話框
        }
      }
    }
  }
</script>
           

CSS什麼的就不寫了,跟元件本身關系比較小。不過值得注意的是,上面的dialog__wrapper這個class也是全屏的,透明的,主要用于擷取點選事件并鎖定點選的位置,通過DOM的Node.contains()方法來判斷點選的位置是不是dialog本身,如果是點選到了dialog外面,比如半透明的modal層那麼就派發關閉事件,把dialog給關閉掉。

當我們在外部要調用的時候,就可以如下調用:

<template>
  <div class="xxx">
    <dialog :visible.sync="visible"></dialog> 
    <button @click="openDialog"></button>
  </div>
</template>

<script>
  import Dialog from 'Dialog'
  export default {
    components: {
      Dialog
    },
    data() {
      return {
        visible: false
      }
    },
    methods: {
      openDialog() {
        this.visible = true // 通過data顯式控制dialog
      }
    }
  }
</script>
           

為了Dialog開啟和關閉好看點,你可試着加上<transition></transition>元件配合上過渡效果,簡單的一點過渡動效也将會很好看。

Notice

這個元件類似于element-ui的

message

(消息提示)。它吸引我的最大的地方在于,它不是通過顯式的在頁面裡寫好元件的html結構通過v-model去調用的,而是通過在js裡通過形如this.$message()這樣的方法調用的。這種方法雖然跟Vue的資料驅動的思想有所違背。不過不得不說在某些情況下真的特别友善。

Vue元件的三種調用方式

對于Notice這種元件,一次隻要提示幾個文字,給使用者簡單的消息提示就行了。提示的資訊可能是多變的,甚至可以出現疊加的提示。如果通過第一種方式去調用,事先就得寫好html結構,這無疑是麻煩的做法,而且無法預知有多少消息提示框。而通過js的方法調用的話,隻需要考慮不同情況調用的文字、類型不同就可以了。

而之前的做法都是寫一個Vue檔案,然後通過components屬性引入頁面,顯式寫入标簽調用的。那麼如何将元件通過js的方法去調用呢?

這裡的關鍵是Vue的extend

方法

文檔裡并沒有詳細給出extend能這麼用,隻是作為需要手動mount的一個Vue的元件構造器說明了一下而已。

通過檢視element-ui的源碼,才算是了解了如何實作上述的功能。

首先依然是建立一個Notice.vue的檔案

<template>
  <div class="notice">
    <div class="content">
      {{ content }}
    </div>
  </div>
</template>

<script>
  export default {
    name: 'notice',
    data () {
      return {
        visible: false,
        content: '',
        duration: 3000
      }
    },
    methods: {
      setTimer() {
        setTimeout(() => {
          this.close() // 3000ms之後調用關閉方法
        }, this.duration)
      },
      close() {
        this.visible = false
        setTimeout(() => {
          this.$destroy(true)
          this.$el.parentNode.removeChild(this.$el) // 從DOM裡将這個元件移除
        }, 500)
      }
    },
    mounted() {
      this.setTimer() // 挂載的時候就開始計時,3000ms後消失
    }
  }
</script>
           

上面寫的東西跟普通的一個單檔案Vue元件沒有什麼太大的差別。不過差別就在于,沒有props了,那麼是如何通過外部來控制這個元件的顯隐呢?

是以還需要一個js檔案來接管這個元件,并調用extend方法。同目錄下可以建立一個index.js的檔案。

import Vue from 'vue'

const NoticeConstructor = Vue.extend(require('./Notice.vue')) // 直接将Vue元件作為Vue.extend的參數

let nId = 1

const Notice = (content) => {
  let id = 'notice-' + nId++

  const NoticeInstance = new NoticeConstructor({
    data: {
      content: content
    }
  }) // 執行個體化一個帶有content内容的Notice

  NoticeInstance.id = id
  NoticeInstance.vm = NoticeInstance.$mount() // 挂載但是并未插入dom,是一個完整的Vue執行個體
  NoticeInstance.vm.visible = true
  NoticeInstance.dom = NoticeInstance.vm.$el
  document.body.appendChild(NoticeInstance.dom) // 将dom插入body
  NoticeInstance.dom.style.zIndex = nId + 1001 // 後插入的Notice元件z-index加一,保證能蓋在之前的上面
  return NoticeInstance.vm
}

export default {
  install: Vue => {
    Vue.prototype.$notice = Notice // 将Notice元件暴露出去,并挂載在Vue的prototype上
  }
}
           

這個檔案裡我們能看到通過NoticeConstructor我們能夠通過js的方式去控制一個元件的各種屬性。最後我們把它注冊進Vue的prototype上,這樣我們就可以在頁面内部使用形如this.$notice()方法了,可以友善調用這個元件來寫做出簡單的通知提示效果了。

當然别忘了這個相當于一個Vue的插件,是以需要去主js裡調用一下Vue.use()方法:

// main.js

// ...
import Notice from 'notice/index.js'

Vue.use(Notice)

// ...
           

Loading

在看element-ui的時候,我也發現了一個很有意思的元件,是Loading,用于給一些需要加載資料等待的元件套上一層加載中的樣式的。這個loading的調用方式,最友善的就是通過v-loading這個指令,通過指派的true/false來控制Loading層的顯隐。這樣的調用方法當然也是很友善的。而且可以選擇整個頁面Loading或者某個元件Loading。這樣的開發體驗自然是很好的。

Vue元件的三種調用方式

其實跟Notice的思路差不多,不過因為涉及到directive,是以在邏輯上會相對複雜一點。

平時如果不涉及Vue的directive的開發,可能是不會接觸到modifiers、binding等概念。參考

文檔

簡單說下,形如:v-loading.fullscreen="true"這句話,v-loading就是directive,fullscreen就是它的modifier,true就是binding的value值。是以,就是通過這樣簡單的一句話實作全屏的loading效果,并且當沒有fullscreen修飾符的時候就是對擁有該指令的元素進行loading效果。元件通過binding的value值來控制loading的開啟和關閉。(類似于v-model的效果)

其實loading也是一個實際的DOM節點,隻不過要把它做成一個友善的指令還不是特别容易。

首先我們需要寫一下loading的Vue元件。建立一個Loading.vue檔案

<template>
  <transition
    name="loading"
  	@after-leave="handleAfterLeave">
    <div
      v-show="visible"
      class="loading-mask"
      :class={'fullscreen': fullscreen}>
      <div class="loading">
        ...
      </div>
      <div class="loading-text" v-if="text">
        {{ text }}
      </div>
    </div>
  </transition>
</template>
<script>
export default {
  name: 'loading',
  data () {
    return {
      visible: true,
      fullscreen: true,
      text: null
    }
  },
  methods: {
    handleAfterLeave() {
      this.$emit('after-leave');
    }
  }
}
</script>
<style>
.loading-mask{
  position: absolute; // 非全屏模式下,position是absolute
  z-index: 10000;
  background-color: rgba(255,235,215, .8);
  margin: 0;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  transition: opacity .3s;
}
.loading-mask.fullscreen{
  position: fixed; // 全屏模式下,position是fixed
}
// ...
</style>
           

Loading關鍵是實作兩個效果:

    1.全屏loading,此時可以通過插入body下,然後将Loading的position改為fixed,插入body實作。

    2.對所在的元素進行loading,此時需要對目前這個元素的的position修改:如果不是absolute的話,就将其修改為relatvie,并插入目前元素下。此時Loading的position就會相對于目前元素進行絕對定位了。

是以在目前目錄下建立一個index.js的檔案,用來聲明我們的directive的邏輯。

import Vue from 'vue'
const LoadingConstructor = Vue.extend(require('./Loading.vue'))

export default {
  install: Vue => {
    Vue.directive('loading', { // 指令的關鍵
      bind: (el, binding) => {
        const loading = new LoadingConstructor({ // 執行個體化一個loading
          el: document.createElement('div'),
          data: {
            text: el.getAttribute('loading-text'), // 通過loading-text屬性擷取loading的文字
            fullscreen: !!binding.modifiers.fullscreen 
          }
        })
        el.instance = loading; // el.instance是個Vue執行個體
        el.loading = loading.$el; // el.loading的DOM元素是loading.$el
        el.loadingStyle = {};
        toggleLoading(el, binding);
      },
      update: (el, binding) => {
        el.instance.setText(el.getAttribute('loading-text'))
        if(binding.oldValue !== binding.value) {
          toggleLoading(el, binding)
        }   
      },
      unbind: (el, binding) => { // 解綁
        if(el.domInserted) {
          if(binding.modifiers.fullscreen) {
              document.body.removeChild(el.loading);
          }else {
            el.loading &&
            el.loading.parentNode &&
            el.loading.parentNode.removeChild(el.loading);
          }
        }
      }
    })

    const toggleLoading = (el, binding) => { // 用于控制Loading的出現與消失
      if(binding.value) { 
        Vue.nextTick(() => {
          if (binding.modifiers.fullscreen) { // 如果是全屏
            el.originalPosition = document.body.style.position;
            el.originalOverflow = document.body.style.overflow;
            insertDom(document.body, el, binding); // 插入dom
          } else {
            el.originalPosition = el.style.position;
            insertDom(el, el, binding); // 如果非全屏,插入元素自身
          }
        })
      } else {
        if (el.domVisible) {
          el.instance.$on('after-leave', () => {
            el.domVisible = false;
            if (binding.modifiers.fullscreen && el.originalOverflow !== 'hidden') {
              document.body.style.overflow = el.originalOverflow;
            }
            if (binding.modifiers.fullscreen) {
              document.body.style.position = el.originalPosition;
            } else {
              el.style.position = el.originalPosition;
            }
          });
          el.instance.visible = false;
        }
      }
    }

    const insertDom = (parent, el, binding) => { // 插入dom的邏輯
      if(!el.domVisible) {
        Object.keys(el.loadingStyle).forEach(property => {
          el.loading.style[property] = el.loadingStyle[property];
        });
        if(el.originalPosition !== 'absolute') {
          parent.style.position = 'relative'
        }
        if (binding.modifiers.fullscreen) {
          parent.style.overflow = 'hidden'
        }
        el.domVisible = true;
        parent.appendChild(el.loading) // 插入的是el.loading而不是el本身
        Vue.nextTick(() => {
          el.instance.visible = true;
        });
        el.domInserted = true;
      }
    }
  }
}
           

同樣,寫完整個邏輯,我們需要将其注冊到項目裡的Vue下:

// main.js

// ...
import Loading from 'loading/index.js'

Vue.use(Loading)

// ...
           

至此我們已經可以使用形如

<div v-loading.fullscreen="loading" loading-text="正在加載中">

           

這樣的方式來實作調用一個loading元件了。

總結

在用Vue寫我們的項目的時候,不管是寫頁面還是寫形如這樣的功能型元件,其實都是一件很有意思的事情。本文介紹的三種調用元件的方式,也是根據實際情況出發而實際操作、實作的。不同的元件通過不同的方式去調用,友善了開發人員,也能更好地對代碼進行維護。當然也許還有其他的方式,我并沒有了解,也歡迎大家在評論裡指出!

最後再次感謝

的源碼給予的極大啟發。

文章作者: Molunerfinn

文章連結:

https://molunerfinn.com/vue-components/

版權聲明: 本部落格所有文章除特别聲明外,均采用 CC BY-NC-SA 4.0 許可協定。轉載請注明來自 MARKSZのBlog!

繼續閱讀