You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Angular集成FCM推送时Chrome无法自动重启Service Worker

Angular集成FCM推送时Service Worker被终止后无法被推送事件自动唤醒问题排查

问题现象

目前在Angular应用中集成FCM推送通知功能,Service Worker(下文简称SW)已经在Chrome中注册成功,状态如下图:
SW注册激活状态截图
当前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初始化/事件处理失败,后续不再为推送事件唤醒它:

  1. removeAllEventListener('push')是非法调用
    该方法不是Service Worker全局上下文的原生API,SW启动执行到这行时会直接抛出ReferenceError,导致SW初始化流程中断。Chrome遇到SW启动时连续抛出未捕获错误,会将该SW标记为异常状态,拒绝为后续事件唤醒它。
  2. push事件的Promise链存在断裂,会导致Chrome认为事件处理异常
    处理logout事件分支时,直接return messaging.deleteToken(),但这个返回值没有被外层Promise捕获,也没有调用resolve,会导致waitUntil等待的Promise永远处于pending状态,Chrome会认为SW事件处理超时,触发异常熔断。另外当data === null时只调用了resolve()但没有加return,后续代码会继续执行,触发空指针错误。
  3. Firebase初始化时机错误
    把firebase.initializeApp放在push事件回调内部,每次SW唤醒后第一次收到推送才初始化,且初始化是异步的,没有被waitUntil等待,会导致FCM内部的push监听逻辑没有注册完成,事件就已经处理结束,SW被立刻终止。

修复步骤

  1. 首先删除非法的removeAllEventListener('push')代码行。
  2. 把Firebase初始化逻辑移到IIFE最外层,SW启动时就完成初始化,不要放在push事件回调里。
  3. 修复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);
});
})();
  1. 修复后先在Chrome DevTools的Application > Service Workers页面点击Unregister卸载旧的异常SW,再刷新页面注册新SW,关闭所有网站标签页测试推送即可。

额外注意:如果你的站点是在HTTP非安全环境(本地localhost除外),Chrome会限制SW的后台唤醒能力,必须使用HTTPS部署才能正常收到后台推送。

内容的提问来源于stack exchange,提问作者patrick.1729

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.30 12:15:34