如何在同一作用域下让React PWA的Service Worker与FCM Worker协同工作?
React PWA集成FCM推送通知:合并Service Worker的解决方案
问题描述
我有一个service-worker.js文件,用来把ReactJS应用打包成PWA。现在想通过FCM添加推送通知功能,这要求在public文件夹下放firebase-messaging-sw.js文件。两者需要同作用域才能正常工作,但同一作用域下不能存在两个不同的Service Worker。我试过把service-worker.js重命名为firebase-messaging-sw.js但没生效,请问怎么让这两个文件协同工作?
原firebase-messaging-sw.js代码
// Scripts for firebase and firebase messaging importScripts("https://www.gstatic.com/firebasejs/8.2.0/firebase-app.js"); importScripts("https://www.gstatic.com/firebasejs/8.2.0/firebase-messaging.js"); // Initialize the Firebase app in the service worker by passing the generated config const firebaseConfig = { apiKey: "xxxx", authDomain: "xxxx.firebaseapp.com", projectId: "xxxx", storageBucket: "xxxx", messagingSenderId: "xxxx", appId: "xxxx", measurementId: "xxxx" } firebase.initializeApp(firebaseConfig); // Retrieve firebase messaging const messaging = firebase.messaging(); self.addEventListener("notificationclick", function (event) { console.debug('SW notification click event', event) const url = event.notification.data.link event.waitUntil( clients.matchAll({type: 'window'}).then( windowClients => { // Check if there is already a window/tab open with the target URL for (var i = 0; i < windowClients.length; i++) { var client = windowClients[i]; // If so, just focus it. if (client.url === url && 'focus' in client) { return client.focus(); } } // If not, then open the target URL in a new window/tab. if (clients.openWindow) { return clients.openWindow(url); } }) ); }) messaging.onBackgroundMessage(async function(payload) { console.log("Received background message ", payload) const notificationTitle = payload.notification.title const notificationOptions = { body: payload.notification.body, icon: './logo192.png', badge: './notification-badgex24.png', data: { link: payload.data?.link } } self.registration.showNotification(notificationTitle, notificationOptions) })
原service-worker.js代码
import { clientsClaim } from 'workbox-core'; import { ExpirationPlugin } from 'workbox-expiration'; import { precacheAndRoute, createHandlerBoundToURL } from 'workbox-precaching'; import { registerRoute } from 'workbox-routing'; import { StaleWhileRevalidate } from 'workbox-strategies'; clientsClaim(); const fileExtensionRegexp = new RegExp('/[^/?]+\\.[^/]+$'); registerRoute( // Return false to exempt requests from being fulfilled by index.html. ({ request, url }) => { // If this isn't a navigation, skip. if (request.mode !== 'navigate') { return false; } // If this is a URL that starts with /_, skip. if (url.pathname.startsWith('/_')) { return false; } // If this looks like a URL for a resource, because it contains // a file extension, skip. if (url.pathname.match(fileExtensionRegexp)) { return false; } // Return true to signal that we want to use the handler. return true; }, createHandlerBoundToURL(process.env.PUBLIC_URL + '/index.html') ); registerRoute( // Add in any other file extensions or routing criteria as needed. ({ url }) => url.origin === self.location.origin && url.pathname.endsWith('.png'), // Customize this strategy as needed, e.g., by changing to CacheFirst. new StaleWhileRevalidate({ cacheName: 'images', plugins: [ // Ensure that once this runtime cache reaches a maximum size the // least-recently used images are removed. new ExpirationPlugin({ maxEntries: 50 }), ], }) ); self.addEventListener('message', (event) => { if (event.data && event.data.type === 'SKIP_WAITING') { self.skipWaiting(); } });
解决方案:合并两个Service Worker逻辑
核心思路是将Workbox的PWA缓存逻辑整合到firebase-messaging-sw.js中,因为FCM会优先查找该文件名的Service Worker,这样就能在同一个SW中同时处理PWA缓存和推送通知。
步骤1:修改firebase-messaging-sw.js引入Workbox脚本
在Firebase的importScripts之后,添加对应版本的Workbox CDN脚本(版本必须和项目依赖的Workbox版本一致):
importScripts("https://storage.googleapis.com/workbox-cdn/releases/6.4.1/workbox-sw.js");
步骤2:整合原service-worker.js的逻辑
把service-worker.js的所有代码,放到Firebase初始化和消息监听逻辑的后面,最终完整代码如下:
// Scripts for firebase and firebase messaging importScripts("https://www.gstatic.com/firebasejs/8.2.0/firebase-app.js"); importScripts("https://www.gstatic.com/firebasejs/8.2.0/firebase-messaging.js"); // 引入Workbox脚本(版本需与项目依赖一致) importScripts("https://storage.googleapis.com/workbox-cdn/releases/6.4.1/workbox-sw.js"); // Initialize the Firebase app in the service worker const firebaseConfig = { apiKey: "xxxx", authDomain: "xxxx.firebaseapp.com", projectId: "xxxx", storageBucket: "xxxx", messagingSenderId: "xxxx", appId: "xxxx", measurementId: "xxxx" } firebase.initializeApp(firebaseConfig); const messaging = firebase.messaging(); // FCM通知点击处理 self.addEventListener("notificationclick", function (event) { console.debug('SW notification click event', event) const url = event.notification.data.link event.waitUntil( clients.matchAll({type: 'window'}).then( windowClients => { for (var i = 0; i < windowClients.length; i++) { var client = windowClients[i]; if (client.url === url && 'focus' in client) { return client.focus(); } } if (clients.openWindow) { return clients.openWindow(url); } }) ); }) // FCM后台消息监听 messaging.onBackgroundMessage(async function(payload) { console.log("Received background message ", payload) const notificationTitle = payload.notification.title const notificationOptions = { body: payload.notification.body, icon: './logo192.png', badge: './notification-badgex24.png', data: { link: payload.data?.link } } self.registration.showNotification(notificationTitle, notificationOptions) }) // -------------------------- 原service-worker.js逻辑开始 -------------------------- import { clientsClaim } from 'workbox-core'; import { ExpirationPlugin } from 'workbox-expiration'; import { precacheAndRoute, createHandlerBoundToURL } from 'workbox-precaching'; import { registerRoute } from 'workbox-routing'; import { StaleWhileRevalidate } from 'workbox-strategies'; clientsClaim(); const fileExtensionRegexp = new RegExp('/[^/?]+\\.[^/]+$'); registerRoute( ({ request, url }) => { if (request.mode !== 'navigate') return false; if (url.pathname.startsWith('/_')) return false; if (url.pathname.match(fileExtensionRegexp)) return false; return true; }, createHandlerBoundToURL(process.env.PUBLIC_URL + '/index.html') ); registerRoute( ({ url }) => url.origin === self.location.origin && url.pathname.endsWith('.png'), new StaleWhileRevalidate({ cacheName: 'images', plugins: [ new ExpirationPlugin({ maxEntries: 50 }), ], }) ); self.addEventListener('message', (event) => { if (event.data && event.data.type === 'SKIP_WAITING') { self.skipWaiting(); } }); // -------------------------- 原service-worker.js逻辑结束 --------------------------
步骤3:清理冗余文件
删除public文件夹中的原service-worker.js,避免出现多个SW冲突。
步骤4:更新SW注册逻辑
在React应用的入口文件(如index.js)中,注册指定firebase-messaging-sw.js:
if ('serviceWorker' in navigator) { window.addEventListener('load', () => { navigator.serviceWorker.register('/firebase-messaging-sw.js') .then(registration => { console.log('SW registered: ', registration); }) .catch(registrationError => { console.log('SW registration failed: ', registrationError); }); }); }
关键注意事项
- 版本匹配:Workbox的CDN版本必须和项目
package.json中依赖的Workbox版本完全一致,否则会出现API兼容问题。 - 作用域确保:
firebase-messaging-sw.js必须放在public根目录,保证作用域覆盖整个应用。 - 清除旧SW:测试前在浏览器开发者工具的
Application > Service Workers中注销旧的Service Worker,刷新页面重新注册新SW,避免缓存异常。
内容的提问来源于stack exchange,提问作者Art
相关产品推荐
相关产品推荐

