React中如何在非public文件夹注册Service Worker并实现通知交互
解决Service Worker注册失败及通知按钮调用服务函数的问题
一、修复Service Worker注册失败的MIME类型错误
你遇到的DOMException: Failed to register a ServiceWorker... unsupported MIME type ('text/html')错误,核心原因是注册时请求的sw.js路径不对,服务器返回了HTML页面(比如404页面)而非JavaScript文件。结合你的场景,按以下步骤修复:
1. 修正注册路径
你的sw.js实际路径是src/services/notify/sw.js,但前端项目中src目录下的文件默认不会直接被浏览器访问(除非构建工具配置了静态资源映射)。首先要确保sw.js能被浏览器通过正确URL访问:
- 如果是Create React App这类框架,建议把sw.js放在
public/services/notify/目录下(这样构建后会被复制到输出目录的对应路径)。 - 然后修改注册函数中的脚本路径为绝对路径:
export const registerServiceWorker = async () => { if ("serviceWorker" in navigator) { try { // 改为实际可访问的绝对路径 const registration = await navigator.serviceWorker.register("/services/notify/sw.js", { scope: "/", // 需要全局作用域的话保持这个配置 }); if (registration.installing) { Logger.log("Service worker installing"); } else if (registration.waiting) { Logger.log("Service worker installed"); } else if (registration.active) { Logger.log("Service worker active"); } } catch (error) { Logger.error(`Registration failed with ${error}`); } } };
2. 处理全局作用域(scope: "/")的限制
当Service Worker脚本不在根目录,但需要全局作用域时,需要额外配置:
- 方式1:添加Service-Worker-Allowed响应头
在服务器返回sw.js时,添加HTTP响应头:Service-Worker-Allowed: /,告诉浏览器该脚本可以作用于根路径。- 如果是开发服务器(比如webpack-dev-server),可以通过devServer.headers配置添加;
- 生产环境则在Nginx/Apache等服务器上配置响应头。
- 方式2:将sw.js放在根目录,通过导入模块复用逻辑
如果不想配置响应头,可以把sw.js放回根目录,然后在sw.js中导入src/services/notify/下的逻辑模块(需要确保模块能在Service Worker环境运行,无DOM依赖):// 根目录的sw.js import './services/notify/notificationHandler.js'; // 导入你的通知处理逻辑
3. 确保服务器返回正确的MIME类型
确保服务器对.js文件返回application/javascript的MIME类型:
- 大多数现代开发服务器(如CRA、Vite)默认会处理,但如果是自定义服务器,需要配置mime映射(比如Nginx中添加
types { application/javascript js; })。
二、实现通知按钮点击调用服务函数的方案
Service Worker是独立于页面的隔离环境,不能直接调用页面中的服务函数,推荐以下两种可行方案:
方案1:通过postMessage与页面通信
在Service Worker的通知点击事件中,向活跃的页面客户端发送消息,页面监听消息后调用服务函数:
- 修改Service Worker的notificationclick事件:
self.onnotificationclick = (event) => { event.notification.close(); const action = event.action; event.waitUntil( clients.matchAll({ type: "window", includeUncontrolled: true }).then((clientList) => { // 找到活跃的页面客户端 const targetClient = clientList[0] || clients.openWindow("/"); if (targetClient) { // 向页面发送动作类型 targetClient.postMessage({ type: "NOTIFICATION_ACTION", action: action }); } }) ); };
- 在页面中监听消息并调用服务函数:
在你的业务页面(比如login.jsx或App.jsx)中添加监听:
useEffect(() => { const handleMessage = (event) => { if (event.data.type === "NOTIFICATION_ACTION") { const { action } = event.data; // 调用你的服务函数 if (action === 'answer') { // 比如调用answerService() answerService(); } else if (action === 'decline') { declineService(); } } }; navigator.serviceWorker.addEventListener('message', handleMessage); return () => { navigator.serviceWorker.removeEventListener('message', handleMessage); }; }, []);
方案2:在Service Worker中直接导入服务模块
如果你的服务函数没有DOM依赖(比如只是纯逻辑或API调用),可以把服务函数抽成独立的模块,在sw.js中直接导入:
- 抽离服务函数为独立模块:
// src/services/notify/notificationActions.js export const handleAnswerAction = async () => { // 这里写你的answer逻辑,比如调用API await fetch('/api/answer-call', { method: 'POST' }); }; export const handleDeclineAction = async () => { await fetch('/api/decline-call', { method: 'POST' }); };
- 在sw.js中导入并使用:
// src/services/notify/sw.js import { handleAnswerAction, handleDeclineAction } from './notificationActions.js'; self.onnotificationclick = (event) => { event.notification.close(); if (event.action === 'answer') { event.waitUntil(handleAnswerAction()); } else if (event.action === 'decline') { event.waitUntil(handleDeclineAction()); } // 页面聚焦逻辑保持不变 event.waitUntil( clients.matchAll({ type: "window", includeUncontrolled: true }).then((clientList) => { if (clientList.length > 1) { clientList.forEach((client) => { if (client.url === "/" && "focus" in client) return client.focus(); }) if (clients.openWindow) return clients.openWindow("/"); } else { clientList[0]?.focus(); } }) ); };
注意:如果使用ES模块,需要在注册Service Worker时指定type: 'module':
navigator.serviceWorker.register("/services/notify/sw.js", { scope: "/", type: 'module' // 添加这个配置 });
内容的提问来源于stack exchange,提问作者Sai Barath R
相关产品推荐
相关产品推荐

