In the update

Tip: try to use Chrome incognito mode when testing the application to ensure that the Service Worker does not read data from the previous residual state!!

PWA initialization on the Angular 6 project:

After sudo ng new pwa creates a new project, run sudo ng add @angular/pwa in the root directory of the project, and the Service Worker files, manifest. json files, and icon files of various sizes will be automatically added. Angular PWA Chinese portal

PWA program update

Import {SwUpdate} from ‘@angular/service-worker’ in app.component.ts; To load the update module of SW, every time the PWA program has an update, you can use the SwUpdate module here to get the update, and use the following code to achieve the update operation of the program:

export class AppComponent {
  update: boolean;
  constructor(updates: SwUpdate, private data: DataService) {
    updates.available.subscribe( event= > {
      this.update = true;
      updates.activateUpdate().then((a)= >
        document.location.reload() ); }); } title ='PWA';
}
Copy the code

SwUpdate Document portal

Then use an *ngIf in the HTML to determine whether it is updated or not (if it is, text is displayed, if it is not) :

<span *ngIf="update">There's an update associated with your progressive web application!</span>
Copy the code

Build the production program again each time you update the program, run sudo ng build –prod in the root directory, then go to CD Dist /PWA, and finally run http-server-o to run the updated program on the server.

Because ng serve does not work with Service workers, you must test the project locally with a separate HTTP server. You can use any HTTP server; I used the HTTP-server package from NPM. You can also customize ports to prevent port collisions:

http-server -p 8080 -c-1 dist/<project-name>
Copy the code

I had a problem with ERR_INVALID_REDIRECT when I started the server using http-server but couldn’t open the web page properly. Change the HTTP-server version to solve this problem: NPM install -g [email protected].

Note: If you want to update the PWA regularly, that is, to create a periodic polling method using interval, you need to make the process of the application registering the Aervice worker enter a stable state first, and then let it start the polling process. If you keep polling updates (such as calling interval()), the application will be prevented from reaching the stable state. ServiceWorker scripts will never be registered in the browser. Also: the various polling performed in the application prevents it from reaching a stable state

constructor(appRef: ApplicationRef, updates: SwUpdate) {
        // Allow the app to stabilize first, before starting polling for updates with `interval()`.
        const appIsStable$ = appRef.isStable.pipe(first(isStable= > isStable === true));
        const everySixHours$ = interval(6 * 60 * 60 * 1000);
        const everySixHoursOnceAppIsStable$ = concat(appIsStable$, everySixHours$);
        everySixHoursOnceAppIsStable$.subscribe((a)= > updates.checkForUpdate());
  }
Copy the code

So a summary of the use of the auto-update module:

constructor(appRef: ApplicationRef, updates: SwUpdate, private data: DataService) {
    const appIsStable$ = appRef.isStable.pipe(first(isStable= > isStable === true));
    const everySixHours$ = interval(6 * 1000);
    const everySixHoursOnceAppIsStable$ = concat(appIsStable$, everySixHours$);
    everySixHoursOnceAppIsStable$.subscribe((a)= > {
      updates.checkForUpdate();
      // console.log('check update in Service Worker');
    });
    updates.available.subscribe(event= > {
      console.log('gotta new version here', event.available);
      updates.activateUpdate().then((a)= > document.location.reload());
    });
  }
Copy the code

Check the updated version every 6 seconds if there is no updates.activateUpdate().then(() => document.location.reload()); It simply reminds not to refresh and update the program when a new version is detected. When testing, you need to re-run the HTTP server ng build –prod and http-server -p 8080-C-1 dist/PWA. You will see a notification of the new version on the console on the original page.

When a new build of the application is published, the Service Worker treats it as a new version of the application, determined by the contents of the ngsw.json file, which contains hashes of all known content. If any cached file changes, the file’s hash changes in ngsw.json, causing the Angular Service Worker to treat the active file collection as a new version.)

How to cache files and API addresses and other items?

Json file. For example, if you want to cache Google Montserrat fonts and API addresses, all the code in this file is in glob format, i.e. :

  • ‘**’ matches 0 to multisegment paths
  • ‘*’ matches zero or more characters other than /
  • ? Matches a character other than /
  • ! The prefix indicates that the pattern is inverse, meaning that only files that do not match the pattern are included

Such as:

  • /**/*.html Specifies all HTML files
  • /*.html specifies only the HTML file in the root directory
  • ! /**/*.map excludes all source mapping files

Do this in real code:

<link href="https://fonts.googleapis.com/css?family=Montserrat" rel="stylesheet">
Copy the code

Add to the created assetGroups:

"urls": [
    "https://fonts.googleapis.com/**"
  ]
Copy the code

AssetGroup follows TypeScript interface rules:

interface AssetGroup {
  name: string; installMode? :'prefetch' | 'lazy';
  // Prefetch tells the Angular Service Worker to fetch each listed resource when caching the current version of the app. This is a bandwidth-intensive pattern, but ensures that these resources are available when requested, even when the browser is offline
  // Lazy does not pre-cache any resources. In contrast, the Angular Service Worker only caches resources that it receives requests for. This is an on-demand caching pattern. Resources that are never requested are never cached. This is useful for resources like images that are served at different resolutions, so that the Service Worker only caches the right resources for a particular screen and device orientation.updateMode? :'prefetch' | 'lazy';
  // Prefetch tells the Service Worker to download and cache the updated resource immediately
  Lazy tells the Service Worker not to cache these resources, but to treat them as unrequested and wait until they are requested again to update them.The lazy updateMode is valid only if installMode is also lazy. resources: { files? :string[];
    /** @deprecated As of v6 `versionedFiles` and `files` options have the same behavior. Use `files` instead. */versionedFiles? :string[]; urls? :string[];
  };
}
Copy the code

Create the dataGroups cache API address below:

"dataGroups": [
    {
      "name": "jokes-api",
      "urls": [
        "https://api.chucknorris.io/jokes/random"
      ],
      "cacheConfig": {
        "strategy": "freshness",
        "maxSize": 20,
        "maxAge": "1h",
        "timeout": "5s"
      }
    }
  ]
Copy the code

The dataGroups configuration follows the following interface:

export interface DataGroup {
  name: string;
  urls: string[]; version? :number;
  cacheConfig: {
    maxSize: number;
    maxAge: string; timeout? :string; strategy? :'freshness' | 'performance';
  };
}
Copy the code

The items in the cache setup are:

  • “strategy” :
    1. Performance, the default, optimized for quick response. If a resource exists in the cache, this cached version is used. It allows resources to be somewhat stale (depending on maxAge) in exchange for better performance. This works for resources that do not change often, such as user avatars.
    2. Freshness optimizes for data immediacy, preferentially fetching requested data from the network. Only when the network times out will the request fall back into the cache according to timeout. This is useful for resources that change frequently, such as account balances.
  • “MaxSize” : (required) maximum number of entries or responses in the cache. The open cache can grow indefinitely and eventually exceed the storage quota. It is recommended to clear it timely.
  • “MaxAge” : d (required) The maxAge parameter indicates how long a response is allowed to remain in the cache before it is cleared due to invalidation. (D: days, h: hours, M: minutes, S: seconds, u: microseconds)
  • “Timeout” : The duration string is used to specify the network timeout. If configured, the Angular Service Worker waits for a network response before using the cache, which is the network timeout.

PWA Push Notification

The functionality of the test Push Notification API cannot be tested in stealth mode

  1. First generate VAPID (Voluntary Application Server Identification), using Node’s WebPush library to introduce dependencies directly:npm install web-push -gThen create the VAPID Key:web-push generate-vapid-keys --json. Get a VAPID similar to the following:
{
    "publicKey":"BApAO10ISTLAR1bWho_6f4yL5-5z2RWHgnkqzG7SB81WdcsLkDdxrc1iWwHZ49trIUFekIEFGyBjomxjuKDZGc8"."privateKey":"7y1-NPiG_igcck_iIJ5sidurBa7ghC4Py0MTQPOFLGM"
}
Copy the code
  1. We need to introduce the Service Worker’s push module (SwPush) in the Component to support our code, as well as the Service Service to fetch network requests.
    subscribeToNotifications() {
        this.swPush.requestSubscription({
            serverPublicKey: this.VAPID_PUBLIC_KEY
        })  // The browser pops up a message request and gets a Promise if the request agrees
        .then(sub= > this.newsletterService.addPushSubscriber(sub).subscribe())  // A PushSubscription Object is obtained
        .catch(err= > console.error("Could not subscribe to notifications", err));
    }
Copy the code

PushSubscription object:

    {
      "endpoint": "https://fcm.googleapis.com/fcm/send/cbx2QC6AGbY:APA91bEjTzUxaBU7j-YN7ReiXV-MD-bmk2pGsp9ZVq4Jj0yuBOhFRrUS9pjz5FMnIvUenVq NpALTh5Hng7HRQpcUNQMFblTLTF7aw-yu1dGqhBOJ-U3IBfnw3hz9hq-TJ4K5f9fHLvjY"."expirationTime": null."keys": {
        "p256dh": "BOXYnlKnMkzlMc6xlIjD8OmqVh-YqswZdut2M7zoAspl1UkFeQgSLYZ7eKqKcx6xMsGK7aAguQbcG9FMmlDrDIA="."auth": "if-YFywyb4g-bFB1hO9WMw=="}}Copy the code
  1. You can set payload on the back-end Node server to customize the Notification content:
const notificationPayload = {
    "notification": {
        "title": "Angular News"."body": "Newsletter Available!"."icon": "assets/main-page-logo-small-hat.png"."vibrate": [100, 50, 100],
        "data": {
            "dateOfArrival": Date.now(),
            "primaryKey": 1}."actions": [{
            "action": "explore"."title": "Go to the site"}}};Copy the code
  1. Then use the Webpush module to push notifications:
Promise.all(allSubscriptions.map(sub => webpush.sendNotification(
    sub, JSON.stringify(notificationPayload) )))
    .then(() => res.status(200).json({message: 'Newsletter sent successfully.'}))
    .catch(err => {
        console.error("Error sending notification, reason: ", err);
        res.sendStatus(500);
    });
Copy the code

Add a new listener to the Service worker

Sw-custom. Js and sw-master.js.

  • sw-custom.jsDefine the listener we want to add, for example:
(function () {
  'use strict';
  self.addEventListener('notificationclick', (event) => {
    event.notification.close();
    console.log('notification details: ', event.notification); }); } ());Copy the code
  • sw-master.jsIs used tosw-custom.jsandngsw-worker.jsCombine two Service Worker files without losing the original function:
importScripts('./ngsw-worker.js');
importScripts('./sw-custom.js');
Copy the code

Once the files are set up, Angular needs to include our custom files when initializing the render, so add “SRC /sw-master.js” to assets in the angular.json file. Finally, register our new service worker file where we registered the service worker in app.module.ts:

ServiceWorkerModule.register('/sw-master.js', { enabled: environment.production })
Copy the code

One annoying point: Because Angular wraps ngsw-worker.js only in the dist folder created after ng build –prod, you can’t use the service worker without building. This is why testing PWA’s Service Worker function doesn’t run on ng Serve. So if we test their function, to avoid trouble will ServiceWorkerModule. Register (‘/sw – master. Js’ {enabled: Environment. Production}); For ServiceWorkerModule. Register (‘ sw – custom. Js), then we can’t use the original who presents encapsulated functionality but we can test our own listener.

Workaround: Add new environment variables to environment.ts and environment.prod.ts files so that different Service Worker dependencies can be run in different environments.

  • environment.tsAdd:serviceWorkerScript: 'sw-custom.js'
  • environment.prod.tsAdd:serviceWorkerScript: 'sw-master.js'
  • Register a Service WorkerServiceWorkerModule.register(environment.serviceWorkerScript)