如何实现Service Worker后台API拉取与离线通知功能
Service Worker后台操作与离线通知实现指南
核心原理
- Service Worker是独立于网页的后台脚本,只要浏览器处于运行状态,即使标签页关闭,它也能被推送事件唤醒或通过后台同步API执行任务(受浏览器休眠策略限制,但核心功能不受标签页状态影响)
- 用Push API接收服务器主动推送的消息,触发桌面通知;用Background Sync API实现标签页关闭后的后台数据拉取
- 数据缓存可通过Cache API灵活控制,按需选择是否缓存API返回内容
实现步骤
1. 基础:注册Service Worker与推送订阅
在网页主脚本中完成Service Worker注册,申请通知权限并订阅推送服务,同时将订阅信息同步到服务器以便后续推送。
2. 后台数据拉取(可选缓存)
在Service Worker中监听sync事件,触发后台API请求,可结合Cache API缓存请求结果,拉取完成后主动触发通知。
3. 推送通知接收
Service Worker监听push事件,收到服务器推送后立即展示通知,确保用户在标签页关闭时也能收到提醒。
4. 标签页关闭触发逻辑
通过网页的beforeunload事件触发后台同步注册,或由服务器在检测到用户离线后主动发起推送。
修正后完整代码
service-worker.js
// 监听推送事件,收到服务器消息后展示通知 self.addEventListener("push", (e) => { const data = e.data.json(); console.log("收到推送消息..."); // 用waitUntil保证通知展示完成前Service Worker不被销毁 e.waitUntil( self.registration.showNotification(data.title, { body: data.description, icon: data.icon, }) ); }); // 监听后台同步事件,执行API数据拉取(标签页关闭后仍能触发) self.addEventListener("sync", (e) => { if (e.tag === "fetch-data") { e.waitUntil( fetch("/api/data-endpoint") .then(response => { // 可选:缓存请求结果 if (response.ok) { return caches.open("data-cache-v1").then(cache => { return cache.put("/api/data-endpoint", response.clone()); }); } throw new Error("API请求失败"); }) .then(() => { // 拉取成功后主动通知用户 return self.registration.showNotification("数据更新", { body: "后台已完成最新数据拉取", icon: "/icons/notification-icon.png" }); }) .catch(err => console.error("后台拉取失败:", err)) ); } });
index.js
// VAPID公钥,用于推送服务身份验证 const publicVapidKey = "BJthRQ5myDgc7OSXzPCMftGw-n16F7zQBEN7EUD6XxcfTTvrLGWSIG7y_JxiWtVlCFua0S8MTB5rPziBqNx1qIo"; // 检查浏览器是否支持Service Worker if ("serviceWorker" in navigator) { // 先申请通知权限 Notification.requestPermission().then(permission => { if (permission === "granted") { initServiceWorker().catch(err => console.error("初始化失败:", err)); } else { console.warn("用户已拒绝通知权限"); } }); } // 注册Service Worker、订阅推送、注册后台同步 async function initServiceWorker() { // 注册Service Worker,注意路径修正为正确的文件地址 console.log("正在注册Service Worker..."); const register = await navigator.serviceWorker.register( "/service-worker.js", { scope: "/" } ); console.log("Service Worker注册完成..."); // 订阅推送服务 console.log("正在注册推送服务..."); const subscription = await register.pushManager.subscribe({ userVisibleOnly: true, // 要求所有推送都对用户可见 applicationServerKey: urlBase64ToUint8Array(publicVapidKey) }); console.log("推送服务注册完成..."); // 将订阅信息发送到服务器保存 console.log("正在同步订阅信息到服务器..."); await fetch("/user/receive-notification", { method: "POST", body: JSON.stringify({ subscription }), headers: { Authorization: "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2M2NlMDk4YTU0YTIyMDZhMzRhYzQzMjYiLCJlbWFpbCI6ImVtYWlsQGdtYWlsLmNvbSIsInZlcmlmaWVkIjpmYWxzZSwiaWF0IjoxNjc1MDc0MDk3fQ.zkvnahJh4uUlCRRxheE_24F5m55VLQWR1L04YvDO0ww", "Content-Type": "application/json" } }); // 注册后台同步事件,标签页关闭后仍会执行 await register.sync.register("fetch-data"); } // 将Base64格式的VAPID密钥转换为Uint8Array function urlBase64ToUint8Array(base64String) { const padding = "=".repeat((4 - (base64String.length % 4)) % 4); const base64 = (base64String + padding) .replace(/-/g, "+") .replace(/_/g, "/"); const rawData = window.atob(base64); const outputArray = new Uint8Array(rawData.length); for (let i = 0; i < rawData.length; ++i) { outputArray[i] = rawData.charCodeAt(i); } return outputArray; }
关键注意事项
- 必须在HTTPS环境下运行(localhost可用于开发测试)
- Service Worker文件需放在网站根目录,确保作用域覆盖整个站点
- 后台同步功能依赖浏览器支持,部分移动端浏览器可能存在限制
- 服务器需妥善保存用户的推送订阅信息,在需要时(如用户离线后有新数据)主动发起推送请求
内容的提问来源于stack exchange,提问作者Robiul
相关产品推荐
相关产品推荐

