You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.01 08:25:57