Vue $mount實戰--實現消息彈窗組件

以前的項目一直在使用 Element-UI框架,element中的 NotificationMessage組件使用時不須要在html寫標籤,而是使用js調用。那時就很疑惑,爲何element ui使用 this.$notifythis.$message就能夠實現這樣的功能?

一、實現消息彈窗組件的幾個問題

  • 如何在任何組件中使用this.$message就能夠顯示消息?
  • 如何將消息的dom節點插入到body中?
  • 同時出現多個消息彈窗時,消息彈窗的z-index如何控制?

二、效果預覽

圖片描述

三、代碼實現

PMessage.vuehtml

<template>
  <transition name="message-fade">
    <div class="p-message"
         :class="[type, extraClass]"
         v-show="show"
          @mouseenter="clearTimer"
          @mouseleave="startTimer">
      <div class="p-message-container">
        <i class="p-message-icon" :class="`p-message-icon-${type}`"></i>
        <div class="p-message-content">
          <slot class="p-message-content">
            <div v-html="message"></div>
          </slot>
        </div>
      </div>
    </div>
  </transition>
</template>

<script>
  // 綁定事件
  function _addEvent(el, eventName, fn){
    if(document.addEventListener){
      el.addEventListener(eventName, fn, false);
    }else if(window.attachEvent){
      el.attactEvent('on' + eventName, fn);
    }
  };
  // 解綁事件
  function _offEvent(el, eventName, fn){
    if(document.removeEventListener){
      el.removeEventListener(eventName, fn, false);
    }else if(window.detachEvent){
      el.detachEvent('on' + eventName, fn);
    }
  };
  export default {
    name: "PMessage",
    data(){
      return {
        type: 'success',
        duration: 3000,
        extraClass: '',
        message: '',
        timer: null,
        closed: false,
        show: false
      }
    },
    methods: {
      startTimer(){
        if(this.duration > 0){
          this.timer = setTimeout(() => {
            if(!this.closed){
              this.close();
            }
          }, this.duration);
        }
      },
      clearTimer(){
        clearTimeout(this.timer);
      },
      close(){
        this.closed = true;
        if(typeof this.onClose === 'function'){
          // 調用onClose方法,以從p-message.js中的instances數組中移除當前組件,不移除的話就佔空間了
          this.onClose();
        }
      },
      // 銷燬組件
      destroyElement(){
        _offEvent(this.$el, 'transitionend', this.destroyElement);
        // 手動銷燬組件
        this.$destroy(true);
        this.$el.parentNode.removeChild(this.$el);
      },
    },
    watch: {
      // 監聽closed,若是它爲true,則銷燬message組件
      closed(newVal){
        if(newVal){
          this.show = false;
          // message過渡完成後再去銷燬message組件及移除元素
          _addEvent(this.$el, 'transitionend', this.destroyElement);
        }
      }
    },
    mounted() {
      this.startTimer();
    }
  }
</script>

<style lang="stylus">
@import "p-message.styl"
</style>

p-message.jsvue

import Vue from 'vue';
import PMessage from './PMessage.vue';
import {popupManager} from "../../common/js/popup-manager";
let PMessageControl = Vue.extend(PMessage);
let count = 0;
// 存儲message組件實例,如需有關閉全部message的功能就須要將每一個message組件都存儲起來
let instances = [];
const isVNode = function (node) {
  return node !== null && typeof node === 'object' && Object.prototype.hasOwnProperty.call(node, 'componentOptions');
};

const Message = function (options) {
  options = options || {};
  if(typeof options === 'string'){
    options = {
      message: options
    };
  }
  let id = 'message_' + ++count;
  let userOnClose = options.onClose;
  // PMsesage.vue銷燬時會調用傳遞進去的onClose,而onClose的處理就是將指定id的message組件從instances中移除
  options.onClose = function (){
    Message._close(id, userOnClose);
  };
  /* 這裏傳遞給PMessageControl的data不會覆蓋PMessage.vue中原有的data,而是與PMessage.vue中原有的data進行合併,相似
   * 與mixin,包括傳遞methods、生命週期函數也是同樣 */
  let instance = new PMessageControl({
    data: options
  });
  // 傳遞vNode
  if(isVNode(instance.message)){
    instance.$slots.default = [instance.message];
    instance.message = null;
  }
  instance.id = id;
  // 渲染元素,隨後使用原生appendChild將dom插入到頁面中
  instance.$mount();
  let $el = instance.$el;
  // message彈窗的z-index由popupManager來提供
  $el.style.zIndex = popupManager.getNextZIndex();
  document.body.appendChild($el);
  // 將message顯示出來
  instance.show = true;
  console.log(instance)
  instances.push(instance);
  return instance;
};
// message簡化操做
['success','error'].forEach(function (item) {
  Message[item] = options => {
    if(typeof options === 'string'){
      options = {
        message: options
      }
    }
    options.type = item;
    return Message(options);
  }
});
/**
 * 從instances刪除指定message,內部使用
 * @param id
 * @param userOnClose
 * @private
 */
Message._close = function (id, userOnClose) {
  for(var i = 0, len = instances.length; i < len; i++){
    if(instances[i].id === id){
      if(typeof userOnClose === 'function'){
        userOnClose(instances[i]);
      }
      instances.splice(i, 1);
      break;
    }
  }
};
// 關閉全部message
Message.closeAll = function () {
  for(var i = instances.length - 1; i >= 0; i--){
    instances.close();
  }
};

export default Message;

popup-manager.jsnode

let zIndex = 1000;
let hasZIndexInited = false;
const popupManager = {
  // 獲取索引
  getNextZIndex(){
    if(!hasZIndexInited){
      hasZIndexInited = true;
      return zIndex;
    }
    return zIndex++;
  }
};
export {popupManager};

p-index.js數組

import pMessage from './p-message.js';
export default pMessage;

p-message.stylapp

.p-message{
  position: fixed;
  top: 20px;
  left: 50%;
  padding: 8px 15px;
  border-radius: 4px;
  background-color: #fff;
  color: #000;
  transform: translateX(-50%);
  transition: opacity .3s, transform .4s;
  &.message-fade-enter,
  &.message-fade-leave-to{
    opacity: 0;
    transform: translateX(-50%) translateY(-30px);
  }
  &.message-fade-enter-to,
  &.message-fade-leave{
    opacity: 1;
    transform: translateX(-50%) translateY(0);
  }
  &.error{
    color: #ff3737;
  }
  .p-message-icon{ /* 使圖標與內容可以垂直居中 */
    display: table-cell;
    vertical-align: middle;
    width: 64px;
    height: 45px;
    &.p-message-icon-success{
      background: url("../../assets/images/icons/message-icon/icon_success.png") no-repeat 0 0;
    }
    &.p-message-icon-error{
      background: url("../../assets/images/icons/message-icon/icon_error.png") no-repeat 0 0;
    }
  }
  .p-message-content{ /* 使圖標與內容可以垂直居中 */
    display: table-cell;
    vertical-align: middle;
    padding-left: 15px;
  }
}

main.js框架

// 引入pMessage組件
import pMessage from './components/p-message/p-index.js';
// 將pMessage綁定到Vue.prototype中。這樣在組件中就能夠經過this.$pMessage()的形式來使用了
Vue.prototype.$pMessage = pMessage;

三、參考

相關文章
相關標籤/搜索