What is pwa?

Progressive Web App, or PWA, is a new way to improve the experience of Web apps by giving users the experience of native apps.

PWA can achieve the experience of Native application not by referring to a particular technology, but through the application of some new technologies, which has been greatly improved in the three aspects of security, performance and experience. PWA is a Web App in essence, and has some features of Native App with the help of some new technologies. It has advantages of both Web App and Native App.

Technology dependency:

  • Service Worker
  • Web Storage (IndexedDB, Caches)
  • Fetch
  • Promises

PWA advantages

PWA applications should be:

  • Discoverable, can be identified as an application that is easily found through search engines
  • Installable can be installed for mobile devices and added to the user’s home screen
  • Linkable can be connected through URL sharing without complex installation
  • Network independent Works in an offline or low-speed network environment
  • Progressive progressive enhancements are available to all users, making the experience better for supported browsers and not affecting unsupported browser visits
  • Make it easy for users to re-engage through reminders
  • Suitable for any form of equipment
  • Safe The secure content delivery mechanism prevents eavesdropping and ensures that content is not tampered with

Progressive web app advantages. To find out how to implement PWAs, consult the guides listed in the below section.

Progressive

Emphasis is gradual, the transformation process can be carried out gradually, reduce the cost of the transformation of the site, the degree of new technology support is not complete, with the gradual evolution of new technology. PWA involves optimization in terms of security, performance, and experience. Consider the following steps:

  • The first step should be security, and make the whole site HTTPS, because this is the foundation of PWA, without HTTPS, there will be no Service Worker
  • The second step should be Service Worker to improve the basic performance and provide static files offline to improve the user’s first-screen experience
  • Step 3, App Manifest, can be done simultaneously with step 2
  • Later, consider other features, such as offline notification push

Level of support/coverage

Service Worker

The Service Worker, which is a proxy between the browser and the network, solves the problem of how to cache the page’s assets and if it still works offline. It is independent of the current web process, has its own independent worker context, and has no access to the DOM. Unlike traditional apis, it is non-blocking and returns results when ready based on the Promise method. It is not only offline, but also has the ability to notify messages and add desktop ICONS.

The premise condition

  • HTTPS, because the Service Worker requires HTTPS environment, we can use github page to learn debugging. Generally, browsers allow Service Worker debugging when host is localhost.
  • The caching mechanism of Service workers relies on the Cache API
  • Rely on the HTML5 FETCH API
  • Rely on Promise implementation

Lifecycle

A more detailed introduction to The Service Worker Lifecycle

A service worker goes through three steps in its lifecycle:

  • Registration Registration
  • The Installation is installed
  • The Activation the Activation

1. Register

Before installing the Server Worker, we register it in the main process JavaScript code to tell the browser what our Service Worker file is, and then in the background, the Service Worker installs and activates it.

The registration code can be placed in the tag of an HTML file or in a separate main.js file in the import HTML file.

if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/service-worker.js')
  .then(function(registration) {
    console.log('Registration successful, scope is:', registration.scope);
  })
  .catch(function(error) {
    console.log('Service worker registration failed, error:', error);
  });
}
Copy the code

Code, the first test is browser does support Service Worker, if supported, use the navigator. ServiceWorker. Register to register, if successful, will the promise. Then get the registration.

The service-worker.js file is where we will write the service worker function.

When registering, you can also specify the optional parameter scope, which is the scope, or directory, that the Service Worker can access.

navigator.serviceWorker.register('/service-worker.js', {
  scope: '/app/'
});
Copy the code

Service Workder can control the path of internal directories such as app/ app/home/ app/abbout/ and cannot access the path of previous layers such as/’/images’ /app.

If the Service Worker is already installed, registering again returns the registration object for the current activity.

Chrome already supports the debug function of Service workers, you can enter Chrome ://inspect/#service-workers to check whether the registration is successful. Or view it in the Application option of the console.

2. Install

The Install event is bound to the Service Worker file and is triggered when the installation is successful. Generally, we will perform caching in install events, using the aforementioned Cahce API, which is a global object on the Service Worker [5]. It can cache network resources and generate keys according to their requests. This API works in a similar way to the browser standard cache. But only for its own scope domain, the cache will always exist until it is manually cleared or flushed.

var cacheName = 'cachev1'
self.addEventListener('install'.function(event) {
  event.waitUntil(
    caches.open(cacheName).then(function(cache) {
      return cache.addAll(
        [
          '/css/bootstrap.css'.'/css/main.css'.'/js/bootstrap.min.js'.'/js/jquery.min.js'.'/offline.html']); })); });Copy the code
  1. Add install’s listener and useevent.waitUntil()To ensure that the Service Worker is not inwaitUntil()Installation is complete before execution is complete.
  2. usecaches.open()Create a new cache for cachev1 and return a cached Promise object that we will use in the then method when it is resolvedcaches.addAllTo add the list that you want to cache, the list is an array with urls relative to Origin.
  3. If the promise fails to be installed by Rejected, we have no catch, so we will not do anything. We can also modify the code and add the re-registration code.
  4. When the installation is complete, the Service Worker is activated successfully.

3. The activation

When the Service Worker is installed and activated, the Activate event is triggered. By listening for the Activate event you can do some pre-processing, such as updating old versions, cleaning up unused caches, etc.

How does the Service Worker update?

Service-worker. js controls the caching of page resources and requests. If the JS content is updated, when the browser gets a new file when visiting the website page, it will consider the update and start the update algorithm when the js file is not found byte by byte, so it will install the new file and trigger the install event. However, the old activated Service Worker is still running, and the new Service Worker will enter the waiting state after installation. The new Service Worker does not take effect in the following reopened pages until all opened pages are closed and the old Service Worker stops automatically.

What if you want all your pages to be automatically updated in a timely manner when a new version comes out? You can skip the waiting state by executing the self.skipWaiting() method in the install event and go straight to the Activate stage. Then update the Service workers on all clients by executing the self.clients.claim() method when the Activate event occurs.

// The installation phase skips wait and goes directly to active
self.addEventListener('install'.function (event) {
    event.waitUntil(self.skipWaiting());
});

self.addEventListener('activate'.function (event) {
    event.waitUntil(
        Promise.all([
            // Update the client
            self.clients.claim(),

            // Clean up the old version
            caches.keys().then(function (cacheList) {
                return Promise.all(
                    cacheList.map(function (cacheName) {
                        if(cacheName ! = ='cachev1') {
                            returncaches.delete(cacheName); }})); }))); });Copy the code

While js files may have browser cache problems, when the file is changed, the browser is still the old file. This causes the update to go unanswered. If you encounter this problem, try adding a filtering rule for the file on the Web Server, not caching it, or setting a short expiration date.

Or manually call update() to update

navigator.serviceWorker.register('/service-worker.js').then(reg= > {
  / / sometime later...
  reg.update();
});
Copy the code

It can be used in conjunction with localStorage instead of loading updates every time

var version = 'v1';

navigator.serviceWorker.register('/service-worker.js').then(function (reg) {
    if (localStorage.getItem('sw_version') !== version) {
        reg.update().then(function () {
            localStorage.setItem('sw_version', version) }); }});Copy the code

Schematic diagram

Every state has an ing going on.

Web Storage

Choosing the right storage mechanism is important for both local device storage and cloud-based server storage. A good storage engine ensures that information is kept in a reliable manner and reduces bandwidth and improves responsiveness. Correct storage caching strategy is the core building block of offline mobile web experience.

The types of storage, persistence of storage, browser support and other reasons, how to more efficient storage is the focus of our discussion.

data

Web Storage Overview

Using Cache API

Offine Storage for PWA

Best Practices for Using IndexedDB

Inspect and Manage Storage, Databases, and Caches

The cache API and with IndexedDB

To store data offline, you are advised to:

  • For url addressable resources, use the Cache API (part of the Service Worker)
  • For all other data, use [IndexedDB}(developer.mozilla.org/en-US/docs/…) (there is a Promise wrapper)

The basic principle of

Both of the above apis are asynchronous (IndexedDB is event-based, while the Cache API is promise-based). They can be used with Web Workers Windows Service Workers. IndexedDB is available in almost all browser environments (see CanIUse above), with support for Service Wokers and the Cahce API, as you can see from the figure above. It is supported for Chrome, Firefox, and Opera. IndexedDB’s Promise wrapper hides some of the powerful but complex machinery that comes with the IndexedDB library (for example, transaction processing transactions, schema versioning). IndexedDB will support observers, a feature that makes it easy to implement synchronization between tags.

For PWA, we can Cache static resources to use the Application Shell (JS/CSS/HTML file) written by the Cache API and populate offline page data from IndexedDB.

For Web Storage (LocalStorage/SessionStorage) are synchronous, does not support Web worker thread, and has the size and type (string) only.

Add to desktop

Allowing sites to be added to the home screen is an important feature provided by PWA. While some browsers already support adding web shortcuts to the home screen to quickly open sites, PWA is adding more functionality to the home screen than just a web shortcut to make the PWA experience more native.

The functionality that PWA adds to the desktop relies on manifest.json.

To enable the PWA application to be added to the desktop, you need to prepare a manifest.json file to configure the application icon and name in addition to requiring the site to support HTTPS. For example, a basic manifest.json should contain the following information:

{
    "name": "Easyify Docs"."short_name": "Easyify Docs"."start_url": "/"."theme_color": "#FFDF00"."background_color": "#FFDF00"."display":"standalone"."description": "A compilation tools for FE, built with webpack4.x, compile faster and smart, make work easier."."icons": [{"src": "./_assets/icons/32.png"."sizes": "32x32"."type": "image/png"}],... }Copy the code

Deploy manifest.json to the header of the HTML page of the PWA site using the Link tag, as shown below:

<link rel="manifest" href="path-to-manifest/manifest.json">
Copy the code

Parameter Description:

Name: {string} Application name, used to install the banner and display the splash screen short_name: {string} Application short name, used to display the main screen ICONS: {Array.<ImageObject>} Application icon list SRC: {string} icon urltype{string=} Mime type of the icon. This field is not mandatory. This field enables browsers to quickly ignore sizes {string} icon sizes that are not supported. If you need to fill in more than one size, use Spaces to separate it, as in"48x48 96x96 128x128"Start_url: {string=} Application startup address scope: {string} scope // scope should follow the following rules: // If scope is not set in the manifest, the default scope is the folder in the manifest.json; // Scope can be set to.. / or higher level paths to extend the scope of PWA; // start_URL must be scoped; // If start_URL is a relative address, its root path is affected by scope; // If start_URL is an absolute address (starting with /), that address will always have/as the root address; Background_color: {Color} The CSS Color value can specify the background Color of the splash screen. display: // Standalone browser UI (e.g. navigation bar, toolbar, etc.) will be hidden //minimal- UI display form is similar to that of standalone, // The browser UI will be minimized to a single button, with slightly different implementations depending on the browser. The orientation attribute of string applies the following values:  //landscape-primary //landscape-secondary //landscape //portrait-primary //portrait-secondary //portrait //natural //any theme_color: {Color} // CSS Color value The theme_color property specifies the theme Color of the PWA. You can use this property to control the color of the browser UI. For example, the color of the status bar on the PWA splash screen, the status bar in the content page, and the address bar will be affected by theme_color. Related_applications: Array.<AppInfo> The associated application list guides users to download native applications. Platform: {string} Application platform ID: {string} Application IDCopy the code

Push Notifications

We’re all notifications which are messages that pop up on our devices. Notifications can be triggered locally or pushed by the server, and our application is not running at the time. A notification push can remind us of updates to the App, which may also be of interest to us.

When our Web can achieve push, the experience of Web will be one step closer to Native APP.

Push Notifications consists of two apis:

  • The Notifications API is used to display system Notifications
  • The Push API is used to process Push messages sent by the Server

Both apis are built on top of the Service Worker API, which responds to push message times in the background and passes them to the application.

Notification

To obtain permission

Before creating a notification, you should obtain permission from the user:

// main.js
Notification.requestPermission(function(status) {
    console.log('Notification permission status:', status);
    Default granted denied Indicates the default value (asked every time you access the page), Permit, and deny
});
Copy the code

Add notification

After obtaining the user’s permission, you can restrict notifications to the main application through the showNotification() method.

// main.js
function displayNotification() {
  if (Notification.permission == 'granted') {
    navigator.serviceWorker.getRegistration().then(function(reg) {
      reg.showNotification('Hello world! '); }); }}Copy the code

Note showNotification, which is called on the Service Woker registration object. Notifications are created on the active Service Worker to listen for events triggered by the interaction with notifications.

The showNotification method has an optional parameter, options, to configure notification.

// main.js
function displayNotification() {
  if (Notification.permission == 'granted') {
    navigator.serviceWorker.getRegistration().then(function(reg) {
      var options = {
        body: 'Here is a notification body! '.// Add a description to the notification
        icon: 'images/example.png'.// Add an icon image
        vibrate: [100.50.100].// Specify the phone vibration mode of the notification, the phone will vibrate for 100ms, pause for 50ms, and vibrate again for 100ms
        data: {
          dateOfArrival: Date.now(),
          primaryKey: 1
        }, // Add custom data to notifications. When listening for notifications, you can capture these data for easy use.
        actions: [
          {action: 'explore'.title: 'Explore this new world'.icon: 'images/checkmark.png'},
          {action: 'close'.title: 'Close notification'.icon: 'images/xmark.png'},]// Custom operation
     };
      reg.showNotification('Hello world! ', options); }); }}Copy the code

Listen for an event

After the user receives a notification, the related events of the listening Notifications are triggered by actions on the notification, such as a NotificationClose event when the notification is closed.

// service-worker.js
self.addEventListener('notificationclick'.function(e) {
  var notification = e.notification;
  var primaryKey = notification.data.primaryKey;
  var action = e.action;

  if (action === 'close') {
    notification.close();
  } else {
    clients.openWindow('http://www.example.com'); notification.close(); }});Copy the code

Push

The notification operation should be combined with push to realize the interaction with users and actively notify and remind users

Push service

Each browser has a push service. When users grant push permission to the current website, they can subscribe the current website to the push service of the browser. This creates a contract object that contains the endpoint and the public key of the push service. When a push message is sent, it is sent to the endpoint URL, encrypted with the public key, and the Push service is sent to the correct client.

How does the push service know which client to send a message to? The endpoint URL contains a unique identifier. This identifier is used to route the message you send to the correct device and to identify the Service Worker that should handle the request when the browser processes it.

Push notifications and Service workers work together, so push notifications must also be HTTPS, which ensures secure communication between the server and the push Service, as well as from the push Service to the user.

However, HTTPS does not ensure that the push service itself is secure. We must ensure that data sent from the server to the client will not be tampered with or directly examined by any third party. So messages on the server must be encrypted.

The whole process of sending and receiving the presentation

On the client side:

1. Subscribe to push service

2. Send the subscription object to the server

On the server:

1. Generate data to be delivered to users

2. Use the user’s public key to encrypt data

3. Endpoint URL that sends data using the encrypted data payload

The message is routed to the user’s device. Wake up the browser, find the correct Service Worker, and invoke the push event.

1. Receive message data in push event (if any)

2. Perform custom logic in push events

3. Display notifications

Handling Push Events

When a pushing-enabled browser receives a message, it sends a push event to the Service Worker. We can create a push event listener in the Service Worker to handle messages:

// service-worker.js

self.addEventListener('push'.function(e) {
  var options = {
    body: 'This notification was generated from a push! '.icon: 'images/example.png'.vibrate: [100.50.100].data: {
      dateOfArrival: Date.now(),
      primaryKey: '2'
    },
    actions: [{action: 'explore'.title: 'Explore this new world'.icon: 'images/checkmark.png'},
      {action: 'close'.title: 'Close'.icon: 'images/xmark.png'}]}; e.waitUntil( self.registration.showNotification('Hello world! ', options)
  );
});
Copy the code

The difference is that we listen for push events instead of Notification events, and we use the event.waitUntil method to extend the life of the push event until the showNotification asynchronous operation is complete.

Subscribe to push notifications

Before sending push messages, we must first subscribe to the push service. Subscription returns either a subscription object or a subscription. It’s a critical part of the process to know where the push is going.

// main.js
// Check to see if you subscribe
    if ('serviceWorker' in navigator) {
        navigator.serviceWorker.register('sw.js').then(function (reg) {
                console.log('Service Worker Registered! ', reg);

                reg.pushManager.getSubscription().then(function (sub) {
                    if (sub === null) {
                        // Update UI to ask user to register for Push
                        console.log('Not subscribed to push service! ');
                    } else {
                        // We have a subscription, update the database
                        console.log('Subscription object: ', sub); }}); }) .catch(function (err) {
                console.log('Service Worker registration failed: ', err);
            });
    }

    function subscribeUser() {
        if ('serviceWorker' in navigator) {
            navigator.serviceWorker.ready.then(function (reg) {

                reg.pushManager.subscribe({
                    userVisibleOnly: true
                }).then(function (sub) {
                    console.log('Endpoint URL: ', sub.endpoint);
                }).catch(function (e) {
                    if (Notification.permission === 'denied') {
                        console.warn('Permission for notifications was denied');
                    } else {
                        console.error('Unable to subscribe to push', e); }}); }}})Copy the code

Web Push protocol

The Web Push protocol is the official standard for sending Push messages to the browser. It describes the structure and process of creating push messages, encrypting them, and sending them to the push messaging platform. The protocol abstracts out the details of which messaging platform and browser the user has.

The Web Push protocol is complex, but we don’t need to know all the details. Browsers are automatically responsible for subscribing to push services. As developers, our job is to get the subscription token, extract the URL, and send a message there.

{"endpoint":"https://fcm.googleapis.com/fcm/send/dpH5lCsTSSM:APA91bHqjZxM0VImWWqDRN7U0a3AycjUf4O-byuxb_wJsKRaKvV_iKw56s16ekq6FUqoCF7 k2nICUpd8fHPxVTgqLunFeVeB9lLCQZyohyAztTH8ZQL9WCxKpA6dvTG_TUIhQUFq_n"."keys": {
    "p256dh":"BLQELIDm-6b9Bl07YrEuXJ4BL_YBVQ0dvt9NQGGJxIQidJWHPNa9YrouvcQ9d7_MqzvGS9Alz60SZNCG3qfpk="."auth":"4vQK-SvRAN5eo-8ASlrwA=="}}Copy the code

VSPID authentication is commonly used to identify people. Let’s go to the last example

//main.js
    var endpoint;
    var key;
    var authSecret;

    // We need to convert the VAPID key to a base64 string when we subscribe
    function urlBase64ToUint8Array(base64String) {
      const padding = '='.repeat((4 - base64String.length % 4) % 4);
      const base64 = (base64String + padding)
        .replace(/\-/g.'+')
        .replace(/_/g.'/');

      const rawData = window.atob(base64);
      const outputArray = new Uint8Array(rawData.length);

      for (let i = 0; i < rawData.length; ++i) {
        outputArray[i] = rawData.charCodeAt(i);
      }
      return outputArray;
    }

    function determineAppServerKey() {
      var vapidPublicKey = 'BAyb_WgaR0L0pODaR7wWkxJi__tWbM1MPBymyRDFEGjtDCWeRYS9EF7yGoCHLdHJi6hikYdg4MuYaK0XoD0qnoY';
      return urlBase64ToUint8Array(vapidPublicKey);
    }

    if ('serviceWorker' in navigator) {
      navigator.serviceWorker.register('sw.js').then(function (registration) {

        return registration.pushManager.getSubscription()
          .then(function (subscription) {

            if (subscription) {
              // We already have a subscription, let's not add them again
              return;
            }

            return registration.pushManager.subscribe({
                userVisibleOnly: true.applicationServerKey: determineAppServerKey()
              })
              .then(function (subscription) {

                var rawKey = subscription.getKey ? subscription.getKey('p256dh') : ' ';
                key = rawKey ? btoa(String.fromCharCode.apply(null.new Uint8Array(rawKey))) : ' ';
                var rawAuthSecret = subscription.getKey ? subscription.getKey('auth') : ' ';
                authSecret = rawAuthSecret ?
                  btoa(String.fromCharCode.apply(null.new Uint8Array(rawAuthSecret))) : ' ';

                endpoint = subscription.endpoint;

                return fetch('http://localhost:3111/register', {
                  method: 'post'.headers: new Headers({
                    'content-type': 'application/json'
                  }),
                  body: JSON.stringify({
                    endpoint: subscription.endpoint,
                    key: key,
                    authSecret: authSecret,
                  }),
                })

              });
          });
      }).catch(function (err) {
        // registration failed :(
        console.log('ServiceWorker registration failed: ', err);
      });
    }

Copy the code
// server.js

const webpush = require('web-push');
const express = require('express');
var bodyParser = require('body-parser');
var path = require('path');
const app = express();

// Express setup
app.use(express.static('public'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({     // to support URL-encoded bodies
  extended: true
}));

function saveRegistrationDetails(endpoint, key, authSecret) {
  // Save the users details in a DB
}

webpush.setVapidDetails(
  'mailto:[email protected]'.'BAyb_WgaR0L0pODaR7wWkxJi__tWbM1MPBymyRDFEGjtDCWeRYS9EF7yGoCHLdHJi6hikYdg4MuYaK0XoD0qnoY'.'p6YVD7t8HkABoez1CvVJ5bl7BnEdKUu5bSyVjyxMBh0'
);

// Send a message
app.post('/sendMessage'.function (req, res) {

  var endpoint = req.body.endpoint;
  var authSecret = req.body.authSecret;
  var key = req.body.key;

  const pushSubscription = {
    endpoint: req.body.endpoint,
    keys: {
      auth: authSecret,
      p256dh: key
    }
  };

  var body = 'Breaking News: Nose picking ban for Manila police';
  var iconUrl = 'https://raw.githubusercontent.com/deanhume/progressive-web-apps-book/master/chapter-6/push-notifications/public/images/ homescreen.png';

  webpush.sendNotification(pushSubscription,
    JSON.stringify({
      msg: body,
      url: 'http://localhost:3111/article? id=1'.icon: iconUrl,
      type: 'actionMessage'
    }))
    .then(result= > {
      console.log(result);
      res.sendStatus(201);
    })
    .catch(err= > {
      console.log(err);
    });
});

// Register the user
app.post('/register'.function (req, res) {

  var endpoint = req.body.endpoint;
  var authSecret = req.body.authSecret;
  var key = req.body.key;

  // Store the users registration details
  saveRegistrationDetails(endpoint, key, authSecret);

  const pushSubscription = {
    endpoint: req.body.endpoint,
    keys: {
      auth: authSecret,
      p256dh: key
    }
  };

  var body = 'Thank you for registering';
  var iconUrl = '/images/homescreen.png';

  webpush.sendNotification(pushSubscription,
    JSON.stringify({
      msg: body,
      url: 'https://localhost:3111'.icon: iconUrl,
      type: 'register'
    }))
    .then(result= > {
      console.log(result);
      res.sendStatus(201);
    })
    .catch(err= > {
      console.log(err);
    });

});

// The server
app.listen(3111.function () {
  console.log('Example app listening on port 3111! ')});Copy the code

We’ll talk more about the push process later. May look at a given Google first tutorial describes developers.google.com/web/ilt/pwa…

reference

Progressive Web Apps Training

App Shell

Service Workers: an Introduction

MDN Progressive web apps

MDN WorkerGlobalScope [1]

The Service Worker Lifecycle

The W3C with IndexedDB API 3.0

Introduction to Push Notifications