關於 vue 彈窗組件的一些感想

最近是用 vue 開發了一套組件庫 vue-carbon , 在開發過程對對於組件化的開發有一些感想,因而開始記錄下這些。javascript

彈窗組件一直是 web 開發中必備的,使用頻率至關高,最多見的莫過於 alert,confirm,prompt .. 這些(曾經咱們都會用alert來調試程序), 不一樣的組件庫對於彈窗的處理也是不同的。在開發時須要考慮一下三點:css

  1. 進入和彈出的動畫效果。html

  2. z-index 的控制vue

  3. overlay 遮蓋層java

關於動畫

vue 對於動畫的處理相對簡單,給組件加入css transition 動畫便可node

<template>
<div class="modal" transition="modal-scale">
    <!--省略其它內容-->
</div>
</template>
<script>
// ...
</script>
<style>
.modal-scale-transition{
  transition: transform,opacity .3s ease;
}

.modal-scale-enter,
.modal-scale-leave {
    opacity: 0;
}

.modal-scale-enter {
  transform: scale(1.1);
}
.modal-scale-leave {
  transform: scale(0.8);
}
</style>

外部能夠由使用者自行控制,使用 v-if 或是 v-show 控制顯示git

z-index 的控制

關於z-index的控制,須要完成如下幾點github

  1. 保證彈出框的 z-index 足夠高能使 其再最外層web

  2. 後彈出的彈出框的 z-index 要比以前彈出的要高app

要知足以上兩點, 咱們須要如下代碼實現

const zIndex = 20141223  // 先預設較高值

const getZIndex = function () {
    return zIndex++ // 每次獲取以後 zindex 自動增長
}

而後綁定把 z-index 在組件上

<template>
<div class="modal" :style="{'z-index': zIndex}" transition="modal-scale">
    <!--省略其它內容-->
</div>
</template>
<script>
export default {
    data () {
        return {
            zIndex: getZIndex()
        }
    }
}
</script>

overlay 遮蓋層的控制

遮蓋層是彈窗組件中最難處理的部分, 一個完美的遮蓋層的控制須要完成如下幾點:

  1. 遮蓋層和彈出層之間的動畫須要並行

  2. 遮蓋層的 z-index 要較小與彈出層

  3. 遮蓋層的彈出時須要組件頁面滾動

  4. 點擊遮蓋層須要給予彈出層反饋

  5. 保證整個頁面最多隻能有一個遮蓋層(多個疊在一塊兒會使遮蓋層顏色加深)

爲了處理這些問題,也保證全部的彈出框組件不用每個都解決,因此決定利用 vue 的 mixins 機制,將這些彈出層的公共邏輯封裝層一個 mixin ,每一個彈出框組件直接引用就好。

vue-popup-mixin

明確了上述全部的問題,開始開發 mixin, 首先須要一個 overlay (遮蓋層組件) ;

<template>
  <div class="overlay" @click="handlerClick" @touchmove="prevent" :style="style" transition="overlay-fade"></div>
</template>
<script>
export default {
  props: {
    onClick: {
      type: Function
    },
    opacity: {
      type: Number,
      default: 0.4
    },
    color: {
      type: String,
      default: '#000'
    }
  },
  computed: {
    style () {
      return {
        'opacity': this.opacity,
        'background-color': this.color
      }
    }
  },
  methods: {
    prevent (event) {
      event.preventDefault()
      event.stopPropagation()
    },
    handlerClick () {
      if (this.onClick) {
        this.onClick()
      }
    }
  }
}
</script>
<style lang="less">
.overlay {
  position: fixed;
  left: 0;
  right: 0;
  top: 0;
  bottom: 0;
  background-color: #000;
  opacity: .4;
  z-index: 1000;
}


.overlay-fade-transition {
  transition: all .3s linear;
  &.overlay-fade-enter,
  &.overlay-fade-leave {
    opacity: 0 !important;
  }
}
</style>

而後 須要一個 js 來管理 overlay 的顯示和隱藏。

import Vue from 'vue'
import overlayOpt from '../overlay'  // 引入 overlay 組件
const Overlay = Vue.extend(overlayOpt)

const getDOM = function (dom) {
  if (dom.nodeType === 3) {
    dom = dom.nextElementSibling || dom.nextSibling
    getDOM(dom)
  }
  return dom
}

// z-index 控制
const zIndex = 20141223  

const getZIndex = function () {
    return zIndex++ 
}
// 管理
const PopupManager = {
  instances: [],  // 用來儲存全部的彈出層實例
  overlay: false,
  // 彈窗框打開時 調用此方法
  open (instance) {
    if (!instance || this.instances.indexOf(instance) !== -1) return
    
    // 當沒有遮蓋層時,顯示遮蓋層
    if (this.instances.length === 0) {
      this.showOverlay(instance.overlayColor, instance.overlayOpacity)
    }
    this.instances.push(instance) // 儲存打開的彈出框組件
    this.changeOverlayStyle() // 控制不一樣彈出層 透明度和顏色
    
    // 給彈出層加上z-index
    const dom = getDOM(instance.$el)
    dom.style.zIndex = getZIndex()
  },
  // 彈出框關閉方法
  close (instance) {
    let index = this.instances.indexOf(instance)
    if (index === -1) return
    
    Vue.nextTick(() => {
      this.instances.splice(index, 1)
      
      // 當頁面上沒有彈出層了就關閉遮蓋層
      if (this.instances.length === 0) {
        this.closeOverlay()
      }
      this.changeOverlayStyle()
    })
  },
  showOverlay (color, opacity) {
    let overlay = this.overlay = new Overlay({
      el: document.createElement('div')
    })
    const dom = getDOM(overlay.$el)
    dom.style.zIndex = getZIndex()
    overlay.color = color
    overlay.opacity = opacity
    overlay.onClick = this.handlerOverlayClick.bind(this)
    overlay.$appendTo(document.body)

    // 禁止頁面滾動
    this.bodyOverflow = document.body.style.overflow
    document.body.style.overflow = 'hidden'
  },
  closeOverlay () {
    if (!this.overlay) return
    document.body.style.overflow = this.bodyOverflow
    let overlay = this.overlay
    this.overlay = null
    overlay.$remove(() => {
      overlay.$destroy()
    })
  },
  changeOverlayStyle () {
    if (!this.overlay || this.instances.length === 0) return
    const instance = this.instances[this.instances.length - 1]
    this.overlay.color = instance.overlayColor
    this.overlay.opacity = instance.overlayOpacity
  },
  // 遮蓋層點擊處理,會自動調用 彈出層的 overlayClick 方法
  handlerOverlayClick () {
    if (this.instances.length === 0) return
    const instance = this.instances[this.instances.length - 1]
    if (instance.overlayClick) {
      instance.overlayClick()
    }
  }
}

window.addEventListener('keydown', function (event) {
  if (event.keyCode === 27) { // ESC
    if (PopupManager.instances.length > 0) {
      const topInstance = PopupManager.instances[PopupManager.instances.length - 1]
      if (!topInstance) return
      if (topInstance.escPress) {
        topInstance.escPress()
      }
    }
  }
})

export default PopupManager

最後再封裝成一個 mixin

import PopupManager from './popup-manager'

export default {
  props: {
    show: {
      type: Boolean,
      default: false
    },
    // 是否顯示遮蓋層
    overlay: {
      type: Boolean,
      default: true
    },
    overlayOpacity: {
      type: Number,
      default: 0.4
    },
    overlayColor: {
      type: String,
      default: '#000'
    }
  },
  // 組件被掛載時會判斷show的值開控制打開
  attached () {
    if (this.show && this.overlay) {
      PopupManager.open(this)
    }
  },
  // 組件被移除時關閉
  detached () {
    PopupManager.close(this)
  },
  watch: {
    show (val) {
      // 修改 show 值是調用對於的打開關閉方法
      if (val && this.overlay) {
        PopupManager.open(this)
      } else {
        PopupManager.close(this)
      }
    }
  },
  beforeDestroy () {
    PopupManager.close(this)
  }
}

使用

以上全部的代碼就完成了全部彈出層的共有邏輯, 使用時只須要當作一個mixin來加載便可

<template>
  <div class="dialog"
    v-show="show"
    transition="dialog-fade">
    <div class="dialog-content">
      <slot></slot>
    </div>
  </div>
</template>

<style>
  .dialog {
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
    position: fixed;
    width: 90%;
  }

  .dialog-content {
    background: #fff;
    border-radius: 8px;
    padding: 20px;
    text-align: center;
  }

  .dialog-fade-transition {
    transition: opacity .3s linear;
  }

  .dialog-fade-enter,
  .dialog-fade-leave {
    opacity: 0;
  }
</style>

<script>
import Popup from '../src'

export default {
  mixins: [Popup],
  methods: {
    // 響應 overlay事件
    overlayClick () {
      this.show = false
    },
    // 響應 esc 按鍵事件
    escPress () {
      this.show = false
    }
  }
}
</script>

項目地址 vue-popup-mixin

相關文章
相關標籤/搜索