服务工作者无法跳过等待状态

4

我仍在进行实验,以掌握服务工作者,并面临一个问题,可能是由于我在JavaScript和服务工作者方面缺乏专业知识。

问题发生在我想要使用postMessage()让新的服务工作者skipWaiting()时。如果我展示一个带有按钮的弹窗,并在那里绑定对postMessage()的调用,一切正常。如果我直接调用postMessage(),它就不能工作。这是由于竞态条件,因为有时它能够工作,但我无法确定竞争条件。

顺便提一下,postMessage()调用可以工作,服务工作者记录了当获取消息时应该记录的内容:

// Listen to messages from clients.
self.addEventListener('message', event => {
    switch(event.data) {
        case 'skipWaiting': self.skipWaiting(); console.log('I skipped waiting... EXTRA');
            break;
    }
}); 

这里是代码。关键部分在if (registration.waiting)条件语句中。未注释的代码可以工作,而注释掉的则不行:

// Register service worker.
if ('serviceWorker' in navigator) {
    // Helpers to show and hide the update toast.
    let hideUpdateToast = () => {
        document.getElementById('update_available').style.visibility = 'hidden';
    };

    let showUpdateToast = (serviceworker) => {
        document.getElementById('update_available').style.visibility = 'visible';
        document.getElementById('force_install').onclick = () => {
            serviceworker.postMessage('skipWaiting');
            hideUpdateToast();
        };
        document.getElementById('close').onclick = () => hideUpdateToast();
    };

    window.addEventListener('load', () => {

        let refreshing = false;
        navigator.serviceWorker.addEventListener('controllerchange', () => {
            if (refreshing) return;
            refreshing = true;
            window.location.reload();
        });

        navigator.serviceWorker.register('/sw.js').then(registration => {
            // A new service worker has been fetched, watch for state changes.
            //
            // This event is fired EVERY TIME a service worker is fetched and
            // succesfully parsed and goes into 'installing' state. This
            // happens, too, the very first time the page is visited, the very
            // first time a service worker is fetched for this page, when the
            // page doesn't have a controller, but in that case there's no new
            // version available and the notification must not appear.
            //
            // So, if the page doesn't have a controller, no notification shown.
            registration.addEventListener('updatefound', () => {
                // return;  // FIXME
                registration.installing.onstatechange = function () {  // No arrow function because 'this' is needed.
                    if (this.state == 'installed') {
                        if (!navigator.serviceWorker.controller) {
                            console.log('First install for this service worker.');
                        } else {
                            console.log('New service worker is ready to activate.');
                            showUpdateToast(this);
                        }
                    }
                };
            });

            // If a service worker is in 'waiting' state, then maybe the user
            // dismissed the notification when the service worker was in the
            // 'installing' state or maybe the 'updatefound' event was fired
            // before it could be listened, or something like that. Anyway, in
            // that case the notification has to be shown again.
            //
            if (registration.waiting) {
                console.log('New service worker is waiting.');
                // showUpdateToast(registration.waiting);

                // The above works, but this DOESN'T WORK.
                registration.waiting.postMessage('skipWaiting');
            }

        }).catch(error => {
            console.log('Service worker registration failed!');
            console.log(error);
        });
    });
}

为什么使用按钮的 onclick 事件进行间接调用可以正常工作,但是调用 postMessage() 却不行呢?

我真的很困惑,我敢打赌答案很简单,只是我太糊涂了看不出来。

提前非常感谢。


如果在那里添加一个超时,很明显是竞态条件,被注释的代码能够完美地工作... - Raúl Núñez de Arenas Coronado
1
如果您还没有使用 Workbox,请考虑使用它(需要一些时间来学习),但它可以很好地处理缓存、Service Worker 注册、更新和竞争条件等问题。Workbox 可以直接使用,无需额外配置。 - Robert Rowntree
1
与您之前在WB版本中提到的相同问题...可能对您有用。请参见以下链接:https://github.com/GoogleChrome/workbox/blob/8ffe56090f712e9b8bb6cff14e25f7ec090a1ce4/packages/workbox-core/src/skipWaiting.ts#L22-L26 - Robert Rowntree
Posnick在这里深入探讨了一些细节,值得一读:https://github.com/GoogleChrome/workbox/issues/2199#issuecomment-524335980 - Robert Rowntree
请问您能解释一下如何使用超时来解决这个问题吗?我遇到了相同的问题,我需要跳过等待以确保所有用户尽快获得新版本,并且这个问题给我们带来了很多麻烦! - Mehrnoosh
显示剩余4条评论
2个回答

2
我遇到了完全相同的问题。原来,如果在已安装的服务工作者上调用`skipWaiting`时,仍然存在一个待处理的网络请求,那么`skipWaiting`将永远不会解决。有趣的是,一旦请求完成,活动的服务工作者会重新加载,所以这显然是Chrome的一个bug。这也是为什么它只会偶尔发生的原因,以及为什么在页面加载时,设置一个超时或延迟请求`skipWaiting`的机会更大。
所以,在调用`skipWaiting`之前,你需要等待所有打开的网络请求完成。也许还有其他更有创意的方法,但我找到了两种方法来防止竞争条件的发生:
如果您的应用程序只有一个客户端,您可以对XMLHttpRequest进行猴子补丁,以跟踪打开的网络请求,并在所有请求解决之前暂停skipWaiting请求。
如果有多个客户端,您需要引入一个“关闭”程序,使活动的SW推迟所有新的fetch请求(在您的fetch处理程序中使用return false而不是respondWith),同时等待所有打开的请求解决,然后您可以安全地在已安装的SW中调用skipWaiting
这有点复杂,但这是我能够防止我的应用程序在Chrome中偶尔遇到此问题的唯一方法。

1
我接受了你的答案,因为尽管Chrome存在一个错误(据我所知,它仍未修复),但你提供的解决方法对我来说很合适(至少第二个)。非常感谢你的建议。虽然我现在不再开发那段代码,但在某个时候我会回头并尝试实施你的解决方案。真的非常感谢,Mario。 - undefined

2

看起来是Chromium或WebKit中的一个bug,因为这段代码在Firefox中始终有效,但在Chrome和Edge中大多数情况下失败。

我已经向Chromium报告了这个bug,让我们看看它是一个bug还是我的代码有问题。我已经成功构建了最小的可能复制该问题的代码,它非常小,可以在错误报告中找到。

报告可以在这里找到。

抱歉给您带来困扰,我会继续调查这个问题,但无论我如何尝试,代码仍然偶尔失败,我无法发现我的代码中的竞争条件。


1
请更新一下,如果你已经找到了答案。我遇到了同样的问题。 - Craig Howell
很遗憾,正如您从报告中所看到的,这个漏洞甚至还没有被分类。在我的PWA中,我所做的是在可以安装新服务工作者时积极地控制页面。但这是我自己的PWA才能拥有的奢侈品,其他的PWA可能不行...如果解决方案出现了(这是谷歌,不以修复漏洞而著称),我会更新我的回复。 - Raúl Núñez de Arenas Coronado
不用谢,但我什么也没做,这个漏洞仍未得到分类,我认为它永远不会被修复,而我提出的解决方案对于通用PWA来说过于激进。无论如何,感谢您的评论,Ariart :) - Raúl Núñez de Arenas Coronado

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接