正式開始之前,讓我們先看下效果示範視訊。
視訊加載中...
因為錄制的這個視訊是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函數了。核心代碼是不是很簡單?童鞋們學會了嗎?