Service Worker Learning and Practice-Message Pushing

Keywords: Javascript Google Windows JSON Mobile

In the last article Service Worker Learning and Practice (2) - Introduction to PWA The origin, advantages and disadvantages of PWA have been discussed. A simple example is given to illustrate how to install a PWA on the desktop and mobile. This article will illustrate how to use the message push function of Service Worker and bring the native application-like message push experience with PWA technology through an example.

Notification

In the final analysis, the message push of PWA is also a kind of service-side push. Common service-side push methods, such as polling, long polling, Web Socket, etc., are all communication between client and server. In ServiceWorker, the client receives notification based on Notification To push.

So let's see how to send a push directly using Notification. Here is a sample code:

// Use in the main thread
let notification = new Notification('You have new information', {
  body: 'Hello Service Worker',
  icon: './images/logo/logo152.png',
});

notification.onclick = function() {
  console.log('Click.');
};

When the above code is typed on the console, the following notification pops up:

However, Notification API is only recommended for use in Service Worker, not in main threads, and is used in Service Worker in the following ways:

// Add notificationclick event listener to trigger when you click notification
self.addEventListener('notificationclick', function(event) {
  // Close the current bullet window
  event.notification.close();
  // Open the page in a new window
  event.waitUntil(
    clients.openWindow('https://google.com')
  );
});

// Trigger a notification
self.registration.showNotification('You have new information', {
  body: 'Hello Service Worker',
  icon: './images/logo/logo152.png',
});

Readers can MDN Web Docs As for the use of Notification in ServiceWorker, this paper does not waste a lot of space to elaborate in detail.

Right to apply for pushing

If the browser directly gives all developers the right to push notifications to users, then users will be harassed by a large number of spam information. Therefore, this right needs to be applied. If users prohibit message pushing, developers have no right to initiate message pushing to users. We can go through it. serviceWorkerRegistration.pushManager.getSubscription Method to see if the user has permission to push notifications. Modify the code in sw-register.js:

if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js').then(function (swReg) {
    swReg.pushManager.getSubscription()
      .then(function(subscription) {
        if (subscription) {
          console.log(JSON.stringify(subscription));
        } else {
          console.log('No subscription');
          subscribeUser(swReg);
        }
      });
  });
}

The above code calls getSubscription of swReg.pushManager to know if the user has allowed message pushing. If Proise of swReg.pushManager.getSubscription is reject ed, it means that the user has not subscribed to our message yet. Call subscribeUser method to apply for message pushing authority to the user:

function subscribeUser(swReg) {
  const applicationServerKey = urlB64ToUint8Array(applicationServerPublicKey);
  swReg.pushManager.subscribe({
    userVisibleOnly: true,
    applicationServerKey: applicationServerKey
  })
  .then(function(subscription) {
    console.log(JSON.stringify(subscription));
  })
  .catch(function(err) {
    console.log('Subscription failed: ', err);
  });
}

The above code passes through serviceWorkerRegistration.pushManager.subscribe The method returns a Promise with the right to initiate a subscription to the user. If Promise is resolve d, it means that the user allows the application to push messages. Conversely, if rejected, it means that the user rejects the application's message push. As shown in the following figure:

The serviceWorkerRegistration.pushManager.subscribe method usually needs to pass two parameters:

  • userVisibleOnly, which is usually set to true, is used to indicate whether subsequent information is presented to the user.
  • Application Server Key, which is a Uint8Array parameter, is used to encrypt push information on the server side to prevent man-in-the-middle attacks and session tampering by the attacker. This parameter is a public key generated by the server and converted by urlB64ToUint8Array. This function is usually fixed, as follows:
function urlB64ToUint8Array(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;
}

How to acquire the public key of the server will be discussed in the following article.

Authority to deal with rejection

If the user refuses the push permission after calling service WorkerRegistration. pushManager. subscribe, it can also be used in the application by Notification.permission To get this state, Notification.permission has three values:

  • Granted: The user has explicitly granted the right to display notifications.
  • Denied: Users have explicitly denied the right to display notifications.
  • default: Users have not yet been asked whether they are authorized. In an application, permissions in this case will be deemed denied.
if (Notification.permission === 'granted') {
  // User Allow Message Push
} else {
  // Message push is not allowed, and the right to apply for message push is granted to the user.
}

Key generation

The application Server PublicKey in the above code is usually a public key generated by the server, which is returned to the client when the page is initialized, and the server saves the corresponding public and private keys of each user for message pushing.

In my demonstration, we can use Google's experimental website web-push-codelab Generate public and private keys to send message notifications:

Send push

In Service Worker, message push is handled by listening for push events:

self.addEventListener('push', function(event) {
  const title = event.data.text();
  const options = {
    body: event.data.text(),
    icon: './images/logo/logo512.png',
  };

  event.waitUntil(self.registration.showNotification(title, options));
});

In the code above, in the push event callback, the text of the message push is obtained through event.data.text(), and then the self. registration. show Notification mentioned above is invoked to show the message push.

Server Send

Then, how to identify the designated user and send the corresponding message to the server?

After calling the swReg.pushManager.subscribe method, if the user is allowed to push the message, the Promise returned by the function will resolve and get the corresponding subscription in the then.

subscription is generally in the following format:

{
  "endpoint": "https://fcm.googleapis.com/fcm/send/cSEJGmI_x2s:APA91bHzRHllE6tNoEHqjHQSlLpcQHeiGr7X78EIa1QrUPFqDGDM_4RVKNxoLPV3_AaCCejR4uwUawBKYcQLmLpUrCUoZetQ9pVzQCJSomB5BvoFZBzkSnUb-ALm4D1lqwV9w_uP3M0E",
  "expirationTime": null,
  "keys": {
    "p256dh": "BDOx1ZTtsFL2ncSN17Bu7-Wl_1Z7yIiI-lKhtoJ2dAZMToGz-XtQOe6cuMLMa3I8FoqPfcPy232uAqoISB4Z-UU",
    "auth": "XGWy-wlmrAw3Be818GLZ8Q"
  }
}

Using Google's experimental website web-push-codelab Send message push.

web-push

On the server side, use web-push-libs To realize the generation of public key and private key and the function of message pushing. Node.js version.

const webpush = require('web-push');

// VAPID keys should only be generated only once.
const vapidKeys = webpush.generateVAPIDKeys();

webpush.setGCMAPIKey('<Your GCM API Key Here>');
webpush.setVapidDetails(
  'mailto:example@yourdomain.org',
  vapidKeys.publicKey,
  vapidKeys.privateKey
);

// pushSubscription is a subscription obtained by the front end through swReg.pushManager.subscribe
const pushSubscription = {
  endpoint: '.....',
  keys: {
    auth: '.....',
    p256dh: '.....'
  }
};

webpush.sendNotification(pushSubscription, 'Your Push Payload Text');

In the above code, GCM API Key needs to Firebase console You can refer to this article when applying for a course. Bowen.

In this Demo example I wrote, I wrote subscription to death:

const webpush = require('web-push');

webpush.setVapidDetails(
  'mailto:503908971@qq.com',
  'BCx1qqSFCJBRGZzPaFa8AbvjxtuJj9zJie_pXom2HI-gisHUUnlAFzrkb-W1_IisYnTcUXHmc5Ie3F58M1uYhZU',
  'g5pubRphHZkMQhvgjdnVvq8_4bs7qmCrlX-zWAJE9u8'
);

const subscription = {
  "endpoint": "https://fcm.googleapis.com/fcm/send/cSEJGmI_x2s:APA91bHzRHllE6tNoEHqjHQSlLpcQHeiGr7X78EIa1QrUPFqDGDM_4RVKNxoLPV3_AaCCejR4uwUawBKYcQLmLpUrCUoZetQ9pVzQCJSomB5BvoFZBzkSnUb-ALm4D1lqwV9w_uP3M0E",
  "expirationTime": null,
  "keys": {
    "p256dh": "BDOx1ZTtsFL2ncSN17Bu7-Wl_1Z7yIiI-lKhtoJ2dAZMToGz-XtQOe6cuMLMa3I8FoqPfcPy232uAqoISB4Z-UU",
    "auth": "XGWy-wlmrAw3Be818GLZ8Q"
  }
};

webpush.sendNotification(subscription, 'Counterxing');

Interactive response

By default, there is no corresponding interaction after the push message is clicked. clients API Some interactions similar to native applications can be implemented, which is referred to here. Bowen Implementation:

The self.clients object in Service Worker provides access to Client, and the Client interface represents an executable context, such as Worker or SharedWorker. Windows clients are represented by more specific Windows Clients. You can get Client / Windows Client objects from Clients.matchAll() and Clients.get().

New window opens

Open a Web page in a new window using clients.openWindow:

self.addEventListener('notificationclick', function(event) {
  event.notification.close();
  // New window opens
  event.waitUntil(
    clients.openWindow('https://google.com/')
  );
});

Focus on open pages

Using the relevant API provided by cilents, the page URLs that the current browser has opened are retrieved. But these URLs can only be in the same domain as your SW. Then, focus through matching Client. focus () by matching the URL. If not, just open the page.

self.addEventListener('notificationclick', function(event) {
  event.notification.close();
  const urlToOpen = self.location.origin + '/index.html';

  const promiseChain = clients.matchAll({
      type: 'window',
      includeUncontrolled: true
    })
    .then((windowClients) => {
      let matchingClient = null;

      for (let i = 0; i < windowClients.length; i++) {
        const windowClient = windowClients[i];
        if (windowClient.url === urlToOpen) {
          matchingClient = windowClient;
          break;
        }
      }

      if (matchingClient) {
        return matchingClient.focus();
      } else {
        return clients.openWindow(urlToOpen);
      }
    });

  event.waitUntil(promiseChain);
});

Check if push is required

If the user has stayed on the current page, then we may not need to push, so in view of this situation, how should we detect whether the user is on the page?

Windows Client. focused can detect whether the current Client is in focus.

self.addEventListener('push', function(event) {
  const promiseChain = clients.matchAll({
      type: 'window',
      includeUncontrolled: true
    })
    .then((windowClients) => {
      let mustShowNotification = true;

      for (let i = 0; i < windowClients.length; i++) {
        const windowClient = windowClients[i];
        if (windowClient.focused) {
          mustShowNotification = false;
          break;
        }
      }

      return mustShowNotification;
    })
    .then((mustShowNotification) => {
      if (mustShowNotification) {
        const title = event.data.text();
        const options = {
          body: event.data.text(),
          icon: './images/logo/logo512.png',
        };
        return self.registration.showNotification(title, options);
      } else {
        console.log('Users have focused on the current page and do not need to push.');
      }
    });
});

Merge message

This scenario focuses on message merging. For example, when there is only one message, it can be pushed directly. What if the user sends another message? At this point, the better user experience is to merge push directly into one, and then replace it. At this point, we need to get the currently displayed push message, which is mainly obtained through the registration.getNotifications() API. The API also returns a Promise object. Through Promise's notifications after resolve, judge its length and merge messages.

self.addEventListener('push', function(event) {
  // ...
    .then((mustShowNotification) => {
      if (mustShowNotification) {
        return registration.getNotifications()
          .then(notifications => {
            let options = {
              icon: './images/logo/logo512.png',
              badge: './images/logo/logo512.png'
            };
            let title = event.data.text();
            if (notifications.length) {
              options.body = `You have ${notifications.length}New news`;
            } else {
              options.body = event.data.text();
            }
            return self.registration.showNotification(title, options);

          });
      } else {
        console.log('Users have focused on the current page and do not need to push.');
      }
    });
  // ...
});

Summary

This paper describes the principle of message push in Service Worker through a simple example. Message push in Service Worker is based on Notification API. User authorization is the first requirement for the use of this API. Users are granted permission through the service Worker Registration. pushManager. subscribe method when they register with Service Worker. If the user refuses the message push, the application also needs relevant processing.

Message push is based on Google cloud service. Therefore, in China, receiving the restriction of GFW is not very good. Google provides a series of push-related libraries, such as Node.js, which use GFW. web-push To achieve. The general principle is that the public key and private key are generated at the server side, and stored to the server side for the user, while the client only stores the public key. Service Worker's swReg.pushManager.subscribe can get subscription and send it to the server, which uses subscription to initiate message push to the specified user.

Message push function can cooperate with client API for special processing.

If a user installs a PWA application, even if the user shuts down the application, the Service Worker is running, and even if the user does not open the application, he will receive a message notification.

In the next article, I'll try to use Service Worker in my project and describe the best practices of Service Worker through Webpack and Workbox configurations.

Posted by skatermike21988 on Fri, 10 May 2019 17:43:50 -0700