设计一个 Vue 应用的通知中心,支持多种通知类型、持久化存储、离线通知和自定义模板。

各位观众老爷,晚上好!今天咱们就来聊聊怎么用 Vue.js 撸一个功能强大的通知中心,保证让你的应用立马高大上起来!

一、通知中心的需求分析

在撸代码之前,咱们先得搞清楚需求。一个优秀的通知中心,至少得满足以下几个要求:

  • 多种通知类型: 比如系统消息、用户互动、订单更新等等。
  • 持久化存储: 刷新页面后,通知还在,用户不会错过重要信息。
  • 离线通知: 用户离线时也能收到通知,下次上线时可以查看。
  • 自定义模板: 可以根据不同的通知类型,定制不同的显示样式。
  • 操作性: 可以标记已读、删除通知等。

二、技术选型

  • Vue.js: 前端框架,负责 UI 展示和交互。
  • Vuex: 状态管理工具,管理通知数据。
  • localStorage/IndexedDB: 浏览器本地存储,用于持久化存储通知数据。
  • Service Worker (可选): 实现离线通知。

三、项目结构搭建

咱们先搭个简单的项目框架:

notify-center/
├── src/
│   ├── components/
│   │   └── NotificationItem.vue  // 单个通知组件
│   ├── store/
│   │   ├── index.js          // Vuex 根模块
│   │   ├── modules/
│   │   │   └── notifications.js  // 通知相关的状态管理
│   ├── App.vue
│   └── main.js
├── public/
│   └── index.html
├── package.json
└── vue.config.js

四、Vuex 状态管理

咱们把通知数据放到 Vuex 里管理,这样方便组件之间共享和修改。

  1. src/store/modules/notifications.js
const state = {
  notifications: []
};

const mutations = {
  ADD_NOTIFICATION(state, notification) {
    state.notifications.unshift(notification); // 新消息放最前面
  },
  MARK_AS_READ(state, id) {
    const notification = state.notifications.find(n => n.id === id);
    if (notification) {
      notification.read = true;
    }
  },
  DELETE_NOTIFICATION(state, id) {
    state.notifications = state.notifications.filter(n => n.id !== id);
  },
  SET_NOTIFICATIONS(state, notifications) {
    state.notifications = notifications;
  }
};

const actions = {
  addNotification({ commit }, notification) {
    notification.id = Date.now(); // 简单生成唯一 ID
    notification.read = false;
    commit('ADD_NOTIFICATION', notification);
    localStorage.setItem('notifications', JSON.stringify(state.notifications)); // 持久化存储
  },
  markAsRead({ commit }, id) {
    commit('MARK_AS_READ', id);
    localStorage.setItem('notifications', JSON.stringify(state.notifications));
  },
  deleteNotification({ commit }, id) {
    commit('DELETE_NOTIFICATION', id);
    localStorage.setItem('notifications', JSON.stringify(state.notifications));
  },
  loadNotifications({ commit }) {
    const notifications = JSON.parse(localStorage.getItem('notifications') || '[]');
    commit('SET_NOTIFICATIONS', notifications);
  }
};

const getters = {
  unreadCount: state => state.notifications.filter(n => !n.read).length,
  allNotifications: state => state.notifications
};

export default {
  namespaced: true,
  state,
  mutations,
  actions,
  getters
};
  1. src/store/index.js
import Vue from 'vue';
import Vuex from 'vuex';
import notifications from './modules/notifications';

Vue.use(Vuex);

export default new Vuex.Store({
  modules: {
    notifications
  }
});

五、通知组件 (NotificationItem.vue)

这个组件负责展示单个通知。

<template>
  <div class="notification-item" :class="{ 'read': notification.read }">
    <div class="notification-content">
      {{ notification.message }}
      <div v-if="notification.type === 'order'">
        订单号: {{ notification.orderId }}
      </div>
    </div>
    <div class="notification-actions">
      <button @click="markAsRead(notification.id)" v-if="!notification.read">
        标记为已读
      </button>
      <button @click="deleteNotification(notification.id)">删除</button>
    </div>
  </div>
</template>

<script>
export default {
  props: {
    notification: {
      type: Object,
      required: true
    }
  },
  methods: {
    markAsRead(id) {
      this.$store.dispatch('notifications/markAsRead', id);
    },
    deleteNotification(id) {
      this.$store.dispatch('notifications/deleteNotification', id);
    }
  }
};
</script>

<style scoped>
.notification-item {
  border: 1px solid #ccc;
  padding: 10px;
  margin-bottom: 5px;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.notification-item.read {
  background-color: #f0f0f0;
}
</style>

六、主组件 (App.vue)

主组件负责展示通知列表,并提供添加通知的功能。

<template>
  <div id="app">
    <h1>通知中心</h1>
    <p>未读消息数: {{ unreadCount }}</p>
    <input type="text" v-model="newMessage" placeholder="输入消息内容">
    <select v-model="notificationType">
      <option value="system">系统消息</option>
      <option value="user">用户互动</option>
      <option value="order">订单更新</option>
    </select>
    <input type="text" v-if="notificationType === 'order'" v-model="orderId" placeholder="订单号">
    <button @click="addNotification">添加通知</button>

    <div v-if="allNotifications.length === 0">
      暂无通知
    </div>
    <div v-else>
      <NotificationItem
        v-for="notification in allNotifications"
        :key="notification.id"
        :notification="notification"
      />
    </div>
  </div>
</template>

<script>
import NotificationItem from './components/NotificationItem.vue';

export default {
  components: {
    NotificationItem
  },
  data() {
    return {
      newMessage: '',
      notificationType: 'system',
      orderId: ''
    };
  },
  computed: {
    unreadCount() {
      return this.$store.getters['notifications/unreadCount'];
    },
    allNotifications() {
      return this.$store.getters['notifications/allNotifications'];
    }
  },
  methods: {
    addNotification() {
      const notification = {
        message: this.newMessage,
        type: this.notificationType,
        orderId: this.orderId
      };
      this.$store.dispatch('notifications/addNotification', notification);
      this.newMessage = '';
      this.orderId = '';
    }
  },
  mounted() {
    this.$store.dispatch('notifications/loadNotifications');
  }
};
</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;
}
</style>

七、main.js

import Vue from 'vue'
import App from './App.vue'
import store from './store'

Vue.config.productionTip = false

new Vue({
  store,
  render: h => h(App)
}).$mount('#app')

八、代码讲解

  • Vuex: 使用 Vuex 管理通知数据,方便组件之间共享和修改。notifications.js 模块定义了 state (通知数据),mutations (修改 state 的方法),actions (提交 mutations 的方法) 和 getters (获取 state 的计算属性)。
  • localStorage: actions 中的 addNotificationmarkAsReaddeleteNotification 方法,在修改了 Vuex 的 state 之后,都会将 state.notifications 序列化成 JSON 字符串,然后存储到 localStorage 中。actions 中的 loadNotifications 方法,会在应用加载时,从 localStorage 中读取通知数据,然后更新 Vuex 的 state。
  • 通知组件 (NotificationItem.vue): 接收一个 notification 对象作为 prop,然后根据 notification 的属性,展示不同的内容。
  • 主组件 (App.vue): 展示通知列表,并提供添加通知的功能。使用 v-if 指令,根据 notificationType 的值,展示不同的输入框。使用 this.$store.dispatch 方法,调用 Vuex 的 actions,添加、标记已读、删除通知。使用 this.$store.getters 方法,获取 Vuex 的 getters,展示未读消息数和所有通知。

九、进阶功能

  1. IndexedDB 替代 localStorage: localStorage 的存储空间有限,而且是同步 API,可能会阻塞 UI 线程。IndexedDB 存储空间更大,而且是异步 API,更适合存储大量的通知数据。

    // 简单示例,需要引入 indexedDB 封装库,比如 localForage
    import localForage from 'localforage';
    
    const actions = {
      async addNotification({ commit }, notification) {
        notification.id = Date.now();
        notification.read = false;
        commit('ADD_NOTIFICATION', notification);
        await localForage.setItem('notifications', state.notifications);
      },
      async loadNotifications({ commit }) {
        const notifications = await localForage.getItem('notifications') || [];
        commit('SET_NOTIFICATIONS', notifications);
      }
    };
  2. Service Worker 实现离线通知: Service Worker 是一个在浏览器后台运行的脚本,可以拦截网络请求,并提供离线缓存功能。可以用它来接收推送通知,并在用户离线时,将通知存储到 IndexedDB 中。当用户上线时,再将通知展示出来。

    • 注册 Service Worker:main.js 中注册 Service Worker。

      if ('serviceWorker' in navigator) {
        navigator.serviceWorker.register('/service-worker.js')
          .then(registration => {
            console.log('Service Worker registered with scope:', registration.scope);
          })
          .catch(error => {
            console.error('Service Worker registration failed:', error);
          });
      }
    • 创建 public/service-worker.js: 这个文件包含 Service Worker 的逻辑。

      self.addEventListener('push', function(event) {
        const notificationData = event.data.json();
        const title = notificationData.title || '新消息';
        const options = {
          body: notificationData.body || '您有一条新消息',
          icon: notificationData.icon || '/icon.png' // optional
        };
      
        event.waitUntil(self.registration.showNotification(title, options));
      });
      
      self.addEventListener('notificationclick', function(event) {
        event.notification.close();
      
        // Open the app or navigate to a specific URL
        event.waitUntil(
          clients.openWindow('/') // Replace with your app's URL
        );
      });
      
      // 离线缓存策略 (简单示例)
      const CACHE_NAME = 'my-site-cache-v1';
      const urlsToCache = [
        '/',
        '/css/app.css',
        '/js/app.js',
        '/icon.png' // Optional
      ];
      
      self.addEventListener('install', function(event) {
        // Perform install steps
        event.waitUntil(
          caches.open(CACHE_NAME)
            .then(function(cache) {
              console.log('Opened cache');
              return cache.addAll(urlsToCache);
            })
        );
      });
      
      self.addEventListener('fetch', function(event) {
        event.respondWith(
          caches.match(event.request)
            .then(function(response) {
              // Cache hit - return response
              if (response) {
                return response;
              }
      
              // IMPORTANT: Clone the request. A request is a stream and
              // can only be consumed once. Since we are consuming this
              // once by cache and once by the browser for fetch, we need
              // to clone the response.
              const fetchRequest = event.request.clone();
      
              return fetch(fetchRequest).then(
                function(response) {
                  // Check if we received a valid response
                  if(!response || response.status !== 200 || response.type !== 'basic') {
                    return response;
                  }
      
                  // IMPORTANT: Clone the response. A response is a stream
                  // and can only be consumed once. Since we are consuming this
                  // once by cache and once by the browser for fetch, we need
                  // to clone the response.
                  const responseToCache = response.clone();
      
                  caches.open(CACHE_NAME)
                    .then(function(cache) {
                      cache.put(event.request, responseToCache);
                    });
      
                  return response;
                }
              );
            })
          );
      });
    • 后端推送通知: 需要一个后端服务,使用 Web Push 协议,向 Service Worker 推送通知。

  3. 自定义模板: 可以使用 Vue 的 component 标签,根据 notification.type 动态渲染不同的组件。

    // App.vue
    <template>
      <div>
        <component
          :is="getNotificationComponent(notification.type)"
          :notification="notification"
        />
      </div>
    </template>
    
    <script>
    import SystemNotification from './components/SystemNotification.vue';
    import OrderNotification from './components/OrderNotification.vue';
    
    export default {
      components: {
        SystemNotification,
        OrderNotification
      },
      methods: {
        getNotificationComponent(type) {
          switch (type) {
            case 'system':
              return 'SystemNotification';
            case 'order':
              return 'OrderNotification';
            default:
              return 'NotificationItem'; // 默认组件
          }
        }
      }
    };
    </script>
  4. 更完善的 ID 生成策略: Date.now() 生成的 ID 在高并发情况下可能会重复,可以使用 UUID 或者更复杂的算法生成唯一 ID。

十、总结

咱们今天撸了一个功能比较完善的 Vue 通知中心,包括了多种通知类型、持久化存储、以及自定义模板。还简单介绍了如何使用 Service Worker 实现离线通知,以及使用 IndexedDB 存储大量通知数据。当然,这只是一个基础版本,还有很多可以优化和扩展的地方,比如:

  • 更好的 UI 交互: 添加动画效果,优化通知的显示和隐藏。
  • 更完善的错误处理: 处理 localStorage/IndexedDB 读写错误。
  • 后端集成: 与后端服务集成,实现实时通知。

希望今天的讲座能对你有所帮助! 各位,下课!

发表回复

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