Closed scambier closed 3 years ago
Hi,
There is a pending think to do about refresh on new content detected.
You can use register-service-worker, and do a manual installation of service worker via VitePWA
configuration on vite.config.ts
file, something like this:
useServiceWorker.ts
import type { Ref } from 'vue'
import { register } from 'register-service-worker'
import { ref } from 'vue'
// https://medium.com/google-developer-experts/workbox-4-implementing-refresh-to-update-version-flow-using-the-workbox-window-module-41284967e79c
// https://github.com/yyx990803/register-service-worker
// https://developers.google.com/web/fundamentals/primers/service-workers/high-performance-loading
export const useServiceWorker = (): {
appNeedsRefresh: Ref<boolean>
offlineAppReady: Ref<boolean>
} => {
const offlineAppReady = ref(false)
const appNeedsRefresh = ref(false)
register(
'/sw.js',
{
registrationOptions: { scope: '/' },
updated: async() => { appNeedsRefresh.value = true },
cached: () => { offlineAppReady.value = true },
},
)
return {
appNeedsRefresh,
offlineAppReady,
}
}
then on yourmain.ts
file (entry point), just import it and then watch appNeedsRefresh
and include:
const {
appNeedsRefresh,
offlineAppReady,
} = useServiceWorker()
watch(appNeedsRefresh, async() => {
window.location.reload()
}, { immediate: true })
obviously, you will need to add some button on the screen, then show/enable it using appNeedsRefresh
and then add an @click
listener to this button to call window.location.reload()
.
Thanks, that looks like a reasonable solution. I'll keep this ticket open until I implement this properly :)
@antfu As soon I have time I'll try to fix it using workbox instead register-service-worker, I'm very busy at work...
In the meantime this is the approach I'm using on my projects with vitesse template.
@antfu I have a problem about using workbox-util
, let me explain:
Workbox
and messageSW
to register service worker instead using navigation capabilities.Then, the problem arises, I need to include workbox-util
as a dependency
not as a dev dependency
: the problem is not here, is on the target project, for example, vitesse template
where this plugin is installed as a dev dependency
, and so, when building...
To put you in context:
My first attempt was to configure my pwa like this (vite.config.ts
):
VitePWA({
...
workbox: {
cleanupOutdatedCaches: true,
skipWaiting: true,
clientsClaim: true,
}
}),
...
This aproach has a problem, the pages remains on the cache, also after a refresh and my pages stop working, because I removed the old registered service worker (cleanupOutdatedCaches
): refresing page with F5 in online mode, request are served from the cache, so the oldest assets are missing from my server and the page stop working.
My second attemp was just removing cleanupOutdatedCaches
, this will at least keep my pages working, but with oldest assets!!!.
The problem with activating skipWaiting
is that the pages remains with the original assets links, and there is no way no bypass it.
The solution described in Offer a page reload for users seems to work, but I haven't test it yet.
In this link you can find the problem and how can be solved, in fact it points to the Offer a page reload for users, just read it.
We can also find a warning in workbox using skipWaiting
here
Thanks a lot for the detailed info!
Offer a page reload for users
This looks like a good solution to me! We can make an option for people to opt-in with this behavior.
cleanupOutdatedCaches & skipWaiting
Do we need these tho?
No, just to remove both and do it manually with the Offer a page reload for users
script: the script (module) included there is what we need to create here
this script will replace manual registration: what this plugin currently does will be replace with the script in Offer a page reload for users
What I had in mind was to include useServiceWorker
in @vueuse
using workbox-util
and include the logic described in Offer a page reload for users
with a callback: createUIPrompt
would be an option to useServiceWorker
.
And then, here is the problem again (mixing build time and runtime)...
I think maybe we don't need to be Vue specific (this plugin is framework-agnostic).
We could update this https://github.com/antfu/vite-plugin-pwa/blob/c2054640b1a81650a8172e1fe1dadb28178e4957/src/html.ts#L9-L16 to put the snippet from Offer a page reload for users
and ask users to install workbox-window
if they want to have this feature.
After that, we can provide a minimal example of how to set it up for people to explore. (and Vitesse as well for sure)
The problem with this approach is that is not tied to the app ui, that is, we need to add a callback, for example, with my first approach using register-service-worker
using appNeedsRefresh
.
I think it would be nice to add the useServiceWorker
here with the logic, exposing this 2 guys (appNeedsRefresh
and offlineAppReady
, and expose some wrapper callback to be called from the ui once the user click on the refresh option.
As you can see the problem is mixing again buildtime and runtime, we need to interact with the ui to activated the service worker.
My suggestion is to include a new custom
option and generate useServiceWorker.ts
, but where to put it?. Then instruct/ask the user to include workbox-build
/workbox-window
as dependency and show how to use it (something similar in my response to @scambier).
I am thinking about we can serve the register script in a virtual module, so the usage would be like this, where they can be bundled with their UI logics
// virtual module
import registerSW from 'vite-plugin-pwa-register'
const sw = registerSW({
onNeedRefresh() {
// show an UI for user to refresh
}
})
wait to @scambier response, I think it must work (if not using skipWaiting
, clientsClaim
and cleanupOutdatedCaches
)...
@userquin
You can use register-service-worker, and do a manual installation of service worker via
VitePWA
configuration onvite.config.ts
file, something like this: ...
The code you proposed actually doesn't solve my issue it seems. In the end it just triggers a location.reload()
, but a simple refresh isn't enough. Even a ctrl+f5
with the cache disabled in the network tab doesn't work, I have to unregister the service worker to finally get the latest files.
@scambier yes, this is the problem I have, but with my SPA acting as a MPA (my server build route pages instead just returning/forwarding to index.html), and I havenn't tested on a real SPA, your answer confirms my suspicions...
can you try this one (just change the callback)?:
updated: async(registration) => {
registration.update()
appNeedsRefresh.value = true
},
@antfu try to make a branch where we can test it before merging into master, the problem is I haven't time...
upps, add await
keyword: await registration.update()
@scambier the problem is that the service worker is installed but not activated, we need to provide a callback to the service worker to control the opened pages/tabs: the lifecycle of the service worker is a little complicated.
What I'm talking with @antfu is to try to activate the service worker once is updated, but we need to change the code.
For example, vuejs 3 (using workbox 4, here we are using 6.1.1), has some code, I'll try to investigate, to activate and claim clients to take control of opened windows/tabs controlled by the service worker (I need to see where attaching the listener to show the dialog for New content available
):
from https://v3.vuejs.org/ => at the top of the service-worker.js file
self.addEventListener('message', (event) => {
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting();
}
});
from https://v3.vuejs.org/ => at the bottom of the service-worker.js file
addEventListener('message', event => {
const replyPort = event.ports[0]
const message = event.data
if (replyPort && message && message.type === 'skip-waiting') {
event.waitUntil(
self.skipWaiting().then(
() => replyPort.postMessage({ error: null }),
error => replyPort.postMessage({ error })
)
)
}
})
@antfu vuejs next docs is using @vuepress/pwa
and here is the code generating the service-worker.js
file (similiar to this one but touching also the app entry).
It uses a global registered component to show the dialog on new content available.
Following the code, it seems using Offer a page reload for users
can simplify the logic on @vuepress/pwa
: see lib directory.
@scambier yes, this is the problem I have, but with my SPA acting as a MPA (my server build route pages instead just returning/forwarding to index.html), and I havenn't tested on a real SPA, your answer confirms my suspicions...
can you try this one (just change the callback)?:
updated: async(registration) => { registration.update() appNeedsRefresh.value = true },
@antfu try to make a branch where we can test it before merging into master, the problem is I haven't time...
This seems to do the job.
Here's what I have at the moment:
useServiceWorker.ts
register(
'/sw.js',
{
registrationOptions: { scope: '/' },
updated: async (registration) => {
await registration.update()
registration.unregister()
appNeedsRefresh.value = true
},
cached: () => {
offlineAppReady.value = true
}
}
)
main.ts
const { appNeedsRefresh, offlineAppReady } = useServiceWorker()
watch(appNeedsRefresh, async (val) => {
if (val) {
console.log('app updated and sw unregistered, will refresh')
// TODO: prompt user
location.reload()
}
}, { immediate: true })
There's just a point I'm missing. In your first response, you wrote
do a manual installation of service worker via VitePWA configuration
But I'm not quite sure which setting to change.
Edit: also, I guess unregistering the worker before showing the prompt isn't really a good idea, but I'll change that later. For now I just need to properly clean the cache after an update :)
vite.config.ts
VitePWA({
injectRegister: null,
})
to clear the cache use this:
vite.config.ts
VitePWA({
...
workbox: {
cleanupOutdatedCaches: true,
}
}),
if you don't have injectRegister: null
you're registering the service worker twice, former in the html and second one in main.ts
.
Setting injectRegister: null
will just remove the registration of the sw from the index.html file (see current generated index.html and you will see navigator.serviceWorker.register
or a script tag pointing to registerSW.js
).
And please, confirm that update call just works or adding unregister.
Testing on my MPA it seems we need to unregister first, then update and then refresh page, but still service worker not controlling page/tabs.
About the cache @scambier see screenshot below.
I'm checking if clientsClain: true,
will do the work...
@antfu With this approach there is no need to modify code, just wait until my test can confirm that works...
Ok, the app correctly cleans the cache after a new deployment, but only if I refresh or open a new tab. And in that case, it only reloads the first tab. The updated: async (registration) => {}
never triggers by itself after a new deployment.
To recap, here's what I currently have.
useServiceWorker.ts:
import type { Ref } from 'vue'
import { register } from 'register-service-worker'
import { ref } from 'vue'
// https://medium.com/google-developer-experts/workbox-4-implementing-refresh-to-update-version-flow-using-the-workbox-window-module-41284967e79c
// https://github.com/yyx990803/register-service-worker
// https://developers.google.com/web/fundamentals/primers/service-workers/high-performance-loading
export const useServiceWorker = (): {
appNeedsRefresh: Ref<boolean>
offlineAppReady: Ref<boolean>
} => {
const offlineAppReady = ref(false)
const appNeedsRefresh = ref(false)
register(
'/sw.js',
{
registrationOptions: { scope: '/' },
updated: async (registration) => {
await registration.update()
registration.unregister()
appNeedsRefresh.value = true
},
cached: () => {
offlineAppReady.value = true
}
}
)
return {
appNeedsRefresh,
offlineAppReady
}
}
main.ts (after createApp(App)
):
const { appNeedsRefresh } = useServiceWorker()
watch(appNeedsRefresh, async (val) => {
console.log('Does app need refresh? ' + val)
if (val) {
// if (confirm('App updated. Do you want to refresh?')) {
location.reload()
// }
}
}, { immediate: true })
PWA config in vite.config.ts:
VitePWA({
injectRegister: null,
manifest: {
/* */
},
workbox: {
cleanupOutdatedCaches: true
}
})
Thanks a lot for your explanations. I'll take time to read some more doc about service workers next week :)
I'm trying to simulate Workbox approach, just copy/paste workbox-window
into my MPA: if working I'll provide the virtual module to be used...
@antfu confirmed that works at least on my MPA with the /
route, below the virtual module.
Just update the imports for workbox-window
, remove the import for useRouter
and remove the router.isReady()
callback and remove all unnecesary comments.
The usage is calling updateServiceWorker
instead reload the window once the user click on the UI once appNeedsRefresh
is activated.
import type { Ref } from 'vue'
import { Workbox } from '/~/logics/service-worker/Workbox'
import { messageSW } from '/~/logics/service-worker/messageSW'
import { ref } from 'vue'
import { useRouter } from 'vue-router'
export const useServiceWorker = (immediate = false): {
offlineAppReady: Ref<boolean>
appNeedsRefresh: Ref<boolean>
updateServiceWorker: () => Promise<void>
} => {
const offlineAppReady = ref(false)
const appNeedsRefresh = ref(false)
const router = useRouter()
let registration: ServiceWorkerRegistration
let wb: Workbox
const updateServiceWorker = async() => {
// Assuming the user accepted the update, set up a listener
// that will reload the page as soon as the previously waiting
// service worker has taken control.
wb.addEventListener('controlling', (event) => {
if (event.isUpdate)
window.location.reload()
})
if (registration && registration.waiting) {
// Send a message to the waiting service worker,
// instructing it to activate.
// Note: for this to work, you have to add a message
// listener in your service worker. See below.
await messageSW(registration.waiting, { type: 'SKIP_WAITING' })
}
}
router.isReady().then(() => {
if ('serviceWorker' in navigator) {
wb = new Workbox('/sw.js', { scope: '/' })
const showSkipWaitingPrompt = () => {
// `event.wasWaitingBeforeRegister` will be false if this is
// the first time the updated service worker is waiting.
// When `event.wasWaitingBeforeRegister` is true, a previously
// updated service worker is still waiting.
// You may want to customize the UI prompt accordingly.
// Assumes your app has some sort of prompt UI element
// that a user can either accept or reject.
appNeedsRefresh.value = true
}
wb.addEventListener('controlling', (event) => {
if (!event.isUpdate)
offlineAppReady.value = true
})
// Add an event listener to detect when the registered
// service worker has installed but is waiting to activate.
wb.addEventListener('waiting', showSkipWaitingPrompt)
// @ts-ignore
wb.addEventListener('externalwaiting', showSkipWaitingPrompt)
wb.register({ immediate }).then(r => registration = r!)
}
})
return {
offlineAppReady,
appNeedsRefresh,
updateServiceWorker,
}
}
THIS WILL NOT WORK, SO FORGET IT: seems we need to register controlling event to the right registration (this approach will register on old sw) SO USE MODULE FROM PREVIOUS COMMENT.
after a revision:
import type { Ref } from 'vue'
import { Workbox } from '/~/logics/service-worker/Workbox'
import { messageSW } from '/~/logics/service-worker/messageSW'
import { ref } from 'vue'
import { useRouter } from 'vue-router'
export const useServiceWorker = (immediate = false): {
offlineAppReady: Ref<boolean>
appNeedsRefresh: Ref<boolean>
updateServiceWorker: () => Promise<void>
} => {
const offlineAppReady = ref(false)
const appNeedsRefresh = ref(false)
const router = useRouter()
let registration: ServiceWorkerRegistration
const updateServiceWorker = async() => {
if (registration && registration.waiting) {
// Send a message to the waiting service worker,
// instructing it to activate.
// Note: for this to work, you have to add a message
// listener in your service worker. See below.
await messageSW(registration.waiting, { type: 'SKIP_WAITING' })
}
}
router.isReady().then(() => {
if ('serviceWorker' in navigator) {
const wb = new Workbox('/sw.js', { scope: '/' })
const showSkipWaitingPrompt = () => {
// `event.wasWaitingBeforeRegister` will be false if this is
// the first time the updated service worker is waiting.
// When `event.wasWaitingBeforeRegister` is true, a previously
// updated service worker is still waiting.
// You may want to customize the UI prompt accordingly.
// Assumes your app has some sort of prompt UI element
// that a user can either accept or reject.
appNeedsRefresh.value = true
}
wb.addEventListener('controlling', (event) => {
// Assuming the user accepted the update, set up a listener
// that will reload the page as soon as the previously waiting
// service worker has taken control.
if (event.isUpdate)
window.location.reload()
else
offlineAppReady.value = true
})
// Add an event listener to detect when the registered
// service worker has installed but is waiting to activate.
wb.addEventListener('waiting', showSkipWaitingPrompt)
// @ts-ignore
wb.addEventListener('externalwaiting', showSkipWaitingPrompt)
wb.register({ immediate }).then(r => registration = r!)
}
})
return {
offlineAppReady,
appNeedsRefresh,
updateServiceWorker,
}
}
also working on my MPA, just configuring templatesURL
on workbox
including all dynamic routes generated on server side.
The configuration for VitePWA
will be as @scambier reported:
VitePWA({
injectRegister: null,
manifest: {
/* */
},
workbox: {
cleanupOutdatedCaches: true
}
})
@antfu I attach here the copy/paste
files from workbox 6.0 that are the same for 6.1.1 if you want to play with it before starting...
@scambier can you just download the zip from previous comment, unzip it on your src/logics
directory (create it if does not exist) and change the logic to import this new one useServiceWorker
calling updateServiceWorker
method from the button?
If you are using router
just leave it otherwise remove the import and remove the router.isReady()
callback.
I upload here new service-worker, second approach doesn't work, we need to register on right registration!!!
@antfu ping
Thanks for looking into it. But I am afraid if we need that complicated approach, and wish we are not coupled with Vue. Maybe I will take some time to give a shot later.
This is an example for my tests. We can provide some virtual module abstracting callbacks and workbox things.
Try to see logic on vuepress....
El dom., 7 mar. 2021 6:43, Anthony Fu notifications@github.com escribió:
Thanks for looking into it. But I am afraid if we need that complicated approach, and wish we are not coupled with Vue. Maybe I will take some time to give a shot later.
— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/antfu/vite-plugin-pwa/issues/33#issuecomment-792221848, or unsubscribe https://github.com/notifications/unsubscribe-auth/ABQEZTYDFTPLW7LTE2JL4RTTCMHA7ANCNFSM4YTIY7EQ .
The problem is that if you dont provide this, the pwa just stop working, see the title of this issue...
One last thing to change about the ready to work offline message
, we need to listen to activated
event instead controlling
one:
wb.addEventListener('activated', (event) => {
// this will only controls the offline request.
// `event.isUpdate` will be true if another version of the service
// worker was controlling the page when this version was registered.
if (!event.isUpdate)
offlineAppReady.value = true
})
A proporsal will be somethig like this:
export type RegisterSWOptions = {
scriptURL?: string
scope?: string
immediate?: boolean
onNeedRefresh: () => void
onOfflineReady?: () => void
}
export type UpdateServiceWorkerFn = () => Promise<void>
export declare function registerSW(registerSWOptions: RegisterSWOptions): UpdateServiceWorkerFn
the implementation will be something like this:
import { Workbox, messageSW } from 'workbox-window'
export const registerSW = (registerSWOptions: RegisterSWOptions): UpdateServiceWorkerFn => {
let registration: ServiceWorkerRegistration
let wb: Workbox
const updateServiceWorker = async() => {
// Assuming the user accepted the update, set up a listener
// that will reload the page as soon as the previously waiting
// service worker has taken control.
wb && wb.addEventListener('controlling', (event) => {
if (event.isUpdate)
window.location.reload()
})
if (registration && registration.waiting) {
// Send a message to the waiting service worker,
// instructing it to activate.
// Note: for this to work, you have to add a message
// listener in your service worker. See below.
await messageSW(registration.waiting, { type: 'SKIP_WAITING' })
}
}
if ('serviceWorker' in navigator) {
wb = new Workbox(registerSWOptions.scriptURL || '/sw.js', { scope: registerSWOptions.scope || '/' })
const showSkipWaitingPrompt = () => {
// `event.wasWaitingBeforeRegister` will be false if this is
// the first time the updated service worker is waiting.
// When `event.wasWaitingBeforeRegister` is true, a previously
// updated service worker is still waiting.
// You may want to customize the UI prompt accordingly.
// Assumes your app has some sort of prompt UI element
// that a user can either accept or reject.
registerSWOptions.onNeedRefresh()
}
wb.addEventListener('activated', (event) => {
// this will only controls the offline request.
// `event.isUpdate` will be true if another version of the service
// worker was controlling the page when this version was registered.
if (!event.isUpdate && registerSWOptions.onOfflineReady)
registerSWOptions.onOfflineReady()
})
// Add an event listener to detect when the registered
// service worker has installed but is waiting to activate.
wb.addEventListener('waiting', showSkipWaitingPrompt)
// @ts-ignore
wb.addEventListener('externalwaiting', showSkipWaitingPrompt)
// register the service worker
wb.register({ immediate: registerSWOptions.immediate || false }).then(r => registration = r!)
}
return updateServiceWorker
}
And so, for vue
(usage) we can have something like this:
import registerSW from 'vite-plugin-pwa-register'
export const useServiceWorker = (immediate = false, scriptURL = '/sw.js', scope = '/'): {
offlineAppReady: Ref<boolean>
appNeedsRefresh: Ref<boolean>
updateServiceWorker: () => Promise<void>
} => {
const offlineAppReady = ref(false)
const appNeedsRefresh = ref(false)
const updateServiceWorker = registerSW({
scriptURL,
scope,
immediate,
onNeedRefresh: () => {
appNeedsRefresh.value = true
},
onOfflineReady: () => {
offlineAppReady.value = true
},
})
return {
offlineAppReady,
appNeedsRefresh,
updateServiceWorker,
}
}
@antfu we need that complicated approach just because of lifecycle of the service worker , there is no another way to do it...
If you look at @vuepress/pwa
, that is scary: modify the script generated by workbox-build, also modifying the ui and register listeners ...
Wow @userquin, that's exactly what I was thinking about. Great work!
In the addition of
import registerSW from 'vite-plugin-pwa-register'
When we could have something like
import { useServiceWorker } from 'vite-plugin-pwa-register/vue'
import { useServiceWorker } from 'vite-plugin-pwa-register/react'
as the extensions.
It only remains to make it nice and unify the types a little
I started to make the virtual module in the plugin, but I mess with the workspaces, and how to structure everything within the project, although I think it should be outside
@userquin Just sent you the invitation to this project. Appreciated all you have done on this topic!
Please feel free to create a new branch and draft a PR. Let me know if you have any other questions, I am happy to review and help with the discussion and implementation! Thanks
@antfu a few questions:
1) How to start it? Make a new folder for virtual module. Where to put dependencies? 2) How about extensions? I don't have any idea how to begin 3) About react I haven't used it, only vue and some little things with svelte.
Maybe you can add a new branch where I can start (clone it), at least with the structure created...
Re: 1, serving virtual module is actually quite simple, you can see the docs here: https://vitejs.dev/guide/api-plugin.html#importing-a-virtual-file
Re: 2, they are just like other virtual modules
Re: 3, no pressure on that, we can leave them for the community to contribute, or if you want, you can do the vue part for sure.
@antfu I have a few problems with virtual modules, I cannot use typescript, just plain javascript.
I'm testing example module and I'll make an initial push to my fork.
I have gotten it to at least compile:
example App.vue
<script lang="ts">
import { defineComponent, ref } from 'vue'
import { registerSW } from 'vite-plugin-pwa-register'
export default defineComponent({
setup() {
const offlineAppReady = ref(false)
const appNeedsRefresh = ref(false)
const { updateServiceWorker } = registerSW({
immediate: false,
onNeedRefresh: () => {
appNeedsRefresh.value = true
},
onOfflineReady: () => {
offlineAppReady.value = true
},
})
return {
offlineAppReady,
appNeedsRefresh,
updateServiceWorker,
}
},
})
</script>
<template>
<div v-if="offlineAppReady">Ready to work offline</div>
<div v-if="appNeedsRefresh">
New content available
<button @click="updateServiceWorker">Refresh</button>
</div>
<div>Hello World</div>
</template>
GENERATED virtual module
import { Workbox, messageSW } from 'workbox-window'
export function registerSW(
immediate,
onNeedRefresh,
onOfflineReady,
) {
let registration
let wb
function updateServiceWorker() {
// Assuming the user accepted the update, set up a listener
// that will reload the page as soon as the previously waiting
// service worker has taken control.
wb && wb.addEventListener('controlling', function(event) {
if (event.isUpdate)
window.location.reload()
})
if (registration && registration.waiting) {
// Send a message to the waiting service worker,
// instructing it to activate.
// Note: for this to work, you have to add a message
// listener in your service worker. See below.
messageSW(registration.waiting, { type: 'SKIP_WAITING' })
}
}
if ('serviceWorker' in navigator) {
wb = new Workbox('/sw.js', { scope: '/' })
wb.addEventListener('activated', function(event) {
// this will only controls the offline request.
// event.isUpdate will be true if another version of the service
// worker was controlling the page when this version was registered.
if (!event.isUpdate && typeof onOfflineReady === 'function')
onOfflineReady()
})
// Add an event listener to detect when the registered
// service worker has installed but is waiting to activate.
wb.addEventListener('waiting', onNeedRefresh)
// @ts-ignore
wb.addEventListener('externalwaiting', onNeedRefresh)
// register the service worker
wb.register({ immediate }).then(r => registration = r)
}
return updateServiceWorker
}
Great, looking forward to it! Yeah, you should use plain js + .d.ts to make it work. We can still write them in ts and transpile them into js in the dist to serve. If you don't get what I mean, you can leave the ts version in the source code and I can take care of the build setup
ok, later I'll try to have both variants, one commented, just coment/remove what you want.
For you info:
1) added modules.ts, just to create the virtual module(s): currently only plain supported (we need to add vue and react: I'll try to add former)
2) modify index.ts to include virtualFileId
and added resolveId
and load
hooks to revolve virtual modules
3) App.vue
: just for testing
4) added register
option on injectRegister
option
5) added workbox-window
as dependency
can you tell me your timezone? I'm from Spain, GMT + 1
UTC+8, and it's 2 AM for me now :P
I am going to have some sleep now, will get back to you later.
@antfu tested on example project and first push to my branch: https://github.com/userquin/vite-plugin-pwa
Changes made:
1) added src/modules.ts
: virtual modules with javascript (typescript on a second round). Also a first version to generate vite-plugin-pwa-register/vue
virtual module.
2) modified src/types.ts
to include register
as another option for injectRegister
.
3) modified src/index.ts
to include virtualFileId
and added resolveId
and load
hooks to revolve virtual modules.
4) added workbox-window
as dependency to package.json
. Only generated when injectRegister: 'register'
. We need to review current options.
5) modified example/vite.config.ts
to remove outdated caches and changed injectRegister
to register
: just added workbox: { cleanupOutdatedCaches: true }
. Also modified base
entry to allow bypass hardcoded base: 'https://github.com/'
and added sourcemap
to allow see source on local tests: build: { sourcemap: process.env.SOURCE_MAP === 'true', }
(see next entry, crossenv passed on script).
6) modified example/package.json
to include node-static
dependency to do local tests and added run-build
script entry to build + serve via node-static
example: I have added some crossenv
variables to allow test on my local ip, just read it.
7) modified package.json
: added example:run-build
script to allow build + run example
: pnpm example:run-build
just build the example and run it on node via example/test-pwa-with-node-static.js
(see 9).
8) modified example/src/App.vue
: just for testing and with vite-plugin-pwa-register
. A real example => how to configure it.
9) added example/test-pwa-with-node-static.js
to test on example
build with node-static
.
10) .gitignore
to exclude certificates and .env.local.json
: see 2) bellow.
Anyway, go to my fork and just compare it with this to see changes I have made.
If you want to test it, you will need:
1) an ssl certificate, for example, use tls-keygen
, then put stuff on example
directory.
2) add example/.env.local.json
file: see .env.local.json
and .env.local.json.content
images bellow . You will need to configure node-static
ssl stuff and optionally you can configure hostname
, https port
and http port
(default to localhost
, 443
and 80
respectivelly). Just see example/test-pwa-with-node-static.js
script.
3) for first test (app ready to work offline
, see app-ready-offline image
bellow) you only need to execute pnpm example:run-build
, open a browser with dev tools opened.
4) to test New content available
, stop node
script, then go to example/src/Vue.app
and change return 'App ready to work offline'
with return 'Application ready to work offline'
, then run again pnpm example:run-build
and go to page on browser opened on previous step, click on navigation bar and just press enter, wait a few seconds, until refresh icon appears, and then press on refresh button (see new content available image
bellow).
.env.local.json image
.env.local.json.content image
Here some screenshots:
app-ready-offline image
first test assets image
first test assets directory image
new content available image
new content available assets image
new content available assets dir image
@scambier @antfu here an animated gif to see the problem described in this issue solved with the new virtual module.
Hello, I'm not well versed in PWA settings, and your plugin works really well as a 0-config tool (thanks for that).
However, I'm faced with a cache issue when I'm redeploying my app. In that case, I had to rename several api endpoints on my backend, but once the Vue app was built and deployed, all I got was 404 errors because I was still served the app from the cache. Since it's still in dev I simply manually unregistered the worker, but I what to do if that happens in production?
If I understand correctly, even if all my .js files and assets have a versioned hash in their name, as long as the index.html file is cached, it will still serve the old assets.
Is there a configuration I can setup (maybe it's still WIP on your side) to fix this, or if applicable, is there something you recommend? I found several solutions for this issue, but nothing looks really clean or fail-proof.