mirror of
https://github.com/facebook/docusaurus.git
synced 2025-06-05 20:32:42 +02:00
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:
parent
efbd8fa351
commit
02cd5d343b
8 changed files with 396 additions and 229 deletions
|
@ -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),
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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') {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue