Rework PWA to precache less files

Use network first strategy by default
Show the prompt after 3 days without reload
pull/2254/head
Jérémy Rialland 2022-08-25 17:57:19 +02:00 committed by Jérémy Rialland
parent d8acd234cb
commit f108a6d7d1
3 changed files with 87 additions and 43 deletions

View File

@ -1,4 +1,5 @@
import { useEffect } from 'react'
import { getItem, removeItem, setItem } from '@/storage/safeLocalStorage'
import { useState } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import styled from 'styled-components'
import { useRegisterSW } from 'virtual:pwa-register/react'
@ -36,35 +37,57 @@ const StyledHideButton = styled.div`
right: 0.375rem;
`
const pwaPromptDelayKey = 'update-pwa-prompt-delay'
export const ServiceWorker = () => {
const { t } = useTranslation()
const [showPrompt, setShowPrompt] = useState(false)
const {
offlineReady: [offlineReady, setOfflineReady],
needRefresh: [needRefresh, setNeedRefresh],
updateServiceWorker,
} = useRegisterSW({
immediate: true,
onRegistered: (r) => {
// eslint-disable-next-line no-console
console.log('=> SW Registered: ', r)
// If no service worker is waiting, delete the old prompt delay
if (!r?.waiting) {
removeItem(pwaPromptDelayKey)
}
},
onNeedRefresh() {
const promptDelay = parseInt(getItem(pwaPromptDelayKey) ?? '0')
// If we need a refresh and there is no prompt delay, create one in 3 days
if (promptDelay === 0) {
setItem(
pwaPromptDelayKey,
(Date.now() + 3 * 24 * 60 * 60 * 1000).toString()
)
}
// If we need a refresh and the prompt delay has passed, show the prompt
if (promptDelay > 0 && promptDelay < Date.now()) {
setShowPrompt(true)
}
},
onOfflineReady() {
// eslint-disable-next-line no-console
console.log('App is ready to work offline.')
},
onRegisterError: (error) => {
// eslint-disable-next-line no-console
console.log('SW registration error', error)
},
})
useEffect(() => {
if (offlineReady) {
setOfflineReady(false)
// eslint-disable-next-line no-console
console.log('App is ready to work offline.')
}
}, [offlineReady, setOfflineReady])
return (
<PromptContainer>
{needRefresh && (
{needRefresh && showPrompt && (
<StyledMessage type="info">
<StyledSmallBody>
<Trans>

View File

@ -1,14 +1,16 @@
import { ExpirationPlugin } from 'workbox-expiration'
import {
cleanupOutdatedCaches,
createHandlerBoundToURL,
precacheAndRoute,
} from 'workbox-precaching'
import { NavigationRoute, registerRoute, Route } from 'workbox-routing'
import { cleanupOutdatedCaches, precacheAndRoute } from 'workbox-precaching'
import { offlineFallback } from 'workbox-recipes'
import { registerRoute, Route, setDefaultHandler } from 'workbox-routing'
import { NetworkFirst, StaleWhileRevalidate } from 'workbox-strategies'
declare let self: ServiceWorkerGlobalScope
const HOUR = 60 * 60
const DAY = 24 * HOUR
const YEAR = 365 * DAY
const MONTH = YEAR / 12
self.addEventListener('message', (event) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (event.data && event.data.type === 'SKIP_WAITING') {
@ -18,23 +20,41 @@ self.addEventListener('message', (event) => {
cleanupOutdatedCaches()
precacheAndRoute(self.__WB_MANIFEST)
// Allow work offline
registerRoute(
new NavigationRoute(
createHandlerBoundToURL(
// Filter EN files on the FR app and vice versa
const precache = self.__WB_MANIFEST.filter(
(entry) =>
typeof entry !== 'string' &&
!entry.url.includes(
location.href.startsWith(import.meta.env.VITE_FR_BASE_URL)
? 'mon-entreprise.html'
: 'infrance.html'
),
{ denylist: [/^\/api\/.*/, /^\/twemoji\/.*/, /^\/dev\/storybook\/.*/] }
)
? 'infrance'
: 'mon-entreprise'
)
)
const HOUR = 60 * 60
const DAY = HOUR * 24
const YEAR = DAY * 365
precacheAndRoute(precache)
// Allow work offline
offlineFallback({
// When the user is offline and loads a page that is not cached, we return the French/English index file.
pageFallback: location.href.startsWith(import.meta.env.VITE_FR_BASE_URL)
? '/mon-entreprise.html'
: '/infrance.html',
})
// Set the default handler with network first so that every requests (css, js, html, image, etc.)
// goes through the service worker and will be cache
setDefaultHandler(
new NetworkFirst({
cacheName: 'default-network-first',
networkTimeoutSeconds: 1,
plugins: [
new ExpirationPlugin({
maxAgeSeconds: 3 * MONTH,
maxEntries: 40,
}),
],
})
)
const networkFirstJS = new Route(
({ sameOrigin, url }) => {
@ -44,11 +64,10 @@ const networkFirstJS = new Route(
cacheName: 'js-cache',
plugins: [
new ExpirationPlugin({
maxAgeSeconds: 30 * DAY,
maxAgeSeconds: 1 * MONTH,
maxEntries: 40,
}),
],
fetchOptions: {},
})
)
@ -58,11 +77,13 @@ const staleWhileRevalidate = new Route(
({ request, sameOrigin, url }) => {
return (
sameOrigin &&
(url.pathname.startsWith('/twemoji/') || request.destination === 'image')
(url.pathname.startsWith('/twemoji/') ||
request.destination === 'image' ||
request.destination === 'font')
)
},
new StaleWhileRevalidate({
cacheName: 'images',
cacheName: 'media',
plugins: [
new ExpirationPlugin({
maxAgeSeconds: 1 * YEAR,
@ -83,7 +104,7 @@ const networkFirstPolyfill = new Route(
cacheName: 'external-polyfill',
plugins: [
new ExpirationPlugin({
maxAgeSeconds: 1 * YEAR,
maxAgeSeconds: 3 * MONTH,
maxEntries: 5,
}),
],

View File

@ -1,7 +1,7 @@
import { Options } from 'vite-plugin-pwa'
export const pwaOptions: Partial<Options> = {
selfDestroying: true, // Unregister PWA
// selfDestroying: true, // Unregister PWA
registerType: 'prompt',
strategies: 'injectManifest',
srcDir: 'source',
@ -11,18 +11,18 @@ export const pwaOptions: Partial<Options> = {
manifestTransforms: [
(entries) => {
const manifest = entries.filter(
(entry) => !/assets\/.*(-legacy|lazy_)/.test(entry.url)
(entry) =>
!/assets\/.*(-legacy|lazy_)/.test(entry.url) &&
(entry.url.endsWith('.html')
? /(infrance|mon-entreprise)\.html/.test(entry.url)
: true)
)
return { manifest }
},
],
},
includeAssets: [
'logo-*.png',
'fonts/*.{woff,woff2}',
'références-images/*.{jpg,png,svg}',
],
includeAssets: ['logo-*.png'],
manifest: {
start_url: '/',
name: 'Mon entreprise',