Spe*_*les 3 ios reactjs progressive-web-apps
我正在构建一个ReactJs PWA,但在检测iOS上的更新时遇到了麻烦。
在Android上,一切工作正常,因此我想知道这是否与iOS对PWA的支持有关,或者我对Service Worker的实现不好。
到目前为止,这是我所做的:
构建过程和托管
我的应用程序是使用webpack构建的,并托管在AWS上。大多数文件(js / css)的名称均由其内容生成的哈希值构成。对于那些不是(应用清单,index.html,sw.js)的应用程序,我确保AWS为它们提供了一些Cache-Control标头,以防止任何缓存。一切都通过https提供。
服务人员
我尽可能简化了这一过程:除了为我的应用程序外壳程序添加precache之外,我没有添加任何缓存规则:
workbox.precaching.precacheAndRoute(self.__precacheManifest || []);
Run Code Online (Sandbox Code Playgroud)
服务人员注册
服务工作者的注册发生在主要的ReactJs App组件的componentDidMount()生命周期挂钩中:
componentDidMount() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
.then((reg) => {
reg.onupdatefound = () => {
this.newWorker = reg.installing;
this.newWorker.onstatechange = () => {
if (this.newWorker.state === 'installed') {
if (reg.active) {
// a version of the SW is already up and running
/*
code omitted: displays a snackbar to the user to manually trigger
activation of the new SW. This will be done by calling skipWaiting()
then reloading the page
*/
} else {
// first service worker registration, do nothing
}
}
};
};
});
}
}
Run Code Online (Sandbox Code Playgroud)
服务人员生命周期管理
根据Google关于服务人员的文档,导航到范围内的页面时应检测到服务人员的新版本。但是作为单页应用程序,加载该应用程序后不会发生任何困难的导航。
我为此找到的解决方法是挂接到react-router并侦听路由更改,然后手动要求注册的服务工作者进行自我更新:
const history = createBrowserHistory(); // from 'history' node package
history.listen(() => {
if ('serviceWorker' in navigator) {
navigator.serviceWorker
.getRegistration()
.then((reg) => {
if (!reg) {
return null;
}
reg.update();
});
}
});
Run Code Online (Sandbox Code Playgroud)
alert()在上面显示的代码中到处都是一堆,这就是我观察到的:
onupdatefound也从未触发过我的事件处理程序从Maximiliano Firtman的Medium上阅读这篇文章,似乎iOS 12.2为PWA带来了新的生命周期。据他介绍,当应用程序长时间闲置或在设备重新引导期间,应用程序状态以及页面都将被杀死。
我想知道这是否可能是造成我的问题的根本原因,但是到目前为止,我找不到任何遇到同样麻烦的人。
So after a lot of digging and investigation, I finally found out what was my problem.
From what I was able to observe, I think there is a little difference in the way Android and iOS handle PWAs lifecycle, as well as service workers.
On Android, when starting the app after a reboot, it looks like starting the app and searching an update of the service worker (thanks to the hard navigation occuring when reloading the page) are 2 tasks done in parallel. By doing that, the app have enough time to subscribe to the already existing service worker and define a onupdatefound() handler before the new version of the service worker is found.
On the other hand with iOS, it seems that when you start the app after a reboot of the device (or after not using it for a long period, see Medium article linked in the main topic), iOS triggers the search for an update before starting your app. And if an update is found, it will be installed and and enter its 'waiting' status before the app is actually started. This is probably what happens when the splashscreen is displayed...
So in the end, when your app finally starts and you subscribe to the already existing service worker to define your onupdatefound() handler, the update has already been installed and is waiting to take control of the clients.
So here is my final code to register the service worker :
componentDidMount() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
.then((reg) => {
if (reg.waiting) {
// a new version is already waiting to take control
this.newWorker = reg.waiting;
/*
code omitted: displays a snackbar to the user to manually trigger
activation of the new SW. This will be done by calling skipWaiting()
then reloading the page
*/
}
// handler for updates occuring while the app is running, either actively or in the background
reg.onupdatefound = () => {
this.newWorker = reg.installing;
this.newWorker.onstatechange = () => {
if (this.newWorker.state === 'installed') {
if (reg.active) {
// a version of the SW already has control over the app
/*
same code omitted
*/
} else {
// very first service worker registration, do nothing
}
}
};
};
});
}
}
Run Code Online (Sandbox Code Playgroud)
Note :
I also got rid of my listener on history that I used to trigger the search for an update on every route change, as it seemed overkill. Now I rely on the Page Visibility API to trigger this search every time the app gets the focus :
// this function is called in the service worker registration promise, providing the ServiceWorkerRegistration instance
const registerPwaOpeningHandler = (reg) => {
let hidden;
let visibilityChange;
if (typeof document.hidden !== 'undefined') { // Opera 12.10 and Firefox 18 and later support
hidden = 'hidden';
visibilityChange = 'visibilitychange';
} else if (typeof document.msHidden !== 'undefined') {
hidden = 'msHidden';
visibilityChange = 'msvisibilitychange';
} else if (typeof document.webkitHidden !== 'undefined') {
hidden = 'webkitHidden';
visibilityChange = 'webkitvisibilitychange';
}
window.document.addEventListener(visibilityChange, () => {
if (!document[hidden]) {
// manually force detection of a potential update when the pwa is opened
reg.update();
}
});
return reg;
};
Run Code Online (Sandbox Code Playgroud)