Domenic
Domenic

Reputation: 738

Angular PWA with custom offline page

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.

Upvotes: 2

Views: 3429

Answers (2)

Domenic
Domenic

Reputation: 738

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');

Upvotes: 3

Nikola Stekovic
Nikola Stekovic

Reputation: 635

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"  
    ],

Upvotes: 0

Related Questions