如何在 Vue 中实现一个消息通知中心,支持多种通知类型(成功、失败、警告)、自动关闭和手动关闭?

Vue 消息通知中心:让你的应用会“说话”

各位观众老爷,大家好!我是今天的主讲人,江湖人称“代码老司机”。今天咱们来聊聊一个在 Vue 项目中非常实用的小玩意儿:消息通知中心。

想象一下,你的用户辛辛苦苦填完表单,提交后啥反应都没有,一脸懵逼,不知道是成功了还是失败了。这体验,你说糟不糟心?一个好的消息通知中心,就像应用的“嘴巴”,能及时地告诉用户发生了什么,让用户心里踏实。

今天我们就手把手地打造一个功能强大的消息通知中心,支持多种通知类型,自动关闭和手动关闭,让你的应用瞬间“活”起来。

需求分析:我们要做什么?

首先,我们要明确目标,一个优秀的消息通知中心应该具备以下特性:

  • 多类型支持: 至少支持成功、失败、警告等常见类型,并能自定义样式。
  • 自动关闭: 通知显示一段时间后自动消失,无需用户手动干预。
  • 手动关闭: 用户可以点击按钮手动关闭通知。
  • 队列管理: 多个通知出现时,按照顺序排队显示,避免信息拥堵。
  • 全局可用: 在应用的任何地方都能方便地调用。
  • 组件化: 方便复用和维护。

技术选型:用什么来“武装”?

  • Vue.js: 这是毋庸置疑的,我们的主角。
  • Vue Composition API: 让我们用更优雅的方式组织代码(当然,你也可以用 Options API)。
  • CSS Transitions/Animations: 实现平滑的动画效果。

架构设计:如何搭建“舞台”?

我们将采用以下架构:

  1. Notification 组件: 负责显示单个通知消息。
  2. NotificationService (useNotification): 一个 Composition API hook,负责管理通知队列,添加、删除通知,并提供全局访问接口。
  3. App.vue (或其他根组件): 渲染 Notification 组件,并注入 NotificationService。

代码实现:撸起袖子就是干!

1. Notification 组件 (components/Notification.vue)

<template>
  <div
    class="notification"
    :class="typeClass"
    :style="{ top: topPosition + 'px' }"
    @mouseenter="pauseTimer"
    @mouseleave="resumeTimer"
  >
    <div class="notification-content">
      {{ message }}
    </div>
    <button class="notification-close" @click="closeNotification">
      X
    </button>
  </div>
</template>

<script>
import { defineComponent, ref, onMounted, onUnmounted } from 'vue';

export default defineComponent({
  props: {
    id: {
      type: Number,
      required: true,
    },
    message: {
      type: String,
      required: true,
    },
    type: {
      type: String,
      default: 'info',
    },
    duration: {
      type: Number,
      default: 3000,
    },
    onClose: {
      type: Function,
      required: true,
    },
    topPosition: {
      type: Number,
      required: true,
    }
  },
  setup(props) {
    let timer = null;
    const isPaused = ref(false);

    const startTimer = () => {
      timer = setTimeout(() => {
        props.onClose(props.id);
      }, props.duration);
    };

    const pauseTimer = () => {
      isPaused.value = true;
      clearTimeout(timer);
    };

    const resumeTimer = () => {
      isPaused.value = false;
      startTimer();
    };

    const closeNotification = () => {
      clearTimeout(timer);
      props.onClose(props.id);
    };

    onMounted(() => {
      startTimer();
    });

    onUnmounted(() => {
      clearTimeout(timer); //组件卸载时清除定时器,防止内存泄漏
    });

    return {
      closeNotification,
      typeClass: `notification--${props.type}`,
      pauseTimer,
      resumeTimer
    };
  },
});
</script>

<style scoped>
.notification {
  position: fixed;
  right: 20px;
  background-color: #fff;
  border: 1px solid #ccc;
  padding: 10px 20px;
  margin-bottom: 10px;
  border-radius: 4px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  display: flex;
  align-items: center;
  z-index: 9999; /* 确保在最上层 */
  transition: all 0.3s ease;
}

.notification-content {
  flex-grow: 1;
  margin-right: 10px;
}

.notification-close {
  background-color: transparent;
  border: none;
  cursor: pointer;
  font-size: 16px;
}

.notification--success {
  border-color: #28a745;
  color: #28a745;
}

.notification--error {
  border-color: #dc3545;
  color: #dc3545;
}

.notification--warning {
  border-color: #ffc107;
  color: #ffc107;
}

.notification--info {
  border-color: #17a2b8;
  color: #17a2b8;
}
</style>
  • Props: 接收 id (唯一标识), message (消息内容), type (消息类型), duration (自动关闭时间), onClose (关闭回调函数),topPosition(顶部位置)等参数。
  • typeClass: 根据 type prop 生成不同的 CSS 类名,方便应用不同的样式。
  • closeNotification: 手动关闭通知的函数,调用父组件传递的 onClose 回调函数。
  • startTimerpauseTimerresumeTimer: 实现自动关闭功能,并在鼠标悬停时暂停计时器。
  • onMountedonUnmounted: 组件挂载和卸载时启动和清理定时器,防止内存泄漏。

2. NotificationService (useNotification.js)

import { reactive, readonly, ref, computed } from 'vue';

let notificationId = 0; // 用于生成唯一ID

const state = reactive({
  notifications: [],
});

const notificationHeight = 60; // 每个通知的高度,包括margin
const topOffset = 20; // 顶部偏移量

export function useNotification() {
  const addNotification = (message, type = 'info', duration = 3000) => {
    const id = notificationId++;
    state.notifications.push({
      id,
      message,
      type,
      duration,
    });

    return id; //返回id方便后续操作
  };

  const removeNotification = (id) => {
    state.notifications = state.notifications.filter(
      (notification) => notification.id !== id
    );
  };

  const success = (message, duration) => {
    return addNotification(message, 'success', duration);
  };

  const error = (message, duration) => {
    return addNotification(message, 'error', duration);
  };

  const warning = (message, duration) => {
    return addNotification(message, 'warning', duration);
  };

  const info = (message, duration) => {
    return addNotification(message, 'info', duration);
  };

  const notificationPositions = computed(() => {
    const positions = {};
    state.notifications.forEach((notification, index) => {
      positions[notification.id] = topOffset + index * notificationHeight;
    });
    return positions;
  });

  return {
    notifications: readonly(state.notifications),
    addNotification,
    removeNotification,
    success,
    error,
    warning,
    info,
    notificationPositions
  };
}
  • state.notifications: 一个响应式数组,用于存储所有待显示的通知对象。
  • addNotification: 向队列中添加新的通知,生成唯一的 id
  • removeNotification: 从队列中移除指定 id 的通知。
  • success, error, warning, info: 快捷方法,用于添加不同类型的通知。
  • notificationPositions: 计算属性,根据通知在队列中的位置,动态计算每个通知的 top 值,避免重叠。使用computed可以确保当notifications数组发生变化时,positions会自动更新。
  • readonly(state.notifications):notifications 数组设置为只读,防止外部组件直接修改状态,确保数据的一致性。

3. App.vue (或其他根组件)

<template>
  <div id="app">
    <button @click="showSuccess">显示成功消息</button>
    <button @click="showError">显示错误消息</button>
    <button @click="showWarning">显示警告消息</button>
    <button @click="showInfo">显示普通消息</button>

    <Notification
      v-for="notification in notifications"
      :key="notification.id"
      :id="notification.id"
      :message="notification.message"
      :type="notification.type"
      :duration="notification.duration"
      :onClose="removeNotification"
      :topPosition="notificationPositions[notification.id]"
    />
  </div>
</template>

<script>
import { defineComponent } from 'vue';
import Notification from './components/Notification.vue';
import { useNotification } from './useNotification';

export default defineComponent({
  components: {
    Notification,
  },
  setup() {
    const {
      notifications,
      success,
      error,
      warning,
      info,
      removeNotification,
      notificationPositions
    } = useNotification();

    const showSuccess = () => {
      success('操作成功!');
    };

    const showError = () => {
      error('发生错误!');
    };

    const showWarning = () => {
      warning('请注意!');
    };

    const showInfo = () => {
      info('这是一条普通消息。');
    };

    return {
      notifications,
      showSuccess,
      showError,
      showWarning,
      showInfo,
      removeNotification,
      notificationPositions
    };
  },
});
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}

button {
  margin: 0 10px;
  padding: 10px 20px;
  border: 1px solid #ccc;
  background-color: #fff;
  cursor: pointer;
}
</style>
  • 注入 NotificationService:setup 函数中调用 useNotification,获取通知服务提供的各种方法和状态。
  • 渲染 Notification 组件: 使用 v-for 遍历 notifications 数组,动态渲染 Notification 组件,并将必要的数据传递给子组件。
  • showSuccess 等方法: 点击按钮时,调用相应的通知方法,触发通知的显示。

代码解释:知其然,更要知其所以然

  • 为什么使用 Composition API? Composition API 能够更好地组织和复用逻辑代码,特别是对于像通知服务这种需要全局共享的状态和方法,使用 Composition API 可以更加清晰地划分职责,提高代码的可维护性。
  • 为什么使用 reactivereadonly reactive 用于创建响应式对象,当对象内部的属性发生变化时,Vue 会自动更新相关的视图。readonly 则用于创建只读对象,防止外部组件直接修改状态,确保数据的一致性。
  • 为什么使用 computed 计算通知的位置? 使用 computed 可以根据通知队列的长度动态计算每个通知的 top 值,避免通知重叠。当 notifications 数组发生变化时,computed 会自动更新,确保通知的位置始终正确。
  • 为什么要在 Notification 组件的 onUnmounted 钩子中清除定时器? 如果不清除定时器,当组件被销毁时,定时器仍然会继续运行,导致内存泄漏。

进阶:让通知中心更上一层楼

  • 自定义样式: 通过 CSS Modules 或 scoped CSS,为不同类型的通知添加更丰富的样式,使其更符合应用的整体风格。
  • 支持 HTML 内容: 允许通知消息中包含 HTML 代码,提供更灵活的内容展示方式。
  • 全局配置: 提供全局配置选项,例如默认的自动关闭时间、通知位置等,方便统一管理。
  • 过渡动画: 使用 Vue 的 Transition 组件,为通知的显示和隐藏添加平滑的过渡动画,提升用户体验。
  • 持久化存储: 将通知消息存储到本地,即使刷新页面,也能恢复之前的通知状态。

总结:让你的应用“妙语连珠”

通过以上步骤,我们成功地构建了一个功能完善的 Vue 消息通知中心。它不仅能够显示各种类型的通知消息,还支持自动关闭和手动关闭,并且易于扩展和定制。

有了它,你的应用就能像一个健谈的朋友,随时向用户传递重要的信息,让用户体验更上一层楼。

最后,希望今天的讲座能对你有所帮助。记住,代码的世界是充满乐趣的,只要你肯动手实践,就能创造出无限可能。下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注