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

各位观众老爷们,大家好!今天咱们不开车,聊点正经的——Vue 通知中心的设计与实现。别怕,不是让你搞个 CRM 系统,咱就做一个简单好用的通知中心,让你的应用也能像微信一样,收到各种各样的消息,而且还能离线查看,是不是想想就有点小激动?

第一章:需求分析与技术选型

在撸起袖子写代码之前,咱们先来捋一捋需求,明确目标,才能避免写出屎山代码。

  • 多种通知类型: 比如系统消息、用户消息、活动通知等等,不能千篇一律。
  • 持久化存储: 刷新页面消息还在,不能一刷新就清空,用户会骂娘的。
  • 离线通知: 即使没网,也能看到之前的消息,体现用户关怀。
  • 自定义模板: 不同的通知类型,样式肯定不一样,得支持自定义。

有了需求,咱们再来看看用什么武器来打这场仗。

  • Vue.js: 废话,都说了是 Vue 应用了。
  • Vuex: 管理全局状态,存储通知消息,方便组件访问。
  • LocalStorage/IndexedDB: 用于持久化存储,前者简单粗暴,后者性能更好,可以根据实际情况选择。
  • Service Worker: 实现离线通知,让你的应用即使没网也能耍流氓。
  • Vue Template Compiler: 用于编译自定义模板,让你的通知消息更加个性化。

第二章:数据结构设计

好的数据结构是成功的一半。咱们来定义一下通知消息的数据结构。

const notification = {
  id: 'unique_id', // 唯一 ID,用于区分不同的消息
  type: 'system | user | activity', // 通知类型
  title: '通知标题',
  content: '通知内容',
  timestamp: 1678886400000, // 时间戳,方便排序
  read: false, // 是否已读
  data: { // 附加数据,用于自定义模板
    username: '张三',
    productName: 'iPhone 14 Pro Max'
  }
}

用表格来更清晰地展示:

字段 类型 描述
id String 唯一 ID,可以使用 UUID 生成
type String 通知类型,例如 ‘system’, ‘user’, ‘activity’
title String 通知标题
content String 通知内容
timestamp Number 时间戳,毫秒数
read Boolean 是否已读
data Object 附加数据,用于自定义模板

第三章:Vuex 状态管理

Vuex 是咱们的弹药库,用来存储和管理通知消息。

// store/modules/notification.js
import Vue from 'vue'

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) {
      Vue.set(notification, 'read', true) // 使用 Vue.set 确保响应式更新
    }
  },
  REMOVE_NOTIFICATION (state, id) {
    state.notifications = state.notifications.filter(n => n.id !== id)
  },
  SET_NOTIFICATIONS (state, notifications) {
    state.notifications = notifications
  }
}

const actions = {
  addNotification ({ commit }, notification) {
    commit('ADD_NOTIFICATION', notification)
  },
  markAsRead ({ commit }, id) {
    commit('MARK_AS_READ', id)
  },
  removeNotification ({ commit }, id) {
    commit('REMOVE_NOTIFICATION', id)
  },
  loadNotifications ({ commit }) {
    // 从 LocalStorage/IndexedDB 加载通知消息
    const notifications = JSON.parse(localStorage.getItem('notifications') || '[]') // 这里只是示例,实际情况需要处理加载失败的情况
    commit('SET_NOTIFICATIONS', notifications)
  },
  saveNotifications ({ state }) {
    // 将通知消息保存到 LocalStorage/IndexedDB
    localStorage.setItem('notifications', JSON.stringify(state.notifications)) // 这里只是示例,实际情况需要处理保存失败的情况
  }
}

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

export default {
  namespaced: true, // 开启命名空间,避免命名冲突
  state,
  mutations,
  actions,
  getters
}

别忘了在你的 store/index.js 中引入这个 module:

import Vue from 'vue'
import Vuex from 'vuex'
import notification from './modules/notification'

Vue.use(Vuex)

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

第四章:持久化存储

持久化存储是让消息“永垂不朽”的关键。这里咱们先用 LocalStorage 简单实现一下。

  • actions 中添加 loadNotificationssaveNotifications 方法:
// store/modules/notification.js (接上面的代码)
const actions = {
  addNotification ({ commit, dispatch }, notification) {
    commit('ADD_NOTIFICATION', notification)
    dispatch('saveNotifications') // 添加消息后保存
  },
  markAsRead ({ commit, dispatch }, id) {
    commit('MARK_AS_READ', id)
    dispatch('saveNotifications') // 标记已读后保存
  },
  removeNotification ({ commit, dispatch }, id) {
    commit('REMOVE_NOTIFICATION', id)
    dispatch('saveNotifications') // 移除消息后保存
  },
  loadNotifications ({ commit }) {
    // 从 LocalStorage 加载通知消息
    try {
      const notifications = JSON.parse(localStorage.getItem('notifications') || '[]')
      commit('SET_NOTIFICATIONS', notifications)
    } catch (error) {
      console.error('Failed to load notifications from localStorage:', error)
      // 处理加载失败的情况,例如清空 localStorage 或显示错误信息
      localStorage.removeItem('notifications')
      commit('SET_NOTIFICATIONS', [])
    }
  },
  saveNotifications ({ state }) {
    // 将通知消息保存到 LocalStorage
    try {
      localStorage.setItem('notifications', JSON.stringify(state.notifications))
    } catch (error) {
      console.error('Failed to save notifications to localStorage:', error)
      // 处理保存失败的情况,例如显示错误信息
    }
  }
}
  • 在应用初始化时加载通知消息:

在你的 App.vue 或其他入口文件中,在 mounted 钩子函数中调用 loadNotifications action:

// App.vue
<template>
  <div id="app">
    <router-view/>
  </div>
</template>

<script>
export default {
  mounted () {
    this.$store.dispatch('notification/loadNotifications') // 加载通知消息
  }
}
</script>

第五章:离线通知(Service Worker)

Service Worker 是实现离线功能的利器。但是配置起来稍微有点麻烦,咱们一步一步来。

  1. 创建 service-worker.js 文件:

在你的项目根目录下创建一个 service-worker.js 文件。

// service-worker.js
const CACHE_NAME = 'notification-cache-v1'
const urlsToCache = [
  '/',
  '/index.html',
  '/static/js/app.js', // 替换成你实际的 app.js 文件路径
  '/static/css/app.css' // 替换成你实际的 app.css 文件路径
]

self.addEventListener('install', event => {
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(cache => {
        console.log('Opened cache')
        return cache.addAll(urlsToCache)
      })
  )
})

self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request)
      .then(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(
          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 because we want the browser to consume the response
            // as well as the cache consuming the response, we need
            // to clone it so we have two streams.
            const responseToCache = response.clone()

            caches.open(CACHE_NAME)
              .then(cache => {
                cache.put(event.request, responseToCache)
              })

            return response
          }
        )
      })
  )
})

self.addEventListener('activate', event => {
  const cacheWhitelist = [CACHE_NAME]

  event.waitUntil(
    caches.keys().then(cacheNames => {
      return Promise.all(
        cacheNames.map(cacheName => {
          if (cacheWhitelist.indexOf(cacheName) === -1) {
            return caches.delete(cacheName)
          }
        })
      )
    })
  )
})

self.addEventListener('message', event => {
  if (event.data.type === 'NEW_NOTIFICATION') {
    const notification = event.data.payload
    self.registration.showNotification(notification.title, {
      body: notification.content,
      icon: '/icon.png' // 替换成你的通知图标
    })
  }
})
  1. 注册 Service Worker:

在你的 main.js 或其他入口文件中注册 Service Worker。

// main.js
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)
    })
}
  1. 发送离线通知:

当应用在离线状态下收到新的通知时,可以通过 Service Worker 发送系统通知。

// 在你的 Vue 组件中
methods: {
  sendOfflineNotification (notification) {
    if ('serviceWorker' in navigator && navigator.serviceWorker.controller) {
      navigator.serviceWorker.controller.postMessage({
        type: 'NEW_NOTIFICATION',
        payload: notification
      })
    } else {
      console.warn('Service Worker is not available.')
    }
  }
}

注意:

  • 你需要使用 HTTPS 协议才能使用 Service Worker。
  • 需要在浏览器中启用 Service Worker。
  • 缓存的资源需要根据实际情况进行调整。
  • Service Worker 的更新可能需要一些时间,可以手动清除缓存或强制更新。

第六章:自定义模板

自定义模板是让你的通知消息更加个性化的关键。

  1. 使用 Vue Template Compiler:

Vue Template Compiler 可以将字符串模板编译成渲染函数。

import { compile } from 'vue-template-compiler'

function renderTemplate (template, data) {
  const { render } = compile(template)
  const vm = new Vue({
    data: data,
    render: render
  })
  return vm.$mount().$el.outerHTML
}

// 使用示例
const template = '<div>Hello, {{ username }}! You have a new {{ productName }}!</div>'
const data = {
  username: '李四',
  productName: 'iPad Pro'
}

const renderedHtml = renderTemplate(template, data)
console.log(renderedHtml) // 输出:<div>Hello, 李四! You have a new iPad Pro!</div>
  1. 在 Vue 组件中使用:
<template>
  <div v-html="renderedContent"></div>
</template>

<script>
import { compile } from 'vue-template-compiler'

export default {
  props: {
    notification: {
      type: Object,
      required: true
    }
  },
  computed: {
    renderedContent () {
      const template = this.getTemplateByType(this.notification.type)
      return this.renderTemplate(template, this.notification.data)
    }
  },
  methods: {
    getTemplateByType (type) {
      // 根据通知类型返回不同的模板
      switch (type) {
        case 'system':
          return '<div>System Message: {{ message }}</div>'
        case 'user':
          return '<div>Hello, {{ username }}!</div>'
        case 'activity':
          return '<div>New Activity: {{ activityName }}</div>'
        default:
          return '<div>{{ content }}</div>'
      }
    },
    renderTemplate (template, data) {
      const { render } = compile(template)
      const vm = new Vue({
        data: data,
        render: render
      })
      return vm.$mount().$el.outerHTML
    }
  }
}
</script>

第七章:总结与展望

到这里,一个简单的 Vue 通知中心就完成了。当然,这只是一个基础版本,还有很多可以改进的地方。

  • 更完善的持久化存储: 可以使用 IndexedDB 替代 LocalStorage,提供更好的性能和更大的存储空间。
  • 更强大的 Service Worker: 可以实现更复杂的离线功能,例如预缓存资源、后台数据同步等。
  • 更灵活的自定义模板: 可以使用更强大的模板引擎,例如 Handlebars 或 Mustache,支持更复杂的模板语法。
  • 更友好的用户界面: 可以设计更美观、更易用的用户界面,提升用户体验。

希望今天的分享对你有所帮助。记住,代码只是工具,重要的是思考和解决问题的能力。下次再见!

发表回复

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