Vue + Vant H5 应用中实现 SSE 实时通知及系统通知功能

引言

在现代Web应用中,实时通知功能对于提升用户体验至关重要。无论是即时消息、订单状态更新,还是系统公告,能够及时将信息推送给用户,都能显著增强应用的互动性和实用性。本文将详细介绍如何在基于 Vue 和 Vant UI 的 H5 应用中,利用 Server-Sent Events (SSE) 技术实现实时通知,并结合手机系统通知功能,确保用户即使在应用未激活时也能收到重要提醒。

Server-Sent Events (SSE) 是一种轻量级的单向通信技术,允许服务器向客户端推送事件流。相比于 WebSocket 的双向全双工通信,SSE 更专注于服务器到客户端的单向数据流,这使得它在实现实时通知、股票行情、新闻更新等场景时更为简洁高效。本文将从客户端和服务器端两个方面,逐步讲解 SSE 的实现细节,并探讨如何通过 PWA (Progressive Web App) 和 Service Worker 来优化移动端通知体验,解决应用在后台运行时通知的显示问题。无论您是前端开发者还是对实时通信技术感兴趣,本文都将为您提供一份全面的实践指南。

一、SSE 客户端实现

在 Vue + Vant 的 H5 应用中,实现 SSE 客户端主要包括创建 SSE 连接工具类和在 Vue 组件中集成使用这两个部分。

1.1 创建 SSE 连接工具类 utils/sse.js

为了更好地管理 SSE 连接的生命周期和通知逻辑,我们首先创建一个独立的 JavaScript 工具类 SSENotification。这个类将封装 SSE 连接的建立、消息接收、错误处理以及系统通知的显示逻辑。

javascript 复制代码
// utils/sse.js
class SSENotification {
  constructor() {
    this.eventSource = null; // EventSource 实例
    this.subscribed = false; // 订阅状态标记
  }

  /**
   * 订阅通知
   * @param {string} userId - 用户ID,用于标识当前连接的用户
   * @param {function} onMessageCallback - 接收到消息时的回调函数
   * @param {function} onErrorCallback - 连接发生错误时的回调函数
   */
  subscribe(userId, onMessageCallback, onErrorCallback) {
    // 如果已经订阅,则直接返回,避免重复连接
    if (this.subscribed) return;
    
    // 创建 EventSource 实例,连接到服务器的 SSE 接口
    // userId 作为查询参数传递,以便服务器识别用户
    this.eventSource = new EventSource(`/api/notifications/subscribe?userId=${userId}`);
    
    // 监听 'message' 事件,当服务器推送新消息时触发
    this.eventSource.onmessage = (event) => {
      // 解析服务器推送的 JSON 数据
      const notification = JSON.parse(event.data);
      // 调用传入的回调函数处理通知数据
      onMessageCallback(notification);
      
      // 显示系统通知
      this.showSystemNotification(notification);
    };
    
    // 监听 'error' 事件,当连接发生错误时触发
    this.eventSource.onerror = (error) => {
      // 调用传入的错误回调函数
      onErrorCallback(error);
      // 发生错误时取消订阅,关闭连接
      this.unsubscribe();
    };
    
    // 设置订阅状态为 true
    this.subscribed = true;
  }

  /**
   * 取消订阅,关闭 SSE 连接
   */
  unsubscribe() {
    if (this.eventSource) {
      this.eventSource.close(); // 关闭 EventSource 连接
    }
    this.subscribed = false; // 重置订阅状态
  }

  /**
   * 显示系统通知
   * @param {object} notification - 通知数据对象,包含 title, content, icon 等
   */
  showSystemNotification(notification) {
    // 检查当前浏览器是否支持 Notification API
    if (!("Notification" in window)) {
      console.log("当前浏览器不支持系统通知");
      return;
    }
    
    // 检查通知权限
    if (Notification.permission === "granted") {
      // 如果权限已授予,直接创建通知
      this.createNotification(notification);
    } else if (Notification.permission !== "denied") {
      // 如果权限不是"拒绝",则请求用户授权
      Notification.requestPermission().then(permission => {
        if (permission === "granted") {
          // 用户授予权限后,创建通知
          this.createNotification(notification);
        }
      });
    }
  }

  /**
   * 创建并显示一个系统通知
   * @param {object} notification - 通知数据对象
   */
  createNotification(notification) {
    const notif = new Notification(notification.title || "新通知", { // 通知标题
      body: notification.content, // 通知内容
      icon: notification.icon || "/path/to/default-icon.png" // 通知图标
    });
    
    // 监听通知点击事件
    notif.onclick = () => {
      // 点击通知时将浏览器焦点切换到当前窗口
      window.focus();
      // 可以在这里添加路由跳转逻辑,例如:
      // router.push(notification.url || '/');
    };
  }
}

// 导出 SSENotification 的单例,确保全局只有一个 SSE 连接实例
export default new SSENotification();

代码说明:

  • constructor() :初始化 eventSourcenullsubscribed 标记连接状态。
  • subscribe(userId, onMessageCallback, onErrorCallback)
    • 接收 userId 用于服务器识别用户,以及两个回调函数 onMessageCallbackonErrorCallback 分别处理接收到的消息和连接错误。
    • 通过 new EventSource() 建立与服务器 SSE 接口的连接。URL 中包含 userId 参数。
    • eventSource.onmessage:当服务器推送数据时触发。event.data 包含服务器发送的原始数据,通常是 JSON 字符串,需要 JSON.parse() 解析。解析后,调用 onMessageCallback 处理业务逻辑,并调用 showSystemNotification 显示系统通知。
    • eventSource.onerror:当连接发生错误时触发。调用 onErrorCallback 处理错误,并调用 unsubscribe() 关闭连接。
    • subscribed 标记用于防止重复订阅。
  • unsubscribe() :关闭 EventSource 连接,并重置 subscribed 状态。
  • showSystemNotification(notification)
    • 检查浏览器是否支持 Notification API ("Notification" in window)。
    • 检查当前通知权限 (Notification.permission)。如果已授权 (granted),则直接创建通知;如果未授权且不是"拒绝"状态,则通过 Notification.requestPermission() 请求用户授权。
  • createNotification(notification)
    • 使用 new Notification() 创建并显示一个系统通知。可以设置通知的标题 (title)、内容 (body) 和图标 (icon)。
    • notif.onclick:监听通知的点击事件。通常在这里实现点击通知后跳转到应用内相关页面的逻辑。
  • export default new SSENotification() :导出 SSENotification 的一个单例,确保在整个应用中只有一个 SSE 连接实例,避免资源浪费和逻辑混乱。

1.2 在 Vue 组件中使用

在 Vue 组件中集成 SSENotification 工具类非常简单。您可以在需要接收通知的组件中引入并调用 subscribe 方法,并在组件销毁时调用 unsubscribe 方法,以确保连接的正确管理。

vue 复制代码
<template>
  <div>
    <van-button 
      type="primary" 
      @click="subscribeNotifications"
      :disabled="isSubscribed"
    >
      {{ isSubscribed ? '已订阅' : '订阅通知' }}
    </van-button>
    
    <van-notice-bar
      v-if="latestNotification"
      :text="latestNotification.content"
      left-icon="volume-o"
    />
  </div>
</template>

<script>
import SSENotification from '@/utils/sse'; // 引入 SSE 工具类
import { Toast } from 'vant'; // 引入 Vant 的 Toast 组件用于提示

export default {
  data() {
    return {
      isSubscribed: false, // 订阅状态
      latestNotification: null // 最新通知数据
    };
  },
  
  methods: {
    subscribeNotifications() {
      // 获取当前用户ID。在实际应用中,这通常从 Vuex 或其他状态管理中获取
      const userId = this.$store.state.user.id || 'anonymous'; 
      
      // 调用 SSENotification 的 subscribe 方法订阅通知
      SSENotification.subscribe(
        userId,
        (notification) => {
          // 收到新通知时的回调
          this.latestNotification = notification; // 更新最新通知数据
          Toast('收到新通知'); // 使用 Vant Toast 提示用户
        },
        (error) => {
          // 连接失败时的回调
          Toast.fail('通知连接失败'); // 提示连接失败
          console.error('SSE Error:', error); // 打印错误信息
        }
      );
      
      this.isSubscribed = true; // 更新订阅状态
    }
  },
  
  beforeDestroy() {
    // 在组件销毁前,取消订阅,关闭 SSE 连接,释放资源
    SSENotification.unsubscribe();
  }
};
</script>

<style scoped>
/* 样式可以根据您的项目需求添加 */
</style>

代码说明:

  • data() :定义了 isSubscribed 来控制按钮状态,以及 latestNotification 来显示最新收到的通知。
  • subscribeNotifications() 方法
    • 获取 userId。在实际项目中,userId 应该从您的用户认证系统中获取,例如 Vuex store 或其他全局状态管理。
    • 调用 SSENotification.subscribe() 方法,传入 userId 和两个回调函数。第一个回调用于处理接收到的通知数据,更新组件状态并使用 Vant 的 Toast 进行提示。第二个回调用于处理连接错误。
    • 设置 isSubscribedtrue,禁用"订阅通知"按钮,防止重复点击。
  • beforeDestroy() 生命周期钩子
    • 在 Vue 组件销毁之前,调用 SSENotification.unsubscribe() 方法,确保 SSE 连接被正确关闭,避免内存泄漏和不必要的服务器资源占用。这是管理 SSE 连接生命周期的关键一步。

通过以上步骤,您的 Vue + Vant H5 应用就具备了接收 SSE 实时通知的能力,并且能够将这些通知以系统通知的形式展示给用户。

二、服务端实现 (Node.js 示例)

SSE 的核心在于服务器端如何维护与客户端的连接,并向其推送事件流。本节将以 Node.js 和 Express 框架为例,展示如何构建一个支持 SSE 的通知服务。

javascript 复制代码
// server.js
const express = require("express");
const app = express();

// 存储所有客户端连接的 Map,键为 userId,值为 Express 的 response 对象
const clients = new Map();

// SSE 路由:用于客户端订阅通知
app.get("/api/notifications/subscribe", (req, res) => {
  const userId = req.query.userId; // 从查询参数中获取用户ID
  
  // 设置 SSE 所需的响应头
  res.writeHead(200, {
    "Content-Type": "text/event-stream", // 声明响应内容为事件流
    "Cache-Control": "no-cache", // 禁用缓存
    "Connection": "keep-alive" // 保持连接活跃
  });
  
  // 存储客户端的 response 对象,以便后续向其推送数据
  clients.set(userId, res);
  
  // 发送初始消息,确认连接已建立
  // SSE 消息格式为 "data: [your data]\n\n"
  res.write(`data: ${JSON.stringify({content: "连接已建立"})}\n\n`);
  
  // 监听客户端断开连接事件,进行清理工作
  req.on("close", () => {
    console.log(`客户端 ${userId} 已断开连接`);
    clients.delete(userId); // 从 Map 中移除断开的客户端
  });
});

// 发送通知的 API:用于向特定用户发送通知
app.post("/api/notifications/send", express.json(), (req, res) => {
  const { userId, title, content, icon } = req.body; // 从请求体中获取通知数据
  
  // 检查目标用户是否已订阅且在线
  if (userId && clients.has(userId)) {
    const clientRes = clients.get(userId); // 获取对应用户的 response 对象
    const notification = { title, content, icon, timestamp: new Date() };
    
    // 向特定客户端推送通知数据
    clientRes.write(`data: ${JSON.stringify(notification)}\n\n`);
    res.status(200).send("通知已发送");
  } else {
    res.status(404).send("用户未订阅或不存在");
  }
});

// 广播通知的 API:用于向所有在线用户发送通知
app.post("/api/notifications/broadcast", express.json(), (req, res) => {
  const { title, content, icon } = req.body;
  const notification = { title, content, icon, timestamp: new Date() };
  
  // 遍历所有在线客户端,向其推送通知数据
  clients.forEach(clientRes => {
    clientRes.write(`data: ${JSON.stringify(notification)}\n\n`);
  });
  
  res.status(200).send("广播通知已发送");
});

// 启动服务器,监听指定端口
app.listen(3000, () => {
  console.log("Server running on port 3000");
});

代码说明:

  • clients Map :这是一个 Map 对象,用于存储所有当前连接的客户端。键是 userId,值是对应的 Express response 对象。通过 response 对象,服务器可以向特定的客户端发送数据。
  • /api/notifications/subscribe 路由 (GET)
    • 这是 SSE 的核心接口。当客户端通过 EventSource 连接时,会请求这个 URL。
    • res.writeHead(200, { ... }):设置响应头是关键一步。Content-Type: text/event-stream 告诉浏览器这是一个事件流;Cache-Control: no-cache 禁用缓存;Connection: keep-alive 保持连接活跃。
    • clients.set(userId, res):将当前客户端的 response 对象存储起来,以便后续可以向其推送数据。
    • res.write(data: ${JSON.stringify({content: '连接已建立'})}\n\n):向客户端发送一条初始消息,确认连接成功。SSE 消息必须以 data: 开头,并以 \n\n 结尾。
    • req.on('close', ...):监听 request 对象的 close 事件。当客户端断开连接时(例如关闭浏览器标签页或网络中断),会触发此事件。在此回调中,我们从 clients Map 中删除对应的 response 对象,进行资源清理。
  • /api/notifications/send 路由 (POST)
    • 这是一个用于向特定用户发送通知的 API。通常由后端其他服务或管理员界面调用。
    • 它接收 userIdtitlecontenticon 等参数。
    • 通过 clients.has(userId) 检查目标用户是否在线,如果在线,则通过 clients.get(userId).write() 向其推送通知数据。
  • /api/notifications/broadcast 路由 (POST)
    • 这是一个用于向所有在线用户广播通知的 API。
    • 它遍历 clients Map 中的所有 response 对象,向每个在线客户端推送相同的通知数据。

通过上述服务端实现,您就拥有了一个能够接收客户端订阅、向特定用户发送通知以及向所有在线用户广播通知的 SSE 服务。在实际生产环境中,您可能需要考虑更复杂的鉴权、负载均衡和持久化存储等问题。

三、移动端适配和优化

为了让 H5 应用在移动设备上提供更接近原生应用的通知体验,特别是当应用在后台运行时也能接收和显示通知,我们可以结合 Progressive Web App (PWA) 和 Service Worker 技术进行优化。

3.1 添加到主屏幕 (PWA) 支持

PWA 允许用户将您的 H5 应用"安装"到其设备的主屏幕上,使其看起来和行为都更像一个原生应用。这通常通过一个 manifest.json 文件来实现,该文件定义了应用的元数据,如名称、图标、启动 URL 和显示模式等。

在您的 public 目录下创建 manifest.json 文件:

json 复制代码
// public/manifest.json
{
  "name": "我的应用", // 应用的全名
  "short_name": "应用", // 应用的短名称,显示在主屏幕图标下方
  "start_url": "/", // 应用启动时的URL
  "display": "standalone", // 显示模式:standalone 使应用看起来像一个独立应用,隐藏浏览器UI
  "background_color": "#ffffff", // 应用启动画面背景色
  "theme_color": "#1989fa", // 应用的主题色,影响浏览器地址栏颜色等
  "icons": [ // 应用图标列表
    {
      "src": "/icons/icon-192x192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-512x512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ]
}

注意:

  • 请确保 icons 中指定的图标文件路径正确,并且图标尺寸符合要求。

  • 在您的 index.html 文件中引用 manifest.json

    html 复制代码
    <link rel="manifest" href="/manifest.json">

3.2 Service Worker 注册与通知处理

Service Worker 是一个在浏览器后台运行的脚本,独立于网页生命周期。它可以拦截网络请求、缓存资源,并且最重要的是,它能够处理推送通知,即使应用没有打开。当应用在后台或完全关闭时,系统通知的显示主要依赖于 Service Worker。

3.2.1 public/sw.js 文件

在您的 public 目录下创建 sw.js 文件,用于处理推送通知和通知点击事件:

javascript 复制代码
// public/sw.js

// 监听 push 事件,当服务器推送消息时触发
self.addEventListener("push", event => {
  // 解析推送数据
  const data = event.data.json();
  
  // 定义通知的选项
  const options = {
    body: data.body, // 通知内容
    icon: data.icon || "/icons/icon-192x192.png", // 通知图标
    badge: "/icons/badge.png" // 徽章图标,显示在通知旁边(部分系统支持)
  };
  
  // 显示通知
  event.waitUntil(
    self.registration.showNotification(data.title, options)
  );
});

// 监听 notificationclick 事件,当用户点击通知时触发
self.addEventListener("notificationclick", event => {
  event.notification.close(); // 关闭通知
  
  // 打开或聚焦到应用窗口
  event.waitUntil(
    clients.openWindow(event.notification.data.url || "/") // 跳转到通知中指定的URL或默认首页
  );
});

// 其他 Service Worker 生命周期事件,例如 install 和 activate
self.addEventListener("install", event => {
  console.log("Service Worker installing.");
  self.skipWaiting(); // 强制新的 Service Worker 立即激活
});

self.addEventListener("activate", event => {
  console.log("Service Worker activating.");
  event.waitUntil(clients.claim()); // 确保 Service Worker 控制所有客户端
});

代码说明:

  • self.addEventListener("push", ...) :监听 push 事件。当服务器通过 Web Push API 发送推送消息时,Service Worker 会接收到此事件。event.data.json() 用于解析推送消息的数据。self.registration.showNotification() 用于显示系统通知。
  • self.addEventListener("notificationclick", ...) :监听 notificationclick 事件。当用户点击系统通知时触发。event.notification.close() 关闭通知,clients.openWindow() 用于打开或聚焦到应用窗口,并可以跳转到通知中指定的 URL。
  • installactivate 事件 :这是 Service Worker 的基本生命周期事件,用于安装和激活 Service Worker。self.skipWaiting()clients.claim() 有助于确保新的 Service Worker 尽快生效并控制所有页面。

3.2.2 在主应用中注册 Service Worker

在您的 Vue 应用的入口文件(通常是 main.js)中注册 Service Worker:

javascript 复制代码
// main.js

// 检查浏览器是否支持 Service Worker
if ("serviceWorker" in navigator) {
  window.addEventListener("load", () => {
    // 注册 Service Worker
    navigator.serviceWorker.register("/sw.js")
      .then(registration => {
        console.log("Service Worker registered: ", registration);
      })
      .catch(registrationError => {
        console.log("Service Worker registration failed: ", registrationError);
      });
  });
}

// ... 其他 Vue 应用初始化代码

注意:

  • Service Worker 文件 (sw.js) 必须放在您的 Web 服务器的根目录下,或者在 scope 选项中指定其作用域。
  • Service Worker 只能通过 HTTPS 提供服务(除了 localhost)。

通过 PWA 和 Service Worker 的结合,您的 H5 应用将能够提供更强大的通知能力,即使在用户关闭浏览器或应用处于后台时,也能通过系统通知及时触达用户。

四、注意事项

在实现 SSE 实时通知和系统通知功能时,还需要注意以下几点,以确保功能的稳定性和用户体验:

4.1 SSE 连接保持

  • 移动端网络不稳定 :移动设备的网络环境复杂且不稳定,SSE 连接可能会因为网络切换、信号丢失等原因而断开。因此,客户端需要实现断线重连机制,确保连接断开后能够自动尝试重新建立连接,避免通知遗漏。
  • 心跳机制 :为了保持连接活跃,防止因长时间无数据传输而被代理服务器或防火墙关闭连接,建议在服务器端和客户端都实现心跳机制。服务器可以定期发送空数据或特定格式的心跳消息,客户端接收到后不进行处理,仅用于维持连接。

4.2 权限请求时机

  • 用户交互时请求 :浏览器对于通知权限的请求有严格的限制。最好在用户进行明确的交互操作后(例如点击"订阅通知"按钮)再请求通知权限,而不是在页面加载时立即请求。否则,浏览器可能会认为这是骚扰行为而拒绝授权,或者用户会直接拒绝,导致后续无法再请求权限。

4.3 后台通知

  • Service Worker 的作用 :当 H5 应用在后台运行或被完全关闭时,浏览器主线程会被挂起,此时传统的 JavaScript 无法显示通知。Service Worker 在此发挥关键作用 ,它作为一个独立的线程在后台运行,可以接收到服务器推送的 push 事件,并利用 self.registration.showNotification() 来显示系统通知,确保用户即使不打开应用也能收到提醒。

4.4 跨域问题

  • CORS 配置 :如果您的前端应用和 SSE 服务部署在不同的域名或端口上,将会遇到跨域问题 。请确保您的 SSE 服务端正确配置了 CORS (Cross-Origin Resource Sharing) 头部,允许前端域名进行跨域请求。例如,在 Node.js Express 中可以使用 cors 中间件。

4.5 性能优化

  • 避免频繁发送大量通知:虽然 SSE 适用于实时通知,但也要避免在短时间内发送过多的通知,这可能会对客户端性能和用户体验造成负面影响。对于高频更新的数据,可以考虑在服务器端进行聚合或限流。
  • SSE 与 WebSocket 的选择
    • SSE :实现简单,适用于单向数据流(服务器到客户端)的场景,如新闻更新、股票行情、系统通知等。它基于 HTTP 协议,可以利用现有的 HTTP 基础设施(如代理、负载均衡)。
    • WebSocket :提供双向全双工通信,适用于需要客户端和服务器之间频繁交互的场景,如在线聊天、多人协作游戏等。实现相对复杂,需要服务器支持 WebSocket 协议。
    • 根据您的具体需求选择合适的技术。对于本文描述的实时通知场景,SSE 是一个非常合适的选择,因为它足够简单且高效。

通过充分考虑并解决这些注意事项,您将能够构建一个更加健壮、高效且用户体验良好的实时通知系统。

相关推荐
加班是不可能的,除非双倍日工资1 小时前
css预编译器实现星空背景图
前端·css·vue3
wyiyiyi2 小时前
【Web后端】Django、flask及其场景——以构建系统原型为例
前端·数据库·后端·python·django·flask
gnip2 小时前
vite和webpack打包结构控制
前端·javascript
excel2 小时前
在二维 Canvas 中模拟三角形绕 X、Y 轴旋转
前端
阿华的代码王国3 小时前
【Android】RecyclerView复用CheckBox的异常状态
android·xml·java·前端·后端
一条上岸小咸鱼3 小时前
Kotlin 基本数据类型(三):Booleans、Characters
android·前端·kotlin
Jimmy3 小时前
AI 代理是什么,其有助于我们实现更智能编程
前端·后端·ai编程
ZXT3 小时前
promise & async await总结
前端
Jerry说前后端3 小时前
RecyclerView 性能优化:从原理到实践的深度优化方案
android·前端·性能优化
画个太阳作晴天3 小时前
A12预装app
linux·服务器·前端