registration.pushManager.subscribe() 调用后为什么会自动访问根目录下的 sw.js

在使用 Web Push 推送功能时,很多人会发现:调用

scss 复制代码
registration.pushManager.subscribe()

之后,浏览器会自动请求网站根目录下的 sw.js 文件,例如:

bash 复制代码
/sw.js

这并不是 pushManager.subscribe() 本身主动加载了 sw.js,而通常是因为 当前页面还没有注册并控制有效的 Service Worker,浏览器需要先完成 Service Worker 的注册、安装和激活流程,才能创建 Push Subscription。

本文记录这一过程,以及正确的使用方式。


一、什么是 pushManager.subscribe()

pushManager.subscribe() 用于创建一个 Push Subscription,也就是当前浏览器与推送服务之间的订阅关系。

基本代码如下:

ini 复制代码
const registration = await navigator.serviceWorker.ready;

const subscription = await registration.pushManager.subscribe({
  userVisibleOnly: true,
  applicationServerKey: publicVapidKey
});

console.log(subscription);

订阅成功后,会返回一个 PushSubscription 对象,通常包含:

  • endpoint
  • expirationTime
  • keys.p256dh
  • keys.auth

这些信息需要发送到服务器保存,后续服务器才能根据该订阅向浏览器发送推送消息。


二、为什么会请求根目录下的 sw.js

关键代码通常是:

c 复制代码
navigator.serviceWorker.register('/sw.js');

这段代码的作用是注册 Service Worker。浏览器会请求:

arduino 复制代码
https://example.com/sw.js

其中 /sw.js 表示网站根目录下的 sw.js 文件。

完整流程通常如下:

csharp 复制代码
await navigator.serviceWorker.register('/sw.js');

const registration = await navigator.serviceWorker.ready;

const subscription = await registration.pushManager.subscribe({
  userVisibleOnly: true,
  applicationServerKey: publicVapidKey
});

执行过程大致为:

  1. 浏览器请求 /sw.js
  2. 下载并解析 Service Worker 文件
  3. 执行安装阶段
  4. 激活 Service Worker
  5. 等待 Service Worker 准备完成
  6. 调用 pushManager.subscribe()
  7. 创建 Push Subscription

因此,看到浏览器访问 /sw.js 是正常现象。


三、Service Worker 为什么是 Web Push 的前提

Service Worker 是运行在后台的脚本,即使网页没有打开,它也可以接收推送消息。

例如,推送消息到达后,Service Worker 可以监听 push 事件:

php 复制代码
self.addEventListener('push', event => {
  const data = event.data
    ? event.data.json()
    : {
        title: '新消息',
        body: '你收到了一条新消息'
      };

  event.waitUntil(
    self.registration.showNotification(data.title, {
      body: data.body,
      icon: '/icons/icon-192.png'
    })
  );
});

由于推送消息需要由 Service Worker 在后台接收和处理,所以浏览器必须先注册一个有效的 Service Worker。


四、推荐的注册和订阅代码

页面代码

javascript 复制代码
async function subscribeToPush() {
  if (!('serviceWorker' in navigator)) {
    throw new Error('当前浏览器不支持 Service Worker');
  }

  if (!('PushManager' in window)) {
    throw new Error('当前浏览器不支持 Web Push');
  }

  const permission = await Notification.requestPermission();

  if (permission !== 'granted') {
    throw new Error('用户没有授予通知权限');
  }

  const registration = await navigator.serviceWorker.register('/sw.js');

  await navigator.serviceWorker.ready;

  const subscription =
    await registration.pushManager.getSubscription();

  if (subscription) {
    return subscription;
  }

  const newSubscription =
    await registration.pushManager.subscribe({
      userVisibleOnly: true,
      applicationServerKey: urlBase64ToUint8Array(
        '你的 VAPID 公钥'
      )
    });

  return newSubscription;
}

辅助函数:

javascript 复制代码
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))
  );
}

调用:

javascript 复制代码
subscribeToPush()
  .then(subscription => {
    console.log('Push Subscription:', subscription);

    // 将 subscription 发送给服务器保存
  })
  .catch(error => {
    console.error('订阅推送失败:', error);
  });

五、为什么使用 /sw.js 而不是 ./sw.js

以下两种写法看起来相似:

c 复制代码
navigator.serviceWorker.register('/sw.js');
c 复制代码
navigator.serviceWorker.register('./sw.js');

但它们的作用域可能不同。

使用根路径

c 复制代码
navigator.serviceWorker.register('/sw.js');

表示从网站根目录加载 Service Worker:

arduino 复制代码
https://example.com/sw.js

默认作用域是整个站点:

arduino 复制代码
https://example.com/

使用相对路径

如果当前页面地址是:

arduino 复制代码
https://example.com/user/profile.html

执行:

c 复制代码
navigator.serviceWorker.register('./sw.js');

浏览器可能会请求:

arduino 复制代码
https://example.com/user/sw.js

默认作用域也可能被限制在 /user/ 目录下。

因此,如果希望 Service Worker 控制整个网站,通常使用:

c 复制代码
navigator.serviceWorker.register('/sw.js');

六、sw.js 文件必须放在哪里

如果注册代码是:

c 复制代码
navigator.serviceWorker.register('/sw.js');

服务器必须能够正确返回:

arduino 复制代码
https://你的域名/sw.js

例如项目结构可以是:

arduino 复制代码
public/
├── index.html
├── sw.js
└── icons/
    └── icon-192.png

构建后,sw.js 应该被部署到网站根目录。

如果访问:

arduino 复制代码
https://example.com/sw.js

返回的是 404、HTML 页面或错误内容,Service Worker 注册就会失败。


七、sw.js 的基本内容

一个最简单的 Service Worker 可以这样写:

ini 复制代码
self.addEventListener('install', event => {
  console.log('Service Worker 安装中');
  self.skipWaiting();
});

self.addEventListener('activate', event => {
  console.log('Service Worker 已激活');
  event.waitUntil(self.clients.claim());
});

self.addEventListener('push', event => {
  const data = event.data
    ? event.data.json()
    : {
        title: '通知',
        body: '你收到了一条推送消息'
      };

  event.waitUntil(
    self.registration.showNotification(data.title, {
      body: data.body,
      icon: '/icons/icon-192.png'
    })
  );
});

self.addEventListener('notificationclick', event => {
  event.notification.close();

  event.waitUntil(
    clients.openWindow('/')
  );
});

注意:sw.js 中不能直接使用页面脚本里的变量,也不能直接访问 window 和 document。

Service Worker 中使用的是:

lua 复制代码
self

而不是:

javascript 复制代码
window

八、navigator.serviceWorker.ready 的作用

推荐使用:

ini 复制代码
const registration = await navigator.serviceWorker.ready;

navigator.serviceWorker.ready 会等待,直到当前页面对应的 Service Worker 注册完成并且处于激活状态。

例如:

csharp 复制代码
const registration = await navigator.serviceWorker.ready;

await registration.pushManager.subscribe({
  userVisibleOnly: true,
  applicationServerKey: publicVapidKey
});

这样可以避免在 Service Worker 尚未激活时就调用订阅接口。

需要注意,ready 返回的是一个 ServiceWorkerRegistration 对象,而不是 Service Worker 脚本本身。


九、如果已经注册过,是否还会重复访问 sw.js

即使已经注册过 Service Worker,浏览器仍然可能再次检查或请求 sw.js,原因包括:

  • 浏览器定期检查 Service Worker 是否有更新
  • 页面重新打开
  • 调用了 register(),浏览器需要确认脚本是否发生变化
  • HTTP 缓存策略导致重新验证
  • Service Worker 版本发生更新
  • 开发环境中 DevTools 设置了禁用缓存

这并不一定意味着 Service Worker 被重新安装了。

浏览器通常会先检查脚本内容是否变化,只有内容发生变化时,才会触发更新流程。


十、如何避免每次重复注册

可以将注册操作封装起来:

csharp 复制代码
let registrationPromise;

function getServiceWorkerRegistration() {
  if (!registrationPromise) {
    registrationPromise =
      navigator.serviceWorker.register('/sw.js');
  }

  return registrationPromise;
}

使用:

ini 复制代码
const registration = await getServiceWorkerRegistration();

const subscription =
  await registration.pushManager.subscribe({
    userVisibleOnly: true,
    applicationServerKey: publicVapidKey
  });

不过,即使多次执行 register('/sw.js'),浏览器也不会重复创建多个相同的 Service Worker 注册。浏览器会根据脚本地址和作用域管理已有注册。


十一、优先使用已有的订阅

在调用 subscribe() 之前,建议先调用:

ini 复制代码
const existingSubscription =
  await registration.pushManager.getSubscription();

完整示例:

csharp 复制代码
async function getPushSubscription() {
  const registration =
    await navigator.serviceWorker.ready;

  let subscription =
    await registration.pushManager.getSubscription();

  if (!subscription) {
    subscription =
      await registration.pushManager.subscribe({
        userVisibleOnly: true,
        applicationServerKey: publicVapidKey
      });
  }

  return subscription;
}

这样可以避免重复创建订阅。


十二、常见错误

1. Failed to register a ServiceWorker

可能原因:

  • /sw.js 不存在
  • 返回内容不是 JavaScript
  • 文件路径错误
  • Service Worker 脚本存在语法错误
  • 当前网站不是 HTTPS

2. The operation is insecure

Service Worker 和 Push API 通常要求安全环境:

  • 正式环境必须使用 HTTPS
  • 本地开发时 localhost 通常被浏览器视为安全来源

例如:

arduino 复制代码
https://example.com

通常可以使用。

而:

arduino 复制代码
http://example.com

通常不允许使用。


3. Registration failed - permission denied

可能原因:

  • 用户拒绝了通知权限
  • 浏览器或系统关闭了通知权限
  • 当前页面不是安全来源
  • 浏览器设置禁止通知

代码中应检查:

javascript 复制代码
const permission = await Notification.requestPermission();

if (permission !== 'granted') {
  console.log('通知权限未获得');
}

4. No active Service Worker

说明 Service Worker 可能还没有激活。

可以使用:

ini 复制代码
const registration = await navigator.serviceWorker.ready;

不要只依赖:

c 复制代码
navigator.serviceWorker.register('/sw.js');

因为 register() 返回时,Service Worker 不一定已经处于激活状态。


5. userVisibleOnly 错误

Chrome 等浏览器通常要求:

vbnet 复制代码
userVisibleOnly: true

这表示每一次推送消息都必须向用户展示可见通知。

正确写法:

php 复制代码
registration.pushManager.subscribe({
  userVisibleOnly: true,
  applicationServerKey: publicVapidKey
});

十三、如何排查 /sw.js 请求问题

可以打开浏览器开发者工具:

  1. 打开 DevTools
  2. 进入 Application
  3. 找到 Service Workers
  4. 查看当前注册状态
  5. 查看注册的脚本地址和作用域
  6. 点击 Unregister 取消注册
  7. 勾选 Update on reload 方便调试

同时在 Network 面板中检查:

bash 复制代码
/sw.js

重点查看:

  • HTTP 状态码是否为 200
  • Content-Type 是否正确
  • 返回内容是否真的是 Service Worker JavaScript
  • 是否被重定向到登录页
  • 是否被前端路由返回了 index.html
  • 是否存在缓存问题

十四、需要注意的关键点

1. Service Worker 文件必须使用正确的 MIME 类型

服务器最好返回:

bash 复制代码
Content-Type: application/javascript

或者:

arduino 复制代码
Content-Type: text/javascript

不要把 sw.js 返回成 HTML。


2. Service Worker 作用域受脚本路径影响

例如:

c 复制代码
navigator.serviceWorker.register('/assets/sw.js');

默认可能只能控制 /assets/ 目录。

如果希望控制根目录,需要:

c 复制代码
navigator.serviceWorker.register('/sw.js');

或者使用服务器响应头扩大作用域:

makefile 复制代码
Service-Worker-Allowed: /

但通常把 sw.js 放在网站根目录是最简单的方式。


3. Push 订阅不是永久不变的

Push Subscription 可能因为以下原因失效:

  • 用户清除浏览器数据
  • 用户取消网站权限
  • 浏览器更换推送服务
  • 订阅过期
  • 浏览器或系统策略变化

因此服务器发送推送失败时,应考虑删除旧订阅,并允许客户端重新订阅。


十五、完整的推荐流程

javascript 复制代码
async function initPush() {
  if (!('serviceWorker' in navigator)) {
    throw new Error('不支持 Service Worker');
  }

  if (!('PushManager' in window)) {
    throw new Error('不支持 Push API');
  }

  const permission =
    await Notification.requestPermission();

  if (permission !== 'granted') {
    throw new Error('通知权限未授权');
  }

  await navigator.serviceWorker.register('/sw.js');

  const registration =
    await navigator.serviceWorker.ready;

  let subscription =
    await registration.pushManager.getSubscription();

  if (!subscription) {
    subscription =
      await registration.pushManager.subscribe({
        userVisibleOnly: true,
        applicationServerKey: urlBase64ToUint8Array(
          publicVapidKey
        )
      });
  }

  console.log('当前 Push Subscription:', subscription);

  // 发送到服务器
  await fetch('/api/push/subscribe', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(subscription)
  });

  return subscription;
}

十六、结论

调用:

scss 复制代码
registration.pushManager.subscribe()

后访问根目录下的:

bash 复制代码
/sw.js

通常是正常现象。

根本原因是:

  1. Web Push 必须依赖 Service Worker
  2. Service Worker 通常通过 /sw.js 注册
  3. 浏览器需要确认 Service Worker 已安装并激活
  4. 浏览器可能定期重新检查 sw.js 是否更新

推荐使用以下方式:

csharp 复制代码
await navigator.serviceWorker.register('/sw.js');

const registration =
  await navigator.serviceWorker.ready;

const subscription =
  await registration.pushManager.subscribe({
    userVisibleOnly: true,
    applicationServerKey: publicVapidKey
  });

只要 /sw.js 能够正常访问、页面使用 HTTPS、通知权限已授权,并且 Service Worker 已经成功激活,这种自动请求一般不需要额外处理。

相关推荐
lpfasd12314 小时前
node-sass 停止维护!老项目迁移 Dart Sass 完整教程
前端·rust·sass
Data analyse45616 小时前
rnweb 回放数据量太大怎么办?分片存储与断点续传的实现思路
前端·网络·算法·数据分析
xiaogai_gai17 小时前
金蝶收料通知单查询策略实战教程:从源系统拉取到中间层落地的完整路径
java·前端·数据库
环境栈笔记17 小时前
指纹浏览器 WebRTC 怎么设置?从 ICE 候选、代理出口到功能验证
前端·人工智能·后端·自动化
赵大仁18 小时前
官网性能优化实战:LCP 3.2s → 2.1s、CLS 0.107 → 0,以及被我推翻的 6 个判断
前端·性能优化·cls·lcp·core web vitals
xiancai_xianyu18 小时前
Text2SQL落地生产库前,SQL要过哪三道闸
前端·数据库·sql·sql注入·text2sql·ai agent开发·企业级ai框架
小羊没烦恼!18 小时前
T-SQL查询进阶--理解SQL SERVER中的分区表
服务器·开发语言·前端·javascript·html
史瓦辛格18 小时前
给 DeepSeek Harness 写费用插件:把「这次会话花了多少钱」算准
前端·deepseek
星光不问赶路人18 小时前
Vue 3 + TypeScript 中 enum 的使用实践
前端·vue.js
鸥梨菌Honevid18 小时前
UVM 中 REQ 和 RSP 类型详解:从通信模型到 item_done 与 put_response
java·前端·servlet