3

Usually whenever I read a blog post about PWA's, the tutorial seems to just precache every single asset. But this seems to go against the app shell pattern a bit, which as I understand is: Cache the bare necessities (only the app shell), and runtime cache as you go. (Please correct me if I understood this incorrectly)

Imagine I have this single page application, it's a simple index.html with a web component: <my-app>. That <my-app> component sets up some routes which looks a little bit like this, I'm using Vaadin router and web components, but I imagine the problem would be the same using React with React Router or something similar.

router.setRoutes([
    {
        path: '/',
        component: 'app-main', // statically loaded
    },
    {
    path: '/posts',
        component: 'app-posts',
        action: () => { import('./app-posts.js');} // dynamically loaded
    },
    /* many, many, many more routes */
    {
        path: '/offline', // redirect here when a resource is not cached and failed to get from network
        component: 'app-offline', // also statically loaded
    }
]);

My app may have many many routes, and may get very large. I don't want to precache all those resources straight away, but only cache the stuff I absolutely need, so in this case: my index.html, my-app.js, app-main.js, and app-offline.js. I want to cache app-posts.js at runtime, when it's requested.

Setting up runtime caching is simple enough, but my problem arises when my user visits one of the potentially many many routes that is not cached yet (because maybe the user hasn't visited that route before, so the js file may not have loaded/cached yet), and the user has no internet connection.

What I want to happen, in that case (when a route is not cached yet and there is no network), is for the user to be redirected to the /offline route, which is handled by my client side router. I could easily do something like: import('./app-posts.js').catch(() => /* redirect user to /offline */), but I'm wondering if there is a way to achieve this from workbox itself.

So in a nutshell: When a js file hasn't been cached yet, and the user has no network, and so the request for the file fails: let workbox redirect the page to the /offline route.

Harry Theo
  • 784
  • 1
  • 8
  • 28
Pazzle
  • 376
  • 6
  • 16
  • have you looked at setCatchHandler? https://developers.google.com/web/tools/workbox/reference-docs/latest/workbox.routing#.setCatchHandler and https://developers.google.com/web/tools/workbox/guides/advanced-recipes#provide_a_fallback_response_to_a_route – James South Oct 07 '19 at 22:45
  • I have seen that, but then I'd still have to just return a response right? What Id like to do is redirect the page to `/offline` from my service worker. Or is there a way to do that that im missing? – Pazzle Oct 07 '19 at 23:11
  • I have only set catch handlers for images, but the code example in the second link returns a fallback html url, I haven't tried that but it looks like it could work. – James South Oct 08 '19 at 06:22
  • That doesn't really solve it though, my question is: when a request to this javascript file fails, can I, from the service worker, somehow make the client go to the `/offline` route? I guess I could return a (precached) `404.js` that just navigates to the `/offline` route, but it seems hacky. I'm wondering if theres a better/nicer way to handle this – Pazzle Oct 08 '19 at 14:31
  • Did you see this question? https://stackoverflow.com/questions/53503761/whats-the-right-way-to-implement-offline-fallback-with-workbox?rq=1 – James South Oct 08 '19 at 21:00

1 Answers1

4

Option 1 (not always useful): As far as I can see and according to this answer, you cannot open a new window or change the URL of the browser from within the service worker. However you can open a new window only if the clients.openWindow() function is called from within the notificationclick event.

Option 2 (hardest): You could use the WindowClient.navigate method within the activate event of the service worker however is a bit trickier as you still need to check if the file requested exists in the cache or not.

Option 3 (easiest & hackiest): Otherwise, you could respond with a new Request object to the offline page:

const cacheOnly = new workbox.strategies.CacheOnly();
const networkFirst = new workbox.strategies.NetworkFirst();

workbox.routing.registerRoute(
  /\/posts.|\/articles/, 
  async args => {
    const offlineRequest = new Request('/offline.html');

    try {
      const response = await networkFirst.handle(args);
      return response || await cacheOnly.handle({request: offlineRequest});

    } catch (error) {
      return await cacheOnly.handle({request: offlineRequest})

    }
  }
);

and then rewrite the URL of the browser in your offline.html file:

 <head>
    <script>
        window.history.replaceState({}, 'You are offline', '/offline');
    </script>
 </head>

The above logic in Option 3 will respond to the requested URL by using the network first. If the network is not available will fallback to the cache and even if the request is not found in the cache, will fetch the offline.html file instead. Once the offline.html file is parsed, the browser URL will be replaced to /offline.

Harry Theo
  • 784
  • 1
  • 8
  • 28
  • This looks really promising! When I tried this, I'm getting a `The FetchEvent for "http://localhost:1117/app-posts-7f1dba0f.js" resulted in a network error response: an object that was not a Response was passed to respondWith().` error though, I tried a couple of things, but I can't seem to make it work... any ideas? (p.s. thanks for the spelling corrections :)) – Pazzle Oct 09 '19 at 17:06
  • I was originally wrong about using `clients.openWindow` because it can only be used in some cases. I have updated the answer with more options. – Harry Theo Oct 10 '19 at 11:17