天天看點

基于Vue3的element-ui message資訊提示元件實作

作者:前端梁哥

正式開始之前,讓我們先看下效果示範視訊。

視訊加載中...

因為錄制的這個視訊是30幀的,是以大家看起來會覺得不夠流暢,其實,真正的效果是非常流暢的。

使用過element-ui的童鞋們應該都用過this.$message吧?根據它的用法,我們可以推斷它的定義大概長這樣:

// 通用提示
const messageCreator = function (options) {}
// 成功提示
messageCreator.success = function (options) {}
// 警告提示
messageCreator.warning = function (options) {}
// 資訊提示
messageCreator.info = function (options) {}
// 錯誤提示
messageCreator.error = function (options) {}
// 關閉所有
messageCreator.closeAll = function (options) {}           

我們先從其它子產品引入一些用到的函數群組件。這些函數群組件都很簡單,我就不做太多講解了。

import { createApp, h, ref, watch, TransitionGroup } from 'vue'
// getMaxZIndex 擷取最大的層,genKey 生成具有唯一性的數值key
import { getMaxZIndex, genKey } from '../../utils'
// isStr 是否字元串判斷  const isStr = v => typeof v === 'string'
import { isStr } from '../../types'
// 資訊提示元件
import XMessage from './Message.vue'           

getMaxZIndex及genKey函數定義:

export function getMaxZIndex () {
  let maxZIndex = 1000
  const els = document.querySelectorAll('body>*')
  for (let i = 0, len = els.length; i < len; i++) {
    const { zIndex } = window.getComputedStyle(els[i], null)
    if (!isNaN(zIndex) && zIndex && zIndex > maxZIndex) {
      maxZIndex = +zIndex
    }
  }
  return maxZIndex + 2
}

export const genKey = ((k = 1) => () => k++)()           

Message.vue檔案源碼:

<template>
  <div v-if="visible" :class="[cls, customClass]">
    <div :class="innerClasses">
      <i :class="[iconClass || `x-icon-${type}`, `${cls}_icon`]"></i>
      <slot />
      <i v-if="showClose" :class="['x-icon-close', `${cls}_close`]" @click="onClose"></i>
    </div>
  </div>
</template>

<script setup>
import { computed, onMounted, ref } from 'vue'
// N: Number, S: String, B:Boolean, oneOf: (arr, v) => arr.includes(v)
import { N, S, B, oneOf } from '../../types'
const props = defineProps({
  type: {
    default: 'info',
    validator: v => oneOf(['success', 'warning', 'info', 'error'], v)
  },
  iconClass: S,
  customClass: S,
  duration: { type: N, default: 3000 },
  showClose: B,
  center: B
})
const emit = defineEmits(['close'])
const cls = 'x-message'
const innerClasses = computed(() => {
  return [
    `${cls}_inner`,
    props.type && `${cls}_${props.type}`,
    {
      'has-close': props.showClose,
      'is-center': props.center
    }
  ]
})
function onClose () {
  emit('close')
}
const visible = ref(false)
onMounted(() => {
  // 必須挂載完成後設定為可見!否則将失去顯示時的動畫!
  visible.value = true
  // 如果duration設定為0,則不自動關閉
  props.duration && setTimeout(onClose, props.duration)
})
</script>           

message.scss檔案:

@import './common/var.scss';
.x-message {
  padding: 6px 12px;
  font-size: 13px;
  text-align: center;
  width: 100%;
  transition: all .3s;
  &-leave-active {
    position: absolute;
  }
  &-enter-from, &-leave-to {
    opacity: 0;
    transform: translateY(-24px);
  }
  &-wrapper {
    position: fixed;
    top: 14px;
    right: 0;
    left: 0;
    pointer-events: none;
    transform: translateX(0);
    transition: top .3s;
  }
  &_inner {
    position: relative;
    min-width: 280px;
    border-radius: 4px;
    display: inline-flex;
    align-items: center;
    text-align: left;
    pointer-events: all;
    padding: 10px;
    color: $--color-info;
    border: 1px solid $--color-primary-light-7;
    background-color: $--color-primary-light-9;
    &.has-close {
      padding-right: 24px;
    }
    &.is-center {
      justify-content: center;
    }
  }
  &_icon {
    font-size: 14px;
    margin-right: 10px;
  }
  &_close {
    font-size: 14px;
    cursor: pointer;
    position: absolute;
    top: 50%;
    right: 8px;
    transform: translateY(-50%);
    color: $--color-info;
    &:hover {
      color: $--color-text-regular;
    }
  }
  &_success {
    color: $--color-success;
    border-color: mix(#fff, $--color-success, 70%);
    background-color: mix(#fff, $--color-success, 90%);
  }
  &_warning {
    color: $--color-warning;
    border-color: mix(#fff, $--color-warning, 70%);
    background-color: mix(#fff, $--color-warning, 90%);
  }
  &_error {
    color: $--color-danger;
    border-color: mix(#fff, $--color-danger, 70%);
    background-color: mix(#fff, $--color-danger, 90%);
  }
}           

現在,我們實作messageCreator函數。由于success,warning,info,error這4個方法都是通過messageCreator擴充而來,是以我們需要把這4個方法的調用,轉化為messageCreator函數的調用。我們需要一個轉化options的函數convertOptions。

function convertOptions (options, type) {
  if (isStr(options)) {
    options = { message: options }
  }
  if (type) {
    options.type = type
  }
  options.key = genKey()
  return options
}           

由于messageItem可以出現多個,并且是按順序從上到下依次排列,所有我們需要使用一個容器元素包裹這些item,采用transition-group進行組過渡。我們首先定義2個變量:

let vm // Vue app執行個體
const items = ref([]) // 用于存儲options的響應式數組           

每當調用this.$message的時候,向items中添加一個options,觸發視圖的更新。現在,我們定義messageCreator函數。我添加了大量注釋,大家應該能看懂吧?

const messageCreator = function (options, type) {
    // 轉換選項
    options = convertOptions(options, type)
    // 将轉換後的options添加進items
    items.value.push(options)
    // 如果vm不存在,我們建立一個
    if (!vm) {
      vm = createApp({
        render () {
          return h(TransitionGroup, { tag: 'div', name: 'x-message' }, {
            // 在預設插槽中渲染XMessage元件清單
            default: () => items.value.map((_, i) => h(XMessage, {
              ..._,
              // 當收到XMessage中發射的close事件時,将對應的options從items中移除
              onClose: () => {
                items.value.splice(i, 1)
                // 如果options中提供了onClose方法,那麼我們調用它
                _.onClose && _.onClose()
              }
            }, {
              // XMessage元件的預設插槽
              default: () => isStr(_.message) && _.dangerouslyUseHTMLString
                ? h('div', { innerHTML: _.message })
                : _.message
            }))
          })
        }
      })
      // 建立一個挂載點DOM元素
      const el = document.createElement('div')
      el.className = 'x-message-wrapper'
      el.style.zIndex = getMaxZIndex()
      // 将該元素添加到body中
      document.body.appendChild(el)
      // 将vm挂載到該元素
      vm.mount(el)
      // 監聽items的長度變化
      watch(
        () => items.value.length,
        (n, o) => {
          // 每添加一個options,就增加zIndex值,確定出現在頁面最頂層。
          if (n > o) el.style.zIndex = getMaxZIndex()
        }
      )
    }
  // 傳回一個對象,close方法用于手動關閉目前打開的資訊提示元件
    return {
      close () {
        const index = items.value.indexOf(options)
        index > -1 && items.value.splice(index, 1)
      }
    }
  }           

現在,我們定義messageCreator的幾個方法。

// 我們使用forEach,一次性定義這4個方法,這樣是不是很簡潔?
;['success', 'warning', 'info', 'error'].forEach(type => {
    messageCreator[type] = function (options) {
      return messageCreator(options, type)
    }
  })
  // 關閉所有,其實就是清空items
  messageCreator.closeAll = function () {
    items.value = []
  }           

最後,我們導出它。

export default messageCreator           

引入它,并添加到通過createApp傳回的app執行個體。

app.config.globalProperties.$message = messageCreator           

現在,我們可以照着element-ui文檔中的例子測試下我們的this.$message函數了。核心代碼是不是很簡單?童鞋們學會了嗎?

繼續閱讀