Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bust cache after a release? #33

Closed
scambier opened this issue Mar 4, 2021 · 87 comments
Closed

Bust cache after a release? #33

scambier opened this issue Mar 4, 2021 · 87 comments

Comments

@scambier
Copy link

scambier commented Mar 4, 2021

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.

@userquin
Copy link
Member

userquin commented Mar 4, 2021

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().

@scambier
Copy link
Author

scambier commented Mar 4, 2021

Thanks, that looks like a reasonable solution. I'll keep this ticket open until I implement this properly :)

@userquin
Copy link
Member

userquin commented Mar 4, 2021

@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.

@userquin
Copy link
Member

userquin commented Mar 5, 2021

@antfu I have a problem about using workbox-util, let me explain:

  • I'm trying use Workbox and messageSW to register service worker instead using navigation capabilities.
  • I need to do this: Offer a page reload for users

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

@antfu
Copy link
Member

antfu commented Mar 5, 2021

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?

@userquin
Copy link
Member

userquin commented Mar 5, 2021

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

@userquin
Copy link
Member

userquin commented Mar 5, 2021

this script will replace manual registration: what this plugin currently does will be replace with the script in Offer a page reload for users

@userquin
Copy link
Member

userquin commented Mar 5, 2021

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)...

@antfu
Copy link
Member

antfu commented Mar 5, 2021

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)

@userquin
Copy link
Member

userquin commented Mar 5, 2021

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.

@userquin
Copy link
Member

userquin commented Mar 5, 2021

As you can see the problem is mixing again buildtime and runtime, we need to interact with the ui to activated the service worker.

@userquin
Copy link
Member

userquin commented Mar 5, 2021

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).

@antfu
Copy link
Member

antfu commented Mar 5, 2021

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
  }
})

@userquin
Copy link
Member

userquin commented Mar 5, 2021

wait to @scambier response, I think it must work (if not using skipWaiting , clientsClaim and cleanupOutdatedCaches)...

@scambier
Copy link
Author

scambier commented Mar 5, 2021

@userquin

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:
...

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.

@userquin
Copy link
Member

userquin commented Mar 5, 2021

@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...

@userquin
Copy link
Member

userquin commented Mar 5, 2021

upps, add await keyword: await registration.update()

@userquin
Copy link
Member

userquin commented Mar 5, 2021

@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 })
      )
    )
  }
})

@userquin
Copy link
Member

userquin commented Mar 5, 2021

@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.

@userquin
Copy link
Member

userquin commented Mar 5, 2021

Following the code, it seems using Offer a page reload for users can simplify the logic on @vuepress/pwa: see lib directory.

@scambier
Copy link
Author

scambier commented Mar 5, 2021

@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 :)

@userquin
Copy link
Member

userquin commented Mar 5, 2021

vite.config.ts

VitePWA({
  injectRegister: null,
})

@userquin
Copy link
Member

userquin commented Mar 5, 2021

to clear the cache use this:

vite.config.ts

VitePWA({
  ...
  workbox: {
    cleanupOutdatedCaches: true,
  }
}),

@userquin
Copy link
Member

userquin commented Mar 5, 2021

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.

@userquin
Copy link
Member

userquin commented Mar 5, 2021

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...

imagen

@scambier
Copy link
Author

scambier commented Mar 5, 2021

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 :)

@userquin
Copy link
Member

userquin commented Mar 5, 2021

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...

@userquin
Copy link
Member

userquin commented Mar 5, 2021

@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,
  }
}

@userquin
Copy link
Member

userquin commented Mar 5, 2021

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,
  }
}

@userquin
Copy link
Member

userquin commented Mar 5, 2021

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
      }
})

@userquin
Copy link
Member

userquin commented Mar 8, 2021

@scambier

No, the issue is being resolved, @antfu has create PR #34 . Once merged on main it will be resolved also this issue (do not close it, github will close automatically).

Anyway, read service worker documentation, maybe your app cannot be used with a service worker, you need to review if matches your requisites.

For example, my MPA was originally to be 2 native apps to work on android and ios. The resources spent to do this compared to make it a PWA are significant. My decision was to make a PWA and so I need a service worker, it also needs to be able to work offline like natives ones. I have to include in my PWA server push notifications to avoid what you are describing...

If your server doesn't have push notifications, either, remove PWA or just make and alternate api entry point to test if there is a new version. For example, make some configuration, a timestamp an internal version number and store it on local storage. You can periodically check it to some api serving a plain number and simulate a refresh.

@userquin
Copy link
Member

userquin commented Mar 8, 2021

@scambier see this option included on the new version 15f5107

You can now include the service worker where you want, using this flag on the client side... See ReloadPrompt.vue on example/src directory.

@MartinMalinda
Copy link

MartinMalinda commented Mar 8, 2021

I tried to read through the discussion here and I'm afraid I couldn't really digest it all :d So a quick question:

Couldn't I just have a networkFirst strategy on the main index.html file? Or that is not allowed from the PWA point of view? The html file on / simply has to be delivered from cache no matter what?

If it was allowed it would basically allow the app to function offline if there's no internet connection, but otherwise latest index.html would be fetched from the server which then links latest js bundles. This would be much better UX than forcing the user to click a button.

@userquin
Copy link
Member

userquin commented Mar 8, 2021

@posva welcome to the hell: the main problem is bypass the browser cache and then the service worker cache: the new version will use StaleWhileRevalidate strategy, that means, go to cache and then in background go to server and then update if necessary.

If we go NetworkFirst strategy then we don't need the service worker, only for offline requirement. In some situations this will work but as a general solution to speed up with local cache then is not suitable.

As I mention above, you will need to check if the app requisites matches the usage of a service worker.

In fact, I have some apps that runs below 200ms using server push with esmodules for any page, so if I configure it with NetworkFirst strategy, the service worker will only be necessary if offline is required.

About the user click on the button, it is a general adoption, just see vuejs docs, vuetify docs, nuxt docs (vuepress)...

I'm talking with @antfu and have included a new auto option to bypass any user interaction.

@userquin
Copy link
Member

userquin commented Mar 8, 2021

@posva Creo que hablas español, te lo explico a ver si resuelve tus dudas.

El problema con los service workers es que tiene un ciclo de vida bastante entrerrevesado y por eso hay cosas que no encajan o no cuadran bien.

El principal problema está en el lapso de tiempo entre que un service worker está esperando a ser activado (skip waiting) y el momento en que se activa (activated). Esto que parece tan simple es un problema ya que provoca que no puedas hacer el bypass de la caché del mismo selectivamente.

Mientras un service worker no está activado, entonces es como si no existiese desde el punto de vista del aplicativo. El que incluyas una estrategia u otra no importa, porque lo que no tienes en cuenta es que quien realmente te está retornando lo que solicitas al backend es el que está activo.

Cuando has planteado la duda, has obviado este matiz, ya que aunque vayas al backend a por los datos y te retorne los últimos, te va a funcionar en ese instante, pero si haces un F5, te lo va a retornar el service worker activo, recuerda que todavía el nuevo no está activado, con lo que solicitará los assets con la versión anterior y lo que pasará es que se te quedará la pantalla en blanco y si miras la pestaña de red en el devtools, verás 404 de todos los recursos antiguos.

Como ves no es que falle en un punto o en otro, es un compendio de cosas, esta incidencia refleja lo que te he descrito y para llegar a ello he estado unos cuantos meses dándoles vueltas a cosas similares a tu planteamiento y que realmente no solucionaban nada.

Si estás un poco sorprendido, creo que hago mención a lo que hace @vuepress/pwa para poder sacar el Nuevo contenido disponible: básicamente espera en el build a que workbox-build termine y luego lo vuelve a modificar.

Si miras este comentario #34 (comment), se lo explico a Anthony, y aún así no consiguíamos hacer el bypass de la caché del navegador.

@posva

This comment has been minimized.

@userquin
Copy link
Member

userquin commented Mar 8, 2021

Leches pues ni me he dado cuenta, perdona Eduardo por las molestias, juraría que escribí el nombre bien al responder, habrá sido el autocomplete de las narices... como el predictivo del telefono

@MartinMalinda
Copy link

If we go NetworkFirst strategy then we don't need the service worker, only for offline requirement. In some situations this will work but as a general solution to speed up with local cache then is not suitable.

Yeah. That would be good enough for me. If I need to pick between fast boot from cache but with occasional manual app refresh via button click or a bit slower app boot (because if online, server is contacted) and keeping the main HTML file up to date automatically, I'd choose the second option.

I think the "networkFirst HTML" way still qualifies as PWA. If you generate proper bundles with hashes (as vite does) you can cache those nicely so that still makes the boot much quicker and if you go fully offline, your app still works.

I'm looking around and it seems some ppl seem to achieve this via workbox:
GoogleChrome/workbox#1767 (comment)

@MartinMalinda
Copy link

Nuxt seems to also go in this direction: nuxt-community/pwa-module#406

@userquin
Copy link
Member

userquin commented Mar 9, 2021

@MartinMalinda you can see the new PR #34 using workbox-window, you can see what we are configuring using vite virutal modules.

@userquin
Copy link
Member

userquin commented Mar 9, 2021

I forgot to mention that the current version is using workbox.runtimeCaching, you can see it on src/cache.ts, in #34 this file has been removed in favor of precaching via precacheAndRoute. The strategies used in the script are using NetworkFirst for html, and StaleWhileRevalidate for sw.js and workbox-**.js files (for .js files in general), but the problem still persists.

@userquin
Copy link
Member

userquin commented Mar 10, 2021

@MartinMalinda following your links, I think we have the same behavior described in GoogleChrome/workbox#1767 (comment)

What the new PR #34 generates is this:

  self.addEventListener('message', event => {
    if (event.data && event.data.type === 'SKIP_WAITING') {
      self.skipWaiting();
    }
  });
  /**
   * The precacheAndRoute() method efficiently caches and responds to
   * requests for URLs in the manifest.
   * See https://goo.gl/S9QRab
   */

  workbox.precacheAndRoute([{
    "url": "assets/index.40de6d3d.css",
    "revision": "0b973314e89c1b1876dfd49f6241cdde"
  }, {
    "url": "assets/index.946e44f0.js",
    "revision": "6c4f2bbf84081cacf649a67aa3f28b72"
  }, {
    "url": "assets/vendor.35599eb4.js",
    "revision": "57c102d464ad82af5c1c300d2de100a0"
  }, {
    "url": "index.html",
    "revision": "f4d735ef53aae1144d2a255bc7d8bf9b"
  }], {});
  workbox.cleanupOutdatedCaches();
  workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html")));

I will test if enabling skipWaiting and clientClaims on workbox entry of VitePWA has the effect you want: enabling this 2 flags then, workbox-build will replace:

 self.addEventListener('message', event => {
    if (event.data && event.data.type === 'SKIP_WAITING') {
      self.skipWaiting();
    }
  });

with:

  self.skipWaiting();
  workbox.clientsClaim();

I need to check if both are equivalent (the change and the link to GoogleChrome).
The downside with this approach will be we need to also add NetworkFirst to all entry pages when using SSG (for example with vitesse template).

@userquin
Copy link
Member

@MartinMalinda @antfu it seems to work, also with an F5, no user intertaction.

I have a pending thing to do, to check nested routes and other routes than / using vue-router: I need to include vue-router and adjust example.

@userquin
Copy link
Member

We have this:

imagen

Pressing F5 or pressing enter on url box (see the date, it is updated before the new sw is activated):

imagen

and with no user notification:

@userquin
Copy link
Member

@MartinMalinda I have on local the injectRegister: 'networkfirst' option almost ready, as simple as:

VitePWA({
  injectRegister: 'networkfirst',
  manifest: {
    ...
  },
})

this is what it generates on build (it needs a review, but it works: basically try call copyWorkboxLibraries after calling injectManifest to remove unnecessary workbox libs, see #35 (comment)):

// src/client/build/networkfirst.ts
importScripts("/workbox-v6.1.1/workbox-sw.js");
var debug = JSON.parse("false") === true;
var modulePathPrefix = "/workbox-v6.1.1/";
var useModulePathPrefix = modulePathPrefix.startsWith("/") ? modulePathPrefix.substring(1) : modulePathPrefix;
workbox.setConfig({debug, modulePathPrefix});
var modules = [
  "workbox-core",
  "workbox-routing",
  "workbox-cacheable-response",
  "workbox-strategies",
  "workbox-expiration"
].map((module) => {
  workbox.loadModule(module);
  return `${useModulePathPrefix}${module}`;
});
var cacheNames = workbox.core.cacheNames;
var {registerRoute, setCatchHandler, setDefaultHandler} = workbox.routing;
var {CacheableResponsePlugin} = workbox.cacheableResponse;
var {
  NetworkFirst,
  StaleWhileRevalidate,
  NetworkOnly
} = workbox.strategies;
var {ExpirationPlugin} = workbox.expiration;
var suffix = debug ? ".dev." : ".prod.";
var manifest = [{"revision":"233692e6a59a401d25babb470b42d0ba","url":"assets/[name].11eba18b.js"},{"revision":"0db45f676295b8a5367f17939bfecde5","url":"assets/about.9f61fe0e.js"},{"revision":"701ce0f558c7c79468284db9441d9485","url":"assets/home.ae1a8a75.js"},{"revision":"b32b3c35ce2176e5239f0f5958d3ed08","url":"assets/index.d8d0085a.js"},{"revision":"a760355bef2fd5ded385ccabf84c8de0","url":"assets/index.f3bb671a.css"},{"revision":"cb39939b6c451569e4fa7e86b2413cd8","url":"index.html"},{"revision":"1872c500de691dce40960bb85481de07","url":"registerSW.js"},{"revision":"a7cdb86a9f313bf7d91abf2997c876f8","url":"workbox-v6.1.1/workbox-background-sync.dev.js"},{"revision":"c37bef1035f38894236763f70f847b01","url":"workbox-v6.1.1/workbox-background-sync.prod.js"},{"revision":"b4834c087eaf8da50fcce69a663cbcc0","url":"workbox-v6.1.1/workbox-broadcast-update.dev.js"},{"revision":"9224c426031572af4cbde2db64b7bba4","url":"workbox-v6.1.1/workbox-broadcast-update.prod.js"},{"revision":"bd4298725d2b05c1a7313861ca43119e","url":"workbox-v6.1.1/workbox-cacheable-response.dev.js"},{"revision":"0f99a971609d97b2e235d6f27347cce2","url":"workbox-v6.1.1/workbox-cacheable-response.prod.js"},{"revision":"0ab1809eb6d8f9823adf893131514fc3","url":"workbox-v6.1.1/workbox-core.dev.js"},{"revision":"85c2be1a0e73006ce9e9d1d0cc889459","url":"workbox-v6.1.1/workbox-core.prod.js"},{"revision":"9fb6484c2667d9df08928b0f2f702c76","url":"workbox-v6.1.1/workbox-expiration.dev.js"},{"revision":"c353b8b02a9452019c2ddd9a76620b10","url":"workbox-v6.1.1/workbox-expiration.prod.js"},{"revision":"6f64a834420ad2f28aa8af74c4673bfc","url":"workbox-v6.1.1/workbox-navigation-preload.dev.js"},{"revision":"b455c3e2414561b90c2dac557cf6e87a","url":"workbox-v6.1.1/workbox-navigation-preload.prod.js"},{"revision":"56973179ef1b3124a2f4cc1046b86b75","url":"workbox-v6.1.1/workbox-offline-ga.dev.js"},{"revision":"0b44aa740a7029014d1356317892b96a","url":"workbox-v6.1.1/workbox-offline-ga.prod.js"},{"revision":"6ad698a436eaee2c23637f36102cff02","url":"workbox-v6.1.1/workbox-precaching.dev.js"},{"revision":"b7b24145fd52a89d6127b3310efab41c","url":"workbox-v6.1.1/workbox-precaching.prod.js"},{"revision":"ee1d732eaf6f444875cdecd34df6ed6a","url":"workbox-v6.1.1/workbox-range-requests.dev.js"},{"revision":"591c63c72daadc424e86793e77b92560","url":"workbox-v6.1.1/workbox-range-requests.prod.js"},{"revision":"34f18376df91157ec6ea489e28f945ce","url":"workbox-v6.1.1/workbox-recipes.dev.js"},{"revision":"922bc61480b51855016fb3fc39304398","url":"workbox-v6.1.1/workbox-recipes.prod.js"},{"revision":"fce9cb70987cdba7f37660722ac9f61f","url":"workbox-v6.1.1/workbox-routing.dev.js"},{"revision":"ba807b7a301d7556f34ae12f94b6044e","url":"workbox-v6.1.1/workbox-routing.prod.js"},{"revision":"7385f58adf6fa4ea3ed0de08651d79c8","url":"workbox-v6.1.1/workbox-strategies.dev.js"},{"revision":"ae4e3f5028e8192585bdd0f3d0ef33e5","url":"workbox-v6.1.1/workbox-strategies.prod.js"},{"revision":"8ab7d86a8d8439a2a95614054205a42c","url":"workbox-v6.1.1/workbox-streams.dev.js"},{"revision":"6aca7378ad212a06ce9a64a55dea76ae","url":"workbox-v6.1.1/workbox-streams.prod.js"},{"revision":"55fb6379e95be0790836c1c942f00bd0","url":"workbox-v6.1.1/workbox-sw.js"},{"revision":"534033888a2eb22884af6c83e01c340e","url":"workbox-v6.1.1/workbox-window.dev.umd.js"},{"revision":"140fbae39a000992dca61016d87a6d8f","url":"workbox-v6.1.1/workbox-window.prod.umd.js"}].filter((entry) => {
  return !entry.url || !entry.url.startsWith(useModulePathPrefix) || modules.some((module) => {
    return entry.url.contains(suffix);
  });
});
var cacheName = cacheNames.runtime;
var manifestURLs = [...manifest].map((entry) => {
  const url = new URL(entry.url, self.location);
  return url.href;
});
self.addEventListener("install", (event) => {
  event.waitUntil(caches.open(cacheName).then((cache) => {
    return cache.addAll(manifestURLs);
  }));
});
self.addEventListener("activate", (event) => {
  event.waitUntil(caches.delete(cacheNames.precache).then((result) => {
    if (result)
      console.log("Precached data removed");
    else
      console.log("No precache found");
  }));
});
self.addEventListener("activate", (event) => {
  event.waitUntil(caches.open(cacheName).then((cache) => {
    cache.keys().then((keys) => {
      keys.forEach((request) => {
        if (!manifestURLs.includes(request.url))
          cache.delete(request);
      });
    });
  }));
});
registerRoute(({url}) => manifestURLs.includes(url.href), new NetworkFirst({cacheName}));
setDefaultHandler(new NetworkOnly());
setCatchHandler(async ({event}) => {
  switch (event.request.destination) {
    case "document":
      return await caches.match("/index.html");
    default:
      return Promise.resolve(Response.error());
  }
});

@dheimoz
Copy link

dheimoz commented Mar 15, 2021

Awesome work!!!!!

@MartinMalinda
Copy link

@userquin that's awesome 🙇 I'll try to test it in upcoming days

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants