2

In an Angular (8) app I'd like to add a custom offline page (just a plain simple html-file to begin with). I have set up my app as a PWA (using @angular/pwa and configured everything so that it at least works smoothly while being online).

However, I've had a hard time making updates available for PWA users. So, after many hours of try and error I decided to exclude index.html from the ngsw-config.json. This has -of course- the effect that index.html gets loaded every single time (not so bad, 'cause it's so small). If there are any updates index.html links to different JS-files and these files get loaded immediately. So, as I said before, the PWA works just as I like it to be.

Now I want to display an offline.html when the user starts the PWA being offline. So I've add offline.html to ngsw-config.json and I've created a custom Service Worker including the official ngsw-worker.js:

importScripts('./ngsw-worker.js');

I'm also using this custom service worker instead of the official one:

ServiceWorkerModule.register('./custom-worker.js', { enabled: true, registrationStrategy: registrationStrategy })

So far, everything still works as expected. Behavior is just like before. Now I wanted to include the offline behavior in my custom worker:

importScripts('./ngsw-worker.js');
self.addEventListener('fetch', function(event) {
    return event.respondWith(
      caches.match(event.request)
      .then(function(response) {
        let requestToCache = event.request.clone();

        return fetch(requestToCache).then().catch(error => {
          // Check if the user is offline first and is trying to navigate to a web page
          if (event.request.method === 'GET' && event.request.headers.get('accept').includes('text/html')) {
            // Return the offline page
            return caches.match("offline.html");
          }
        });
      })
    );
  });

This script comes from: https://stackoverflow.com/a/56738140/4653997 Unfortunately this is the part that doesn't work at all. For now I'm pretty much stuck. I have no idea what do next. I thought service workers get executed whether index.html can get loaded or not.

Any help would be appreciated.

Domenic
  • 708
  • 1
  • 9
  • 23
  • if i got it right, you want user to have offline page before it gets to index.html right? If you check that linked answer you'll notice part where it says `So when user are in offline mode and user try to navigate to another route they will see offline page`. Would you like to have it initially? If that is the case, you can get have it through APP_INITIALIZER injection token, i can write you down short example of it. – Nikola Stekovic Sep 30 '19 at 07:32
  • Yeah, I'd like to have `offline.html` served initially, but only if the user can't access `index.html` – Domenic Sep 30 '19 at 08:03

2 Answers2

3

I've got it working!

In the end it was a relatively simple fetch event listener I had to add to my custom service worker:

// listen to every fetch event
self.addEventListener('fetch', function (event) {
    const request = event.request;
    
    // filter for html document fetches (should only result in one single fetch) --> index.html
    if (request.method === "GET" && request.destination === "document") {

        // only intercept if there was a problem fetching index.html
        event.respondWith(
            fetch(request).catch(function (error) {
                console.error("[onfetch] Failed. Serving cached offline fallback", error);

                // return offline page from cache instead
                return caches.match("/assets/offline.html");
            }));
    }
});

// use all the magic of the Angular Service Worker
importScripts('./ngsw-worker.js');
Domenic
  • 708
  • 1
  • 9
  • 23
0

index.html and .js will get cached with service workers. @angular/pwa will do that for you so it will init angular even when offline. You can use that as advantage to check if user is online before it boots up app so you can use APP_INITIALIZER for it.

First register function that is going to be called on app init as following:

import { NgModule, APP_INITIALIZER } from '@angular/core'; 
import { AppLoadService } from './app-load.service';

export function init_app(appLoadService: AppLoadService) {
    return () => appLoadService.initializeApp();
}

@NgModule({
  imports: [HttpClientModule],
  providers: [
    AppLoadService,
    { provide: APP_INITIALIZER, useFactory: init_app, deps: [AppLoadService], multi: true },
  ]
})

This service is the one that is initially called and will contain method that response to app init. I just added window.location.href as sample here but you can do anything in that if where it checks if browser is online or not.

export class AppLoadModule { }

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Injectable()
export class AppLoadService {

  constructor(private httpClient: HttpClient) { }

  initializeApp(): Promise<any> {
    return new Promise((resolve, reject) => {
                if (!window.navigator.onLine) {
                  window.location.href = offlineURL; // this would probably be something like 'yourURL/assets/offline-page.html'
                }
                resolve();
    });
  }
}

Note that you'll still need to have offline page in your resources ng-sw.config so you can access it when sw caches it

"resources": {
    "files": [
        ...
        "/assets/offline-page.html"  
    ],
Nikola Stekovic
  • 635
  • 3
  • 13
  • No! `index.html` won't get cached! See my initial question. I need a solution where no index.html gets executed at all – Domenic Sep 30 '19 at 08:30
  • tbh, see no reason for doing this `So, after many hours of try and error I decided to exclude index.html from the ngsw-config.json`. How do you think sw can be executed without loading eg. sw.js? It needs to be executed somehow and called from some .html page. – Nikola Stekovic Sep 30 '19 at 08:33
  • I'm still very new to service workers. But the way I see it the service worker is already installed and gets executed whether `index.html` is available or not. To proof that I went offline and looked at the Service Workers Diagnostic page in Chrome DevTools (Application tab). There I can see the status of my Service Worker. It says "activated and running" even though I'm offline... – Domenic Sep 30 '19 at 08:49
  • Needless to say for this behavior to be true the app must have been executed successfully before... (so index.html and sw.js must have been executed before, but not in this session) – Domenic Sep 30 '19 at 08:52