引言
在现代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()
:初始化eventSource
为null
,subscribed
标记连接状态。subscribe(userId, onMessageCallback, onErrorCallback)
:- 接收
userId
用于服务器识别用户,以及两个回调函数onMessageCallback
和onErrorCallback
分别处理接收到的消息和连接错误。 - 通过
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
进行提示。第二个回调用于处理连接错误。 - 设置
isSubscribed
为true
,禁用"订阅通知"按钮,防止重复点击。
- 获取
beforeDestroy()
生命周期钩子 :- 在 Vue 组件销毁之前,调用
SSENotification.unsubscribe()
方法,确保 SSE 连接被正确关闭,避免内存泄漏和不必要的服务器资源占用。这是管理 SSE 连接生命周期的关键一步。
- 在 Vue 组件销毁之前,调用
通过以上步骤,您的 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
,值是对应的 Expressresponse
对象。通过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
对象,进行资源清理。
- 这是 SSE 的核心接口。当客户端通过
/api/notifications/send
路由 (POST) :- 这是一个用于向特定用户发送通知的 API。通常由后端其他服务或管理员界面调用。
- 它接收
userId
、title
、content
和icon
等参数。 - 通过
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。install
和activate
事件 :这是 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 是一个非常合适的选择,因为它足够简单且高效。
通过充分考虑并解决这些注意事项,您将能够构建一个更加健壮、高效且用户体验良好的实时通知系统。