Angular集成FCM推送时Chrome无法自动重启Service Worker
Angular集成FCM推送时Service Worker被终止后无法被推送事件自动唤醒问题排查
问题现象
目前在Angular应用中集成FCM推送通知功能,Service Worker(下文简称SW)已经在Chrome中注册成功,状态如下图:
当前SW安装状态为ACTIVATED,手动在DevTools中点击Start启动SW时,可以正常接收推送通知。但存在异常:Chrome会基于性能优化逻辑,判定SW无运行必要时将其终止,而推送通知事件触发时Chrome不会自动重启SW,导致用户关闭网站后无法收到推送。
运行环境版本
- Angular:
v13 - Chrome:
v102.0.5005.61
当前使用的SW监听push事件代码如下:
importScripts('./assets/js/async-waituntil.js'); importScripts('https://cdn.jsdelivr.net/gh/mozilla/localForage@master/dist/localforage.js'); importScripts('https://www.gstatic.com/firebasejs/8.2.6/firebase-app.js'); importScripts('https://www.gstatic.com/firebasejs/8.2.6/firebase-messaging.js'); (function () { 'use strict'; self.addEventListener('install', function (event) { self.skipWaiting(); }); self.addEventListener('activate', (event) => { event.waitUntil(clients.claim()); }); removeAllEventListener('push'); self.addEventListener('push', (msg) => { const itsGonnaBeLegendary = new Promise((resolve, reject) => { let data = null; if (msg && msg.data && msg.data.json()) { data = msg.data.json(); } if (data === null) { resolve(); } if (data.data && data.data['event'] && data.data['event'] === 'logout') { const firebaseConfig = { apiKey: '****', authDomain: '****', projectId: '****', storageBucket: '****', messagingSenderId: '****', appId: '****', }; firebase.initializeApp(firebaseConfig); const messaging = firebase.messaging(); return messaging.deleteToken(); } if (!msg.data) { resolve(); return; } self.clients .matchAll() .then((client) => { try { if (client.length > 0) { try { client[client.length - 1].postMessage(data); } catch (e) { resolve(); return; } } let url = ''; localforage .getItem('user') .then((userdata) => { const user = JSON.parse(userdata); if (data.data) { const gcmData = data.data; url = gcmData.url; if (gcmData.type === 'note' && !gcmData.notify.split(',').includes(user.name)) { resolve(); return; } } self.clients .matchAll({ type: 'window', includeUncontrolled: true, }) .then(function (windowClients) { try { let isVisible = false; if ( windowClients.length > 0 && windowClients.findIndex((x) => x.url.includes('hello')) > -1 ) { isVisible = true; } else { isVisible = false; } if (!isVisible) { const desc = data.notification; let options = { data: { url }, }; self.registration .showNotification(desc['title'], options) .then(() => { resolve(); return; }) .catch((e) => { resolve(); return; }); } } catch (e) { resolve(); } }) .catch((e) => { resolve(); return; }); }) .catch((e) => { resolve(); return; }); } catch (e) { resolve(); return; } }) .catch((e) => { resolve(); return; }); }); msg.waitUntil(itsGonnaBeLegendary); }); self.addEventListener('notificationclick', (event) => { event.notification.close(); var promise = new Promise(function (resolve) { if (clients.openWindow && event.notification.data.url) { clients.matchAll().then(function (clientsArr) { const client = clientsArr.find( (c) => c.url.includes('custom-url') || c.url.includes('custom-url-1') ); if (client) { resolve(client.focus()); } else { resolve(clients.openWindow(event.notification.data.url)); } }); } }).then((x) => { return x; }); event.waitUntil(promise); }); })();
按照官方文档说明,SW对应目标事件触发时浏览器应当自动重启SW,但实际无法达到该预期,需要排查问题原因。
问题原因&修复方案
SW无法被推送事件自动唤醒,核心是代码存在3个致命问题,会导致Chrome判定SW初始化/事件处理失败,后续不再为推送事件唤醒它:
removeAllEventListener('push')是非法调用
该方法不是Service Worker全局上下文的原生API,SW启动执行到这行时会直接抛出ReferenceError,导致SW初始化流程中断。Chrome遇到SW启动时连续抛出未捕获错误,会将该SW标记为异常状态,拒绝为后续事件唤醒它。- push事件的Promise链存在断裂,会导致Chrome认为事件处理异常
处理logout事件分支时,直接return messaging.deleteToken(),但这个返回值没有被外层Promise捕获,也没有调用resolve,会导致waitUntil等待的Promise永远处于pending状态,Chrome会认为SW事件处理超时,触发异常熔断。另外当data === null时只调用了resolve()但没有加return,后续代码会继续执行,触发空指针错误。 - Firebase初始化时机错误
把firebase.initializeApp放在push事件回调内部,每次SW唤醒后第一次收到推送才初始化,且初始化是异步的,没有被waitUntil等待,会导致FCM内部的push监听逻辑没有注册完成,事件就已经处理结束,SW被立刻终止。
修复步骤
- 首先删除非法的
removeAllEventListener('push')代码行。 - 把Firebase初始化逻辑移到IIFE最外层,SW启动时就完成初始化,不要放在push事件回调里。
- 修复Promise链,所有分支都必须正确触发
resolve/reject,不要出现悬空的return。
修复后的SW核心代码参考:
importScripts('./assets/js/async-waituntil.js'); importScripts('https://cdn.jsdelivr.net/gh/mozilla/localForage@master/dist/localforage.js'); importScripts('https://www.gstatic.com/firebasejs/8.2.6/firebase-app.js'); importScripts('https://www.gstatic.com/firebasejs/8.2.6/firebase-messaging.js'); (function () { 'use strict'; // 提前初始化Firebase,SW启动就完成注册 const firebaseConfig = { apiKey: '****', authDomain: '****', projectId: '****', storageBucket: '****', messagingSenderId: '****', appId: '****', }; firebase.initializeApp(firebaseConfig); const messaging = firebase.messaging(); self.addEventListener('install', function (event) { self.skipWaiting(); }); self.addEventListener('activate', (event) => { event.waitUntil(clients.claim()); }); self.addEventListener('push', (msg) => { const itsGonnaBeLegendary = new Promise(async (resolve) => { try { let data = null; if (msg?.data) { data = msg.data.json(); } // 空数据直接结束,加return阻断后续逻辑 if (!data) { resolve(); return; } // 处理logout事件,正确等待deleteToken完成再resolve if (data.data?.event === 'logout') { await messaging.deleteToken(); resolve(); return; } // 匹配所有客户端 const allClients = await self.clients.matchAll(); if (allClients.length > 0) { allClients[allClients.length - 1].postMessage(data); } const userdata = await localforage.getItem('user'); const user = JSON.parse(userdata); const gcmData = data.data || {}; const url = gcmData.url || ''; // 不需要通知的用户直接结束 if (gcmData.type === 'note' && !gcmData.notify.split(',').includes(user.name)) { resolve(); return; } // 检查可见窗口 const windowClients = await self.clients.matchAll({ type: 'window', includeUncontrolled: true, }); const hasVisiblePage = windowClients.some(x => x.url.includes('hello')); if (!hasVisiblePage && data.notification) { await self.registration.showNotification(data.notification.title, { data: { url } }); } resolve(); } catch (e) { // 所有异常都要正常resolve,不要让Promise挂起 resolve(); } }); msg.waitUntil(itsGonnaBeLegendary); }); self.addEventListener('notificationclick', (event) => { event.notification.close(); const promise = new Promise(async (resolve) => { if (!clients.openWindow || !event.notification.data?.url) { resolve(); return; } const clientsArr = await clients.matchAll(); const existClient = clientsArr.find(c => c.url.includes('custom-url') || c.url.includes('custom-url-1')); if (existClient) { resolve(existClient.focus()); } else { resolve(clients.openWindow(event.notification.data.url)); } }); event.waitUntil(promise); }); })();
- 修复后先在Chrome DevTools的Application > Service Workers页面点击
Unregister卸载旧的异常SW,再刷新页面注册新SW,关闭所有网站标签页测试推送即可。
额外注意:如果你的站点是在HTTP非安全环境(本地localhost除外),Chrome会限制SW的后台唤醒能力,必须使用HTTPS部署才能正常收到后台推送。
内容的提问来源于stack exchange,提问作者patrick.1729
相关产品推荐
相关产品推荐

