Service Worker Learning and Practice (3)-Message Push

  Front end, javascript

In the last articleLearning and Practice of Service Worker (2)-Brief Introduction of ——PWAAs already mentionedPWAThe origin, advantages and disadvantages of the, and through a simple example to illustrate how the desktop and mobile end will be aPWAInstalled on the desktop, this article will illustrate how to use it through an example.Service WorkerThe message push function of, and cooperate withPWATechnology brings a native application-like message push experience.

Notification

After all,PWAThe message push is also a kind of server push, common server push methods, such as widely used polling, long polling,Web SocketAt the end of the day, it is all communication between the client and the serverService WorkerIn, the client receives the notification based onNotificationTo push.

So, let’s look at how to use it directly.NotificationTo send a push? The following is a sample code:

// 在主线程中使用
let notification = new Notification('您有新消息', {
  body: 'Hello Service Worker',
  icon: './images/logo/logo152.png',
});

notification.onclick = function() {
  console.log('点击了');
};

After the console knocks down the above code, the following notification will pop up:

However,NotificationThis ..API, only recommended inService WorkerIt is not recommended to use it in the main thread, and it is not recommended to use it in theService WorkerThe method of use in is:

// 添加notificationclick事件监听器,在点击notification时触发
self.addEventListener('notificationclick', function(event) {
  // 关闭当前的弹窗
  event.notification.close();
  // 在新窗口打开页面
  event.waitUntil(
    clients.openWindow('https://google.com')
  );
});

// 触发一条通知
self.registration.showNotification('您有新消息', {
  body: 'Hello Service Worker',
  icon: './images/logo/logo152.png',
});

Readers can visitMDN Web DocsAboutNotificationInService WorkerThis article will not waste a lot of space to elaborate on the relevant usage in.

Permission to apply for push

If the browser opens the right to push notifications to users directly to all developers, users will inevitably be harassed by a large amount of junk information, so this right needs to be applied. If users prohibit message pushing, developers have no right to initiate message pushing to users. We can pass.serviceWorkerRegistration.pushManager.getSubscriptionMethod to see if the user has permission to push notifications. Modifysw-register.jsCode in:

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('没有订阅');
          subscribeUser(swReg);
        }
      });
  });
}

The above code calls theswReg.pushManagerThegetSubscription, you can know whether the user has allowed message push, ifswReg.pushManager.getSubscriptionThePromiseBereject, it means that the user has not subscribed to our message, callsubscribeUserMethod, apply to the user for permission to push messages:

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('订阅失败: ', err);
  });
}

The above code passesserviceWorkerRegistration.pushManager.subscribeThe right to initiate a subscription to the user. This method returns aPromiseIfPromiseBeresolveTo indicate that the user allows the application to push messages; otherwise, ifrejectTo indicate that the user has rejected the application’s message push. As shown in the following figure:

serviceWorkerRegistration.pushManager.subscribeMethods usually need to pass two parameters:

  • userVisibleOnlyThis parameter is usually set totrueA that indicates whether subsequent information is displayed to the user.
  • applicationServerKeyThis parameter is aUint8Array, which is used to encrypt the push information of the server to prevent man-in-the-middle attacks, and the session is tampered with by the attacker. This parameter is the public key generated by the server and is passed through theurlB64ToUint8ArrayConverted, 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 obtain the public key of the server will be discussed in the following articles.

Permission to process rejections

If you are callingserviceWorkerRegistration.pushManager.subscribeAfter that, the user refused the push permission and could also use theNotification.permissionWhen this state is obtained,Notification.permissionThere are three values:

  • granted: The user has explicitly granted the right to display notifications.
  • denied: The user has explicitly denied the right to display notifications.
  • default: The user has not been asked if he is authorized, in which case the authority will be considered asdenied.
if (Notification.permission === 'granted') {
  // 用户允许消息推送
} else {
  // 还不允许消息推送,向用户申请消息推送的权限
}

Key generation

In the above codeapplicationServerPublicKeyGenerally, the public key generated by the server will be returned to the client when the page is initialized. The server will save the public key and private key corresponding to each user for message push.

In my sample demonstration, we can use theGoogleSupporting experimental websitesweb-push-codelabGenerate public and private keys to send message notifications:

Send push

InService WorkerThrough monitoringpushEvent to handle message push:

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 above code, inpushEvent callback, throughevent.data.text()Get the text pushed by the message and call the aboveself.registration.showNotificationTo show message push.

Sending by server

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

On callswReg.pushManager.subscribeMethod, if the user allows message push, the function returnsPromiseWill beresolveInthenGets the corresponding from thesubscription.

subscriptionGenerally, the format is as follows:

{
  "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"
  }
}

UseGoogleSupporting experimental websitesweb-push-codelabTo send a message push.

web-push

On the server side, useweb-push-libsTo realize the generation of public key and private key, message push function,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是前端通过swReg.pushManager.subscribe获取到的subscription
const pushSubscription = {
  endpoint: '.....',
  keys: {
    auth: '.....',
    p256dh: '.....'
  }
};

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

In the above code,GCM API KeyNeed to be inFirebase consolePlease refer to this article for the application courseBowen.

In this example I wroteDemoChina, I putsubscriptionWrite dead:

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 a pushed message is clicked, so cooperationclients APISome interactions similar to native applications can be realized, which is referred to here.BowenImplementation of:

Service Workerhit the targetself.clientsObject providesClientOn the same day,ClientInterface represents an executable context, such asWorkerOrSharedWorker.WindowThe client is composed of more specificWindowClientTo express. You can start fromClients.matchAll()AndClients.get()Methods such asClient/WindowClientObject.

A new window opens

Useclients.openWindowOpen a web page in a new window:

self.addEventListener('notificationclick', function(event) {
  event.notification.close();
  // 新窗口打开
  event.waitUntil(
    clients.openWindow('https://google.com/')
  );
});

Focus on already opened pages

utilizecilentsRelevant providedAPIGets the page that the current browser has openedURLs. But theseURLsOnly with youSWOf the same domain. Then, by matchingURLThroughmatchingClient.focus()Focus. If not, the page will be opened.

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

Detecting whether push is needed

If the user has already stayed on the current webpage, then we may not need to push it, so how should we detect if the user is on the webpage?

viawindowClient.focusedThe current can be detectedClientIs it 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('用户已经聚焦于当前页面,不需要推送。');
      }
    });
});

Merge message

This scenario is mainly aimed at merging messages. For example, when there is only one message, it can be pushed directly. What if the user sends another message? At this time, a better user experience is to merge the push directly into one and replace it. Then, at this time, we need to get the push message that has been shown currently, which is mainly throughregistration.getNotifications() APITo get it. ThisAPIAlso returned is a.PromiseObject. viaPromiseInresolveI got it later.notificationsJudging itlengthTo 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 = `您有${notifications.length}条新消息`;
            } else {
              options.body = event.data.text();
            }
            return self.registration.showNotification(title, options);

          });
      } else {
        console.log('用户已经聚焦于当前页面,不需要推送。');
      }
    });
  // ...
});

Summary

Through a simple example, this article tells the story ofService WorkerThe principle of message push in.Service WorkerMessage push in is based onNotification APIYes, this oneAPIThe use of the first need user authorization, through inService WorkerAt the time of registrationserviceWorkerRegistration.pushManager.subscribeMethod to apply for permission from the user. if the user refuses the message push, the application program also needs related processing.

Message push is based on Google cloud services, therefore, in China, receivedGFWThe support for this function is not good.GoogleA series of push-related libraries are provided, such asNode.jsIn, useweb-pushTo achieve. The general principle is: the public key and private key are generated at the server, and the public key and private key are stored to the server for the user, while the client only stores the public key.Service WorkerTheswReg.pushManager.subscribeCan be obtainedsubscription, and sent to the server, the server to usesubscriptionInitiates a message push to the specified user.

Message push function can cooperateclients APIDo special treatment.

If the user has installedPWAApplication, even if the user closes the application,Service WorkerIt is also running, and even if the user does not open the application, he will receive a message notification.

In the next article, I will try to use it in my project.Service WorkerAnd passedWebpackAndWorkboxConfigure to TellService WorkerThe best practices of.