React PWA中Service Worker的activate与push事件未触发问题
Service Worker的activate和push事件无法触发的问题解决
核心问题定位
你当前的代码混淆了客户端Service Worker注册脚本和Service Worker自身脚本的职责:
- 你提供的
service-worker.js本质是客户端用来注册SW的逻辑(包含register()、unregister()等方法),里面的window对象、axios依赖都是客户端环境的特性,不能在Service Worker上下文运行。 - 你在脚本末尾添加的
navigator.serviceWorker.addEventListener('activate')和push事件监听,是在客户端页面监听事件,但activate和push是Service Worker自身上下文触发的事件,客户端页面无法直接监听这些事件。
正确的实现步骤
1. 拆分脚本文件
将代码拆分为两个独立文件:
- 客户端注册脚本:负责注册Service Worker、订阅推送(保留你原有的
register()、subscribeToPushNotifications等逻辑) - Service Worker自身脚本:独立的
public/service-worker.js,仅包含Service Worker上下文的事件监听逻辑
2. 修正Service Worker脚本(public/service-worker.js)
在这个脚本里,使用self(Service Worker的全局对象)监听事件:
// public/service-worker.js self.addEventListener('activate', function(event) { console.log('service worker activated'); // 可选:激活后立即接管所有客户端页面 event.waitUntil(self.clients.claim()); }); self.addEventListener('push', async function(event) { const message = await event.data.json(); const { title, description, image } = message; // 用waitUntil确保Service Worker在通知显示完成前不终止 event.waitUntil( self.registration.showNotification(title, { body: description, icon: image, actions: [ { title: "Say hi", action: "Say hi", }, ] }) ); });
3. 调整客户端注册脚本
保留原有的注册、订阅逻辑,但移除末尾的事件监听,确保注册时指向正确的SW脚本路径:
// src/serviceWorkerRegistration.js import axios from "axios"; const isLocalhost = Boolean( window.location.hostname === "localhost" || window.location.hostname === "[::1]" || window.location.hostname.match( /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ ) ); export function register(config) { if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) { const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); if (publicUrl.origin !== window.location.origin) { return; } window.addEventListener("load", () => { // 指向public目录下的独立Service Worker脚本 const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; if (isLocalhost) { checkValidServiceWorker(swUrl, config); } else { registerValidSW(swUrl, config); } }); } } async function subscribeToPushNotifications(serviceWorkerReg) { let subscription = await serviceWorkerReg.pushManager.getSubscription(); if (subscription === null) { const dev_public_vapid_key = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"; const prod_public_vapid_key = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"; const public_vapid_key = isLocalhost ? dev_public_vapid_key : prod_public_vapid_key; subscription = await serviceWorkerReg.pushManager.subscribe({ userVisibleOnly: true, // 注意:VAPID密钥需要转换为ArrayBuffer格式才能被识别 applicationServerKey: urlBase64ToUint8Array(public_vapid_key), }); axios .post("/api/push_notif_subscription/subscribe", subscription) .then((response) => {}) .catch((error) => {}); } } // 新增:将base64格式的VAPID密钥转换为ArrayBuffer的工具函数 function urlBase64ToUint8Array(base64String) { const padding = '='.repeat((4 - base64String.length % 4) % 4); const base64 = (base64String + padding) .replace(/-/g, '+') .replace(/_/g, '/'); const rawData = window.atob(base64); return Uint8Array.from([...rawData].map(char => char.charCodeAt(0))); } export function showPushNotification(title, description, image) { if (!("serviceWorker" in navigator)) { console.log("Service Worker is not supported in this browser"); return; } navigator.serviceWorker.ready.then(function (registration) { registration.showNotification(title, { body: description, icon: image, actions: [ { title: "Say hi", action: "Say hi", }, ], }); }); } function registerValidSW(swUrl, config) { navigator.serviceWorker.register(swUrl).then((registration) => { subscribeToPushNotifications(registration); registration.onupdatefound = () => { const installingWorker = registration.installing; if (!installingWorker) { return; } installingWorker.onstatechange = () => { if (installingWorker.state === "installed") { if (navigator.serviceWorker.controller) { if (config && config.onUpdate) { config.onUpdate(registration); } } else { if (config && config.onSuccess) { config.onSuccess(registration); } } } }; }); }); } function checkValidServiceWorker(swUrl, config) { fetch(swUrl, { headers: { "Service-Worker": "script" }, }).then((response) => { const contentType = response.headers.get("content-type"); if ( response.status === 404 || (!!contentType && contentType.indexOf("javascript") === -1) ) { navigator.serviceWorker.ready.then((registration) => { registration.unregister().then(() => { window.location.reload(); }); }); } else { registerValidSW(swUrl, config); } }); } export function unregister() { if ("serviceWorker" in navigator) { navigator.serviceWorker.ready.then((registration) => { registration.unregister(); }); } }
额外注意事项
- VAPID密钥格式:订阅推送时,
applicationServerKey必须是ArrayBuffer格式,不能直接传字符串,上面的代码已添加转换工具函数。 - SW更新激活:修改SW脚本后,需关闭所有相关标签页重新打开,或在SW脚本的
install事件中添加self.skipWaiting(),确保新SW立即激活。 - 推送权限验证:确保用户已授权接收通知,可在订阅前主动调用
Notification.requestPermission()请求权限。 - 后端推送测试:后端返回201仅表示推送请求被推送服务接收,可在浏览器Application > Service Workers > Push面板手动发送测试推送,验证
push事件是否触发。
内容的提问来源于stack exchange,提问作者AG_HIHI
相关产品推荐
相关产品推荐

