fix(v2): PWA issues + improve docs (#4377)

* debug pwa

* more debugging logs + attempt to upgrade workbox

* fix PWA ?

* fix PWA ?

* enable pwa debugging for deploy previews

* try to fix the  app installed issue?

* try to fix appinstalled not firing

* try to add related applications to the PWA manifest

* attempt to fix related_applications

* attempt to fix related_applications

* attempt to fix related_applications

* improve PWA strategies

* improve PWA doc

* refactor/cleanup registerSw

* cleanup
This commit is contained in:
Sébastien Lorber 2021-03-10 20:00:42 +01:00 committed by GitHub
parent efbd8fa351
commit 02cd5d343b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 396 additions and 229 deletions

View file

@ -10,7 +10,11 @@ const path = require('path');
const DEFAULT_OPTIONS = {
debug: false,
offlineModeActivationStrategies: ['appInstalled', 'queryString'],
offlineModeActivationStrategies: [
'appInstalled',
'queryString',
'standalone',
],
injectManifestConfig: {},
pwaHead: [],
swCustom: undefined,
@ -23,7 +27,14 @@ exports.PluginOptionSchema = Joi.object({
offlineModeActivationStrategies: Joi.array()
.items(
Joi.string()
.valid('appInstalled', 'queryString', 'mobile', 'saveData', 'always')
.valid(
'appInstalled',
'queryString',
'standalone',
'mobile',
'saveData',
'always',
)
.required(),
)
.default(DEFAULT_OPTIONS.offlineModeActivationStrategies),

View file

@ -16,40 +16,91 @@ const PWA_DEBUG = process.env.PWA_DEBUG;
const debug = PWA_DEBUG; // shortcut
const MAX_MOBILE_WIDTH = 940;
const APP_INSTALLED_EVENT_FIRED_KEY = 'docusaurus.pwa.event.appInstalled.fired';
async function clearRegistrations() {
const registrations = await navigator.serviceWorker.getRegistrations();
if (debug && registrations.length > 0) {
if (debug) {
console.log(
`[Docusaurus-PWA][registerSw]: unregister service workers`,
`[Docusaurus-PWA][registerSw]: will unregister all service worker registrations`,
registrations,
);
}
await Promise.all(
registrations.map(async (registration) => {
const result = await registration?.registration?.unregister();
if (debug) {
console.log(
`[Docusaurus-PWA][registerSw]: unregister() service worker registration`,
registrations,
result,
);
}
}),
);
if (debug) {
console.log(
`[Docusaurus-PWA][registerSw]: unregistered all service worker registrations`,
registrations,
);
}
registrations.forEach((registration) => {
registration.registration.unregister();
});
window.location.reload();
}
const MAX_MOBILE_WIDTH = 940;
const APP_INSTALLED_KEY = 'docusaurus.pwa.appInstalled';
/*
As of 2021:
It is complicated and not very reliable to detect an app is actually installed.
https://stackoverflow.com/questions/51735869/check-if-user-has-already-installed-pwa-to-homescreen-on-chrome
- appinstalled event is not in the spec anymore and seems to not fire? https://firt.dev/pwa-2021#less-capabilities-%E2%98%B9%EF%B8%8F
- getInstalledRelatedApps() is only supported in recent Chrome and does not seem to reliable either https://github.com/WICG/get-installed-related-apps
- display-mode: standalone is not exactly the same concept, but looks like a decent fallback https://petelepage.com/blog/2019/07/is-my-pwa-installed/
*/
async function isAppInstalledEventFired() {
return localStorage.getItem(APP_INSTALLED_EVENT_FIRED_KEY) === 'true';
}
async function isAppInstalledRelatedApps() {
if ('getInstalledRelatedApps' in window.navigator) {
const relatedApps = await navigator.getInstalledRelatedApps();
return relatedApps.some((app) => app.platform === 'webapp');
}
return false;
}
function isStandaloneDisplayMode() {
return window.matchMedia('(display-mode: standalone)').matches;
}
const OfflineModeActivationStrategiesImplementations = {
always: () => true,
mobile: () => window.innerWidth <= MAX_MOBILE_WIDTH,
saveData: () => !!(navigator.connection && navigator.connection.saveData),
appInstalled: () => !!localStorage.getItem(APP_INSTALLED_KEY),
appInstalled: async () => {
const installedEventFired = await isAppInstalledEventFired();
const installedRelatedApps = await isAppInstalledRelatedApps();
return installedEventFired || installedRelatedApps;
},
standalone: () => isStandaloneDisplayMode(),
queryString: () =>
new URLSearchParams(window.location.search).get('offlineMode') === 'true',
};
function isOfflineModeEnabled() {
const activeStrategies = PWA_OFFLINE_MODE_ACTIVATION_STRATEGIES.filter(
(strategyName) => {
const strategyImpl =
OfflineModeActivationStrategiesImplementations[strategyName];
return strategyImpl();
},
async function isStrategyActive(strategyName) {
return OfflineModeActivationStrategiesImplementations[strategyName]();
}
async function getActiveStrategies() {
const activeStrategies = await Promise.all(
PWA_OFFLINE_MODE_ACTIVATION_STRATEGIES.map(async (strategyName) => {
const isActive = await isStrategyActive(strategyName);
return isActive ? strategyName : undefined;
}),
);
return activeStrategies.filter(Boolean); // remove undefined values
}
async function isOfflineModeEnabled() {
const activeStrategies = await getActiveStrategies();
const enabled = activeStrategies.length > 0;
if (debug) {
const logObject = {
@ -85,127 +136,164 @@ function createServiceWorkerUrl(params) {
return url;
}
(async () => {
if (typeof window === 'undefined') {
return;
}
async function registerSW() {
const {Workbox} = await import('workbox-window');
const offlineMode = await isOfflineModeEnabled();
const url = createServiceWorkerUrl({offlineMode, debug});
const wb = new Workbox(url);
const registration = await wb.register();
const sendSkipWaiting = () => wb.messageSW({type: 'SKIP_WAITING'});
const handleServiceWorkerWaiting = async () => {
if (debug) {
console.log('[Docusaurus-PWA][registerSw]: handleServiceWorkerWaiting');
}
// Immediately load new service worker when files aren't cached
if (!offlineMode) {
sendSkipWaiting();
} else if (PWA_RELOAD_POPUP) {
const renderReloadPopup = (await import('./renderReloadPopup')).default;
await renderReloadPopup({
onReload() {
wb.addEventListener('controlling', () => {
window.location.reload();
});
sendSkipWaiting();
},
});
}
};
if (debug) {
console.log('[Docusaurus-PWA][registerSw]: debug mode enabled');
if (registration.active) {
console.log(
'[Docusaurus-PWA][registerSw]: registration.active',
registration,
);
}
if (registration.installing) {
console.log(
'[Docusaurus-PWA][registerSw]: registration.installing',
registration,
);
}
if (registration.waiting) {
console.log(
'[Docusaurus-PWA][registerSw]: registration.waiting',
registration,
);
}
}
if ('serviceWorker' in navigator) {
const {Workbox} = await import('workbox-window');
const offlineMode = isOfflineModeEnabled();
const url = createServiceWorkerUrl({offlineMode, debug});
const wb = new Workbox(url);
const registration = await wb.register();
const sendSkipWaiting = () => wb.messageSW({type: 'SKIP_WAITING'});
const handleServiceWorkerWaiting = async () => {
if (debug) {
console.log('[Docusaurus-PWA][registerSw]: handleServiceWorkerWaiting');
}
// Immediately load new service worker when files aren't cached
if (!offlineMode) {
sendSkipWaiting();
} else if (PWA_RELOAD_POPUP) {
const renderReloadPopup = (await import('./renderReloadPopup')).default;
renderReloadPopup({
onReload() {
wb.addEventListener('controlling', () => {
window.location.reload();
});
sendSkipWaiting();
},
});
}
};
// Update the current service worker when the next one has finished
// installing and transitions to waiting state.
wb.addEventListener('waiting', (event) => {
if (debug) {
if (registration.active) {
console.log(
'[Docusaurus-PWA][registerSw]: registration.active',
registration,
);
}
if (registration.installing) {
console.log(
'[Docusaurus-PWA][registerSw]: registration.installing',
registration,
);
}
if (registration.waiting) {
console.log(
'[Docusaurus-PWA][registerSw]: registration.waiting',
registration,
);
}
console.log('[Docusaurus-PWA][registerSw]: event waiting', event);
}
handleServiceWorkerWaiting();
});
// Update current service worker if the next one finishes installing and
// moves to waiting state in another tab.
wb.addEventListener('externalwaiting', (event) => {
if (debug) {
console.log('[Docusaurus-PWA][registerSw]: event externalwaiting', event);
}
handleServiceWorkerWaiting();
});
// Update service worker if the next one is already in the waiting state.
// This happens when the user doesn't click on `reload` in the popup.
if (registration.waiting) {
await handleServiceWorkerWaiting();
}
}
// TODO these events still works in chrome but have been removed from the spec in 2019!
// See https://github.com/w3c/manifest/pull/836
function addLegacyAppInstalledEventsListeners() {
if (typeof window !== 'undefined') {
if (debug) {
console.log(
'[Docusaurus-PWA][registerSw]: addLegacyAppInstalledEventsListeners',
);
}
// Update service worker if the next one is already in the waiting state.
// This happens when the user doesn't click on `reload` in the popup.
if (registration.waiting) {
handleServiceWorkerWaiting();
}
// Update the current service worker when the next one has finished
// installing and transitions to waiting state.
wb.addEventListener('waiting', (event) => {
if (debug) {
console.log('[Docusaurus-PWA][registerSw]: event waiting', event);
}
handleServiceWorkerWaiting();
});
// Update current service worker if the next one finishes installing and
// moves to waiting state in another tab.
wb.addEventListener('externalwaiting', (event) => {
if (debug) {
console.log(
'[Docusaurus-PWA][registerSw]: event externalwaiting',
event,
);
}
handleServiceWorkerWaiting();
});
window.addEventListener('appinstalled', (event) => {
window.addEventListener('appinstalled', async (event) => {
if (debug) {
console.log('[Docusaurus-PWA][registerSw]: event appinstalled', event);
}
localStorage.setItem(APP_INSTALLED_KEY, true);
localStorage.setItem(APP_INSTALLED_EVENT_FIRED_KEY, 'true');
if (debug) {
console.log(
"[Docusaurus-PWA][registerSw]: localStorage.setItem(APP_INSTALLED_EVENT_FIRED_KEY, 'true');",
);
}
// After the app is installed, we register a service worker with the path
// `/sw?enabled`. Since the previous service worker was `/sw`, it'll be
// treated as a new one. The previous registration will need to be
// cleared, otherwise the reload popup will show.
clearRegistrations();
await clearRegistrations();
});
window.addEventListener('beforeinstallprompt', (event) => {
// TODO this event still works in chrome but has been removed from the spec in 2019!!!
window.addEventListener('beforeinstallprompt', async (event) => {
if (debug) {
console.log('[Docusaurus-PWA][registerSw]: event appinstalled', event);
console.log(
'[Docusaurus-PWA][registerSw]: event beforeinstallprompt',
event,
);
}
// TODO instead of default browser install UI, show custom docusaurus prompt?
// event.preventDefault();
if (localStorage.getItem(APP_INSTALLED_KEY)) {
localStorage.removeItem(APP_INSTALLED_KEY);
if (debug) {
console.log(
'[Docusaurus-PWA][registerSw]: localStorage.getItem(APP_INSTALLED_EVENT_FIRED_KEY)',
localStorage.getItem(APP_INSTALLED_EVENT_FIRED_KEY),
);
}
if (localStorage.getItem(APP_INSTALLED_EVENT_FIRED_KEY)) {
localStorage.removeItem(APP_INSTALLED_EVENT_FIRED_KEY);
if (debug) {
console.log(
'[Docusaurus-PWA][registerSw]: localStorage.removeItem(APP_INSTALLED_EVENT_FIRED_KEY)',
);
}
// After uninstalling the app, if the user doesn't clear all data, then
// the previous service worker will continue serving cached files. We
// need to clear registrations and reload, otherwise the popup will show.
clearRegistrations();
await clearRegistrations();
}
});
} else if (debug) {
console.log(
'[Docusaurus-PWA][registerSw]: browser does not support service workers',
);
if (debug) {
console.log(
'[Docusaurus-PWA][registerSw]: legacy appinstalled and beforeinstallprompt event listeners installed',
);
}
}
})();
}
/*
Init code to run on the client!
*/
if (typeof window !== 'undefined') {
if (debug) {
console.log('[Docusaurus-PWA][registerSw]: debug mode enabled');
}
if ('serviceWorker' in navigator) {
// First: add the listeners asap/synchronously
addLegacyAppInstalledEventsListeners();
// Then try to register the SW using lazy/dynamic imports
registerSW().catch((e) => console.error('registerSW failed', e));
}
}

View file

@ -71,20 +71,37 @@ function getPossibleURLs(url) {
const params = parseSwParams();
const precacheManifest = self.__WB_MANIFEST;
const controller = new PrecacheController();
const controller = new PrecacheController({
fallbackToNetwork: true, // safer to turn this true?
});
if (params.offlineMode) {
controller.addToCacheList(precacheManifest);
if (params.debug) {
console.log('[Docusaurus-PWA][SW]: addToCacheList', {
precacheManifest,
});
}
}
await runSWCustomCode(params);
self.addEventListener('install', (event) => {
event.waitUntil(controller.install());
if (params.debug) {
console.log('[Docusaurus-PWA][SW]: install event', {
event,
});
}
event.waitUntil(controller.install(event));
});
self.addEventListener('activate', (event) => {
event.waitUntil(controller.activate());
if (params.debug) {
console.log('[Docusaurus-PWA][SW]: activate event', {
event,
});
}
event.waitUntil(controller.activate(event));
});
self.addEventListener('fetch', async (event) => {
@ -95,15 +112,17 @@ function getPossibleURLs(url) {
const possibleURL = possibleURLs[i];
const cacheKey = controller.getCacheKeyForURL(possibleURL);
if (cacheKey) {
const cachedResponse = caches.match(cacheKey);
if (params.debug) {
console.log('[Docusaurus-PWA][SW]: serving cached asset', {
requestURL,
possibleURL,
possibleURLs,
cacheKey,
cachedResponse,
});
}
event.respondWith(caches.match(cacheKey));
event.respondWith(cachedResponse);
break;
}
}
@ -111,6 +130,12 @@ function getPossibleURLs(url) {
});
self.addEventListener('message', async (event) => {
if (params.debug) {
console.log('[Docusaurus-PWA][SW]: message event', {
event,
});
}
const type = event.data && event.data.type;
if (type === 'SKIP_WAITING') {