仿 ElementPlus 组件库(六)—— Message 组件实现

在仿 ElementPlus 组件库的征程中,我们已经成功攻克了 Dropdown 组件的实现。接下来,让我们一同深入探索 Message 组件的实现。

一、什么是 Message 组件

Message 组件,即消息提示组件,以其轻量级、非阻塞的特性,在各类应用中频繁出现。无论是操作反馈、系统通知还是信息提醒,Message 组件都能及时且有效地将关键信息传达给用户,成为打造流畅、友好交互界面的重要基石。

二、实现 Message 组件

(一)组件目录

目录 复制代码
components
├── Message
    ├── Message.vue
    ├── types.ts
    ├── method.ts
    ├── style.css   

(二)实现 Message 组件基本功能

  • 消息内容展示 :可以接收一个 message 属性,该属性可以是字符串或虚拟节点(VNode),用于在组件中显示具体的消息内容。如果父组件没有传递内容,且 message 属性有值,会通过 RenderVnode 组件渲染该内容。
  • 消息类型区分 :通过 type 属性来定义消息的类型,支持 'success''info''warning''danger' 四种类型。不同类型会为组件添加不同的 CSS 类名,从而实现不同的视觉样式,方便用户快速识别消息的性质。
  • 显示时长控制duration 属性用于设置消息显示的时长,单位为毫秒。当 duration0 时,消息不会自动关闭;否则,在到达指定时间后消息会自动消失。
  • 关闭按钮显示showClose 属性控制是否显示关闭按钮。如果为 true,会在消息提示框中显示关闭按钮,用户点击关闭按钮可以手动关闭消息提示。
  • 自动显示与隐藏 :在组件挂载到页面后,消息提示会自动显示(visible 设为 true),并根据 duration 的值启动定时器,在指定时间后自动隐藏(visible 设为 false)。
types.ts 复制代码
import type { VNode } from 'vue'
export interface MessageProps {
  message?: string | VNode
  duration?: number
  showClose?: boolean
  type?: 'success' | 'info' | 'warning' | 'danger'
}
Message.vue 复制代码
<template>
  <div
    class="yl-message"
    v-show="visible"
    :class="{
      [`yl-message--${type}`]: type,
      'is-close': showClose,
    }"
    role="alert"
  >
    <div class="yl-message__content">
      <slot><RenderVnode :vNode="message" v-if="message" /> </slot>
    </div>
    <div class="yl-message__close" v-if="showClose">
      <Icon @click.stop="visible = false" icon="xmark" />
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue'
import type { MessageProps } from './types'
import RenderVnode from '../Common/RenderVnode'
import Icon from '../Icon/Icon.vue'
const props = withDefaults(defineProps<MessageProps>(), {
  type: 'info',
  duration: 3000,
})
const visible = ref(false)
function startTimer() {
  if (props.duration === 0) return
  setTimeout(() => {
    visible.value = false
  }, props.duration)
}
onMounted(() => {
  visible.value = true
  startTimer()
})
</script>  
App.vue 复制代码
import Message from './components/Message/Message.vue'

(三)将 Message 组件渲染到DOM节点

在之前代码基础上,将 Message 组件更简洁地渲染到 DOM 节点

  • 渲染节点调整 :在 createMessage 函数中,从直接将包含 Message 组件的 container 元素添加到 document.body,改为添加 container 的第一个子元素,即 Message 组件渲染后生成的 DOM 节点,避免引入不必要的包裹元素,使页面结构更简洁。
  • 非空断言使用 :使用非空断言操作符 ! 来告诉 TypeScript,container.firstElementChild 不会是 nullundefined,消除类型检查警告。
method.ts 复制代码
import { render, h } from 'vue'
import type { MessageProps } from './types'
import MessageConstructor from './Message.vue'

export const createMessage = (props: MessageProps) => {
  const container = document.createElement('div')
  const vnode = h(MessageConstructor, props)
  render(vnode, container)
  //!非空断言操作符
  document.body.appendChild(container.firstElementChild! )
}
App.vue 复制代码
import { createMessage } from './components/Message/method'

(四)实现 Message 组件隐藏后的销毁功能

  • 在原有的 Message 组件基础上,实现了组件隐藏后的销毁功能。
  • 新增销毁函数 :在 createMessage 函数内部定义了 destory 函数。该函数调用 render(null, container),其作用是将 null 渲染到之前创建的 container 容器中,从而清空容器内的内容,实现组件的销毁。
  • 传递销毁回调 :创建了一个新的属性对象 newProps,它合并了原有的 props 以及新添加的 onDestory 属性,该属性的值为 destory 函数。这样在创建 Message 组件的虚拟节点时,就把销毁组件的回调函数传递给了 Message 组件。
  • 监听 visible 变化 :使用 watch 函数监听 visible 响应式变量的变化。visible 通常用于控制 Message 组件的显示与隐藏状态。
  • 触发销毁操作 :当 visible 的值变为 false(即组件隐藏)时,调用 props.onDestory(),也就是执行之前传递进来的 destory 函数,从而实现组件隐藏后自动销毁的功能。
method.ts 复制代码
export const createMessage = (props: MessageProps) => {
  //...
  const destory = () => {
    render(null, container)
  }
  const newProps = {
    ...props,
    onDestory: destory
  }
 //...
}
types.ts 复制代码
import type { CreateMessageProps } from './types'

export const createMessage = (props: CreateMessageProps) => {
//...
  const destory = () => {
    render(null, container)
  }
  const newProps = {
    ...props,
    onDestory: destory
  }
  const vnode = h(MessageConstructor, newProps)
//...
}
Message.vue 复制代码
import { ref, onMounted, watch } from 'vue'

watch(visible, (newValue) => {
  if (!newValue) {
    props.onDestory()
  }
})

(五)实现 Message 组件定位功能

通过维护一个 instances 数组来存储所有 Message 组件的实例信息,实现了获取不同实例内容的功能。同时,通过计算组件的偏移量,使得每个 Message 组件可以根据上一个组件的位置进行定位,避免组件之间的重叠。具体步骤如下:

  • 为每个 Message 组件实例生成唯一的 id,并将实例信息存储在 instances 数组中。
  • 提供 getLastInstance 函数,方便获取最后一个组件实例。
  • 定义 MessageProps 接口的 offset 属性和 MessageContext 接口的 vm 属性,用于计算偏移量和存储组件内部实例。
  • 实现 getLastBottomOffset 函数,用于获取上一个组件实例的底部偏移量。
  • Message.vue 组件中,通过计算 lastOffsettopOffsetcssStyle 变量,实现组件的偏移定位。
  • onMounted 钩子中获取组件的高度,并通过 defineExpose 暴露相关属性。

获取不同实例的内容

types.ts 复制代码
export interface MessageContext {
  id: string;
  vnode: VNode;
  props: MessageProps;
}
method.ts 复制代码
import type { CreateMessageProps, MessageContext } from './types'
let seed = 1
const instances: MessageContext[] = []

export const createMessage = (props: CreateMessageProps) => {
  const id = `message_${seed++}`
 //...
   const destory = () => {
    //删除数组中的实例
    const idx = instances.findIndex(instance => instance.id === id)
    if (idx === -1) return
    instances.splice(idx, 1)
    render(null, container)
  }
  
  const instance = {
    id,
    vnode,
    props: newProps,
  }
  instances.push(instance)
  return instance
}

export const getLastInstance = () => {
  return instances.length > 0 ? instances[instances.length - 1] : undefined
}
Message.vue 复制代码
import { getLastInstance } from './method'
const prevInstance = getLastInstance()
console.log('prev',prevInstance);

计算偏移量以实现定位功能

types.ts 复制代码
import type { VNode, ComponentInternalInstance } from 'vue'

export interface MessageProps {
//...
  id: string
  offset?: number;
}

export interface MessageContext {
//...
  vm: ComponentInternalInstance
}

export type CreateMessageProps = Omit<MessageProps, 'onDestory' | 'id'>
method.ts 复制代码
import { render, h, shallowReactive } from 'vue'
const instances: MessageContext[] = shallowReactive([])
  const newProps = {
    ...props,
    id,
    onDestory: destory,
  }
  
  const vm = vnode.component!
  const instance = {
    id,
    vnode,
    vm,
    props: newProps,
  }
  instances.push(instance)
  return instance
}

export const getLastBottomOffset = (id: string) => {
  const idx = instances.findIndex((instance) => instance.id === id)
  console.log('idx', id, idx, instances.length)
  if (idx <= 0) {
    return 0
  } else {
    const prev = instances[idx - 1]
    return prev.vm.exposed!.bottomOffset.value
  }
}
Message.vue 复制代码
<template>
  <div
    //...
    :class="{
    //...
    ref="messageRef"
    :style="cssStyle"
  >
    //...
</template>

import { ref, onMounted, watch, computed, nextTick } from 'vue'
const props = withDefaults(defineProps<MessageProps>(), {
//...
  offset:20
})
const messageRef = ref<HTMLDivElement>()
//计算偏移高度
//这个 div 的高度
const height = ref(0)
//上一个实例的最下面的坐标数字,第一个是0
const lastOffset = computed(() => getLastBottomOffset(props.id))
//这个元素应该使用的 top
const topOffset = computed(() => props.offset + lastOffset.value)
//这个元素为下一个元素预留的 offset, 也就是他最底端 bottom 的值
const cssStyle = computed(() => ({
  top: topOffset.value + 'px',
}))

onMounted(async () => {
//...
  await nextTick()
  height.value = messageRef.value!.getBoundingClientRect().height
})

defineExpose({
  bottomOffset,
  visible
})

(六)微调 Message 组件功能

添加手动删除

增加手动删除组件的功能。具体实现步骤如下:

  • 定义了 manualDestroy 函数,该函数可以根据组件的 idinstances 数组中找到对应的组件实例,并将其 visible 属性设置为 false,从而实现手动销毁组件的目的。
  • manualDestroy 函数作为 destroy 属性添加到每个 Message 组件实例对象中,使得每个实例都具备手动销毁的能力。
  • types.ts 文件中更新 MessageContext 接口,添加 destory 属性的类型定义,以保证 TypeScript 对代码的类型检查正常工作。
methods.ts 复制代码
  //手动调用删除,即手动调整组件中visible值,visible通过expose传出
  const manualDestroy = () => {
    const instance = instances.find((instance) => instance.id === id)
    if (instance) {
      instance.vm.exposed!.visible.value = false
    }
  }
  
    const instance = {
//...
    destroy: manualDestroy,
  }
types.ts 复制代码
export interface MessageContext {
//...
  destory: () => void
}

添加z-index

Message 组件增加了 z-index 相关功能,实现对 Message 组件层叠顺序的动态控制。具体实现过程如下:

  • 创建了 useZIndex 函数,用于管理 z-index 的初始值、当前值以及生成下一个 z-index 值。
  • types.ts 中更新了组件属性接口,增加 zIndex 属性并定义了新的创建属性类型。
  • method.ts 中引入 useZIndex 并使用 nextZIndex 函数为每个 Message 组件实例生成唯一递增的 z-index 值。
  • Message.vue 中通过计算属性将 z-index 应用到组件的样式中,使每个 Message 组件在页面上能够根据 z-index 值正确地进行层叠显示,避免消息组件之间的层叠冲突,提升了组件在复杂界面中的显示效果和交互性。
src/hooks/useZIndex.ts 复制代码
import { computed, ref } from 'vue'

const zIndex = ref(0)
const useZIndex = (initialValue = 2000) => {
  const initialZIndex = ref(initialValue)
  const currentZIndex = computed(() => zIndex.value + initialZIndex.value)
  const nextZIndex = () => {
    zIndex.value ++
    return currentZIndex.value
  }
  return {
    currentZIndex,
    nextZIndex,
    initialZIndex
  }
}

export default useZIndex
types.ts 复制代码
export interface MessageProps {
//...
  zIndex: number
}
export type CreateMessageProps = Omit<MessageProps, 'onDestory' | 'id' | 'zIndex'>
method.ts 复制代码
import useZIndex from '@/hooks/useZIndex'
const { nextZIndex } = useZIndex()
  const newProps = {
    ...props,
    id,
    zIndex: nextZIndex(),
    onDestory: destory,
  }
Message.vue 复制代码
const cssStyle = computed(() => ({
//...
  zIndex:props.zIndex
}))

添加键盘关闭

  • 封装事件监听逻辑 :创建 useEventListener 组合式函数,将事件监听和移除的逻辑封装起来,提高了代码的复用性和可维护性。该函数能够处理 target 为响应式引用或普通 EventTarget 对象的情况,并在组件卸载时自动移除事件监听器,避免了内存泄漏问题。
  • 实现键盘关闭功能 :在 Message.vue 组件中定义 keydown 函数,用于处理键盘按下事件。当按下 Escape 键时,关闭 Message 组件。然后使用 useEventListener 函数为 document 对象添加 keydown 事件监听器,使得在整个文档范围内按下 Escape 键都能关闭 Message 组件,增强了组件的交互性和用户体验。
src/hooks/useEventListener.ts 复制代码
import {  onMounted, onBeforeUnmount, isRef, watch, unref } from 'vue'
import type { Ref } from 'vue'
export default function useEventListener(
  target: Ref<EventTarget | null> | EventTarget,
  event: string,
  handler: (e: Event) => any
) {
  if (isRef(target)) {
    watch(target, (value, oldValue) => {
      oldValue?.removeEventListener(event, handler)
      value?.addEventListener(event, handler)
    })
  } else {
    onMounted(() => {
      target.addEventListener(event, handler)
    })
  }

  onBeforeUnmount(() => {
    unref(target)?.removeEventListener(event, handler)
  })
}
Message.vue 复制代码
import useEventListener from '@/hooks/useEventListener'
function keydown(e: Event) {
  const event = e as KeyboardEvent
  if (event.code === 'Escape') {
    visible.value = false
  }
}
useEventListener(document, 'keydown', keydown)

添加鼠标悬停

  • 交互逻辑增强 :通过在模板中绑定 mouseentermouseleave 事件,使得 Message 组件能够响应鼠标的进入和离开操作。
  • 定时器控制 :利用 startTimerclearTimer 函数对定时器进行控制。当鼠标悬停在 Message 组件上时,会清除定时器,暂停组件的自动关闭;当鼠标离开组件时,会重新启动定时器,在指定的 duration 时间后关闭组件。这种交互方式提高了用户体验,让用户有足够的时间查看 Message 组件的内容,避免了在用户还未查看完时组件就自动关闭的情况。
Message.vue 复制代码
<template>
  <div
    //...
    @mouseenter="clearTimer"
    @mouseleave="startTimer"
  >
   //...
</template>

let timer: any
function startTimer() {
  if (props.duration === 0) return
  timer = setTimeout(() => {
    visible.value = false
  }, props.duration)
}
function clearTimer() {
  clearTimeout(timer)
}

(七)为 Message 组件添加动画和样式

  • 动画方面 :通过 <Transition> 组件和自定义的动画样式,实现了 Message 组件的淡入淡出和移动动画效果。
  • 样式方面 :使用 CSS 自定义属性和 @each 指令,为不同类型的 Message 组件定义了不同的样式,并且对组件的基础样式进行了详细的设置。
  • 逻辑调整 :对组件的生命周期钩子和一些逻辑进行了调整,使得组件的初始化、销毁和高度获取等操作更加合理。通过这些改动,Message 组件在功能和视觉上都得到了进一步的完善。
types.ts 复制代码
export interface MessageProps {
//...
  transitionName?: string;
}
Message.vue 复制代码
<template>
  <Transition :name="transitionName" @after-leave="destroyComponent" @enter="updateHeight">
    //...
  </Transition>
</template>

import { ref, onMounted, computed } from 'vue'
const props = withDefaults(defineProps<MessageProps>(), {
//...
  transitionName:'fade-up'
})

onMounted(async () => {
  visible.value = true
  startTimer()
  // await nextTick()
  // height.value = messageRef.value!.getBoundingClientRect().height
})

useEventListener(document, 'keydown', keydown)
function destroyComponent () {
  props.onDestory()
}
function updateHeight() {
  height.value = messageRef.value!.getBoundingClientRect().height
}
//删掉
// watch(visible, (newValue) => {
//   if (!newValue) {
//     props.onDestory()
//   }
// })
style.css 复制代码
.yl-message {
  --yl-message-bg-color: var(--yl-color-info-light-9);
  --yl-message-border-color: var(--yl-border-color-lighter);
  --yl-message-padding: 15px 19px;
  --yl-message-close-size: 16px;
  --yl-message-close-icon-color: var(--yl-text-color-placeholder);
  --yl-message-close-hover-color: var(--yl-text-color-secondary);
}
.yl-message {
  width: fit-content;
  max-width: calc(100% - 32px);
  box-sizing: border-box;
  border-radius: var(--yl-border-radius-base);
  border-width: var(--yl-border-width);
  border-style: var(--yl-border-style);
  border-color: var(--yl-message-border-color);
  position: fixed;
  left: 50%;
  top: 20px;
  transform: translateX(-50%);
  background-color: var(--yl-message-bg-color);
  padding: var(--yl-message-padding);
  display: flex;
  align-items: center;
  transition: top var(--yl-transition-duration), opacity var(--yl-transition-duration), transform var(--yl-transition-duration);
  .yl-message__content {
    color: var(--yl-message-text-color);
    overflow-wrap: anywhere;
  }
  &.is-close .yl-message__content {
    padding-right: 30px;
  }
  .yl-message__close {
    display: flex;
    align-items: center;
  }
  .yl-message__close svg {
    cursor: pointer;
  }
}
@each $val in info,success,warning,danger {
  .yl-message--$(val) {
    --yl-message-bg-color: var(--yl-color-$(val)-light-9);
    --yl-message-border-color: var(--yl-color-$(val)-light-8);
    --yl-message-text-color: var(--yl-color-$(val));
    .yl-message__close {
      --yl-icon-color: var(--yl-color-$(val));
    }
  }
}
.yl-message.fade-up-enter-from,
.yl-message.fade-up-leave-to {
  opacity: 0;
  transform: translate(-50%, -100%);
}
style/index.css 复制代码
@import '../components/Message/style.css';
相关推荐
Aphasia3113 分钟前
快速上手tailwindcss
前端·css·面试
程序员荒生16 分钟前
基于 Next.js 搞定个人公众号登陆流程
前端·微信·开源
AmyGeng12326 分钟前
el-dropdown全屏模式下不展示下拉菜单处理
javascript·vue.js·ecmascript
deckcode29 分钟前
css基础-选择器
前端·css
倔强青铜三30 分钟前
WXT浏览器开发中文教程(2)----WXT项目目录结构详解
前端·javascript·vue.js
1024小神34 分钟前
html5-qrcode前端打开摄像头扫描二维码功能
前端·html·html5
beibeibeiooo35 分钟前
【Vue3入门2】01-图片轮播示例
前端·vue.js
倔强青铜三35 分钟前
WXT浏览器开发中文教程(1)----安装WXT
前端·javascript·vue.js
计算机-秋大田1 小时前
基于Spring Boot的产业园区智慧公寓管理系统的设计与实现(LW+源码+讲解)
java·vue.js·spring boot·后端·课程设计
2301_815357701 小时前
Spring:IOC
java·前端·spring