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 已经成功激活,这种自动请求一般不需要额外处理。

相关推荐
huakoh1 小时前
MCP 参数校验失败,isError 还是 InvalidParams?用三条探针定归属
前端
涛涛ing1 小时前
你的网站准备好被AI Agent阅读了吗?2026年最被忽视的前端工程问题
前端
百万蹄蹄向前冲1 小时前
妙!文档懒得写,就让AI 生成新员工学习指南
前端·后端·trae
狗头大军之江苏分军1 小时前
《潮水漫过十七岁》对她的感觉
前端·后端
狗头大军之江苏分军1 小时前
《潮水漫过十七岁》我想买相机
前端·后端
excel1 小时前
Node.js 常用反爬虫工具推荐
前端
百万蹄蹄向前冲1 小时前
一句话生成Node.js学习官网秒发布上线
前端·后端·node.js
IT_陈寒1 小时前
当心!你以为简单的 JavaScript 数组操作其实坑不少
前端·人工智能·后端