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 是一个非常合适的选择,因为它足够简单且高效。

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

相关推荐
阿奇__4 分钟前
element 跨页选中,回显el-table选中数据
前端·vue.js·elementui
谢尔登5 分钟前
【React】SWR 和 React Query(TanStack Query)
前端·react.js·前端框架
断竿散人6 分钟前
专题一、HTML5基础教程-Viewport属性深入理解:移动端网页的魔法钥匙
前端
3Katrina7 分钟前
理解Promise:让异步编程更优雅
前端·javascript
星之金币8 分钟前
关于我用Cursor优化了一篇文章:30 分钟学会定制属于你的编程语言
前端·javascript
天外来物9 分钟前
实战分享:用CI/CD实现持续部署
前端·nginx·docker
moxiaoran575311 分钟前
Spring Cloud Gateway 动态路由实现方案
运维·服务器·前端
市民中心的蟋蟀11 分钟前
第十一章 这三个全局状态管理库之间的共性与差异 【上】
前端·javascript·react.js
vvilkim25 分钟前
Flutter 常用组件详解:Text、Button、Image、ListView 和 GridView
前端·flutter
vvilkim31 分钟前
Flutter 命名路由与参数传递完全指南
前端·flutter