Web Push and Vue.js, once again on working with web push messages on the frontend

Having worked on the functionality with web push messages in the next project, I found that there was still not enough information to do it quickly and without questions. Therefore, while everything has not disappeared from my memory, I hasten to formalize this experience in the form of an article.



You can find articles dated 2017 ... 2018 focused on the use of relatively low-level means for sending and receiving web push messages, for example, using the web-push-libs / web-push library . This library is still developing, however it is much easier to work with libraries from firebase nowadays.



Setting up a firebase project



So let's start by creating a project on firebase. With firebase console open , a new project needs to be created. In General Information-> Settings-> General Settings-> Your Applications, you need to create a new web application. This will generate a web application initialization code on the frontend side:



<!-- The core Firebase JS SDK is always required and must be listed first -->
<script src="https://www.gstatic.com/firebasejs/7.19.0/firebase-app.js"></script>

<!-- TODO: Add SDKs for Firebase products that you want to use
     https://firebase.google.com/docs/web/setup#available-libraries -->
<script src="https://www.gstatic.com/firebasejs/7.19.0/firebase-analytics.js"></script>

<script>
  // Your web app's Firebase configuration
  var firebaseConfig = {
    apiKey: "...",
    authDomain: "...",
    databaseURL: "...",
    projectId: "...",
    storageBucket: "...",
    messagingSenderId: "...",
    appId: "...",
    measurementId: "..."
  };
  // Initialize Firebase
  firebase.initializeApp(firebaseConfig);
  firebase.analytics();
</script>


On the same tab of the firebase console General information-> Settings-> Cloud Messaging-> Credentials for the project -> Server key, we find the private key, with which you can send push notifications through the firebase server.



Sending a web push message



Front-end developers can send web push messages on their own using the curl command:



curl -X POST -H "Authorization: key=< >" -H "Content-Type: application/json"    -d '{
    "data": {
        "title": "FCM Message",
        "body": "This is an <i>FCM Message</i>",
        "icon": "/static/plus.png",
        "sound": "/static/push.mp3",
        "click_action": "https://google.com",
  },
  "to": "< >"
}' https://fcm.googleapis.com/fcm/send


Obtaining a server key is described in the section Setting up a firebase project , and obtaining a registration token will be described in the section Getting a registration token .



data vs notification payload



The payload can be sent in the data or notification field of a web push message. For notification payload, the request will look like this (for data payload, see the request in the Sending a push message section ):



curl -X POST -H "Authorization: key=< >" -H "Content-Type: application/json"    -d '{
    "notification": {
        "title": "FCM Message",
        "body": "This is an <i>FCM Message</i>",
        "icon": "/static/plus.png",
        "click_action": "https://google.com",
  },
  "to": "< >"
}' https://fcm.googleapis.com/fcm/send


data and notification payload have two fundamental differences:



  1. notification payload has a strictly defined set of fields, extra fields will be ignored, while data payload sends all fields to the frontend without limitation.
  2. If the web browser is in the background or the active link contains a third-party site, the notification payload web push displays a message without transferring control to the onMessage event handlers, while the data payload web push always transfers control to the onMessage event handlers, but for to display a message, you must explicitly create a Notification object. If the web browser is in an active state and our site is open on the active tab, then the work with data and notification payload does not differ.


Creating a messaging object



To work on the frontend with web push messages, you need to create a messaging object:



const messaging = window.firebase.messaging();


In this code, firebasethis is a global object that is created during the loading of the firebase libraries and initialized on the frontend side as described in Setting up a firebase project . The project was developed in Vue.js. Therefore, connecting scripts via an html script element did not look promising. To connect these scripts, I used the library vue-plugin-load-script:



import Vue from "vue";
import LoadScript from "vue-plugin-load-script";

Vue.use(LoadScript);

var firebaseConfig = {
  apiKey: "...",
  authDomain: "...",
  databaseURL: "...",
  projectId: "...",
  storageBucket: "...",
  messagingSenderId: "...",
  appId: "...",
  measurementId: "..."
};

Promise.resolve()
  .then(() =>
    Vue.loadScript(
      "https://www.gstatic.com/firebasejs/7.14.0/firebase-app.js"
    )
  )
  .then(() =>
    Vue.loadScript(
      "https://www.gstatic.com/firebasejs/7.14.0/firebase-messaging.js"
    )
  )
  .then(() =>
    Vue.loadScript(
      "https://www.gstatic.com/firebasejs/7.14.0/firebase-analytics.js"
    )
  )
  .then(() => {
    window.firebase.initializeApp(firebaseConfig);
    const messaging = window.firebase.messaging();
    ... //    messaging
  });


Obtaining a registration token



A registration token is an identifier that uniquely identifies a device and a web browser, thus allowing a web push message to be sent to a specific device and processed by a specific web browser:



  Notification.requestPermission()
    .then(permission => {
      if (permission === "granted") {
        messaging
          .getToken()
          .then(token => {
            ... //    
          });
      } else {
        console.log("Unable to get permission to notify.");
      }
    });


Under some circumstances, the token can be updated. And you need to handle the token update event:



  messaging.onTokenRefresh(function() {
    messaging
      .getToken()
      .then(function(refreshedToken) {
         ... //     
      });
  });


Regarding this event, I have a question - is it relevant. The fact is that even before moving to FCM, the token rotation procedure worked with GCM. This was described in the Android library and indirectly in the description of the server operation, where each server response contained canonical tokens and they had to be constantly checked and changed (however, as it turned out, other than me, it was rarely followed by anyone). After moving to FCM, such a concept as canonical tokens fell out of use (most likely because in practice they were rarely tracked). In this regard, the cases when an event may occur are not entirely clear onTokenRefresh().



OnMessage event - simplified version



I will immediately answer why it is simplified. We will make at least two simplifications. 1) We will use notification payload to receive and display messages if the application is in the background without additional work. 2) Forget that on mobile devices the security system does not allow the new Notification () operator to be executed.



So, as we already said, for notification payload, a web push message comes and is displayed without the slightest participation of the front-end developer (of course, after sending the registration token to the server). It remains to work out the case the web browser is in an active state and the site is open on an active tab:



  messaging.onMessage(function(payload) {
      const data = { ...payload.notification, ...payload.data };
      const notificationTitle = data.title;
      const notificationOptions = {
          body: data.body,
          icon: data.icon,
          image: data.image,
          click_action: data.click_action,
          requireInteraction: true,
          data
      };
      new Notification(payload.notification.title, payload.notification);
  });


Handling the event of receiving a web push message in the background



In this section, we'll start working with a service worker. And this, among other things, means that you need to configure the site to work using the secure https protocol. And this immediately complicates further development. Therefore, for simple cases, what has already been described earlier is sufficient.



To work with the firebase library, a file named firebase-messaging-sw.js. The name of the file can be different, but it must in any case be in the root directory due to the way the web browser protection works (otherwise, this serivce worker will not work for the entire site).



As a rule, an event handler is also placed in this file notificationclick. You can hardly find anything different from this code:



var firebaseConfig = {
  apiKey: "...",
  authDomain: "...",
  databaseURL: "...",
  projectId: "...",
  storageBucket: "...",
  messagingSenderId: "...",
  appId: "...",
  measurementId: "..."
};

importScripts("https://www.gstatic.com/firebasejs/7.17.2/firebase-app.js");
importScripts("https://www.gstatic.com/firebasejs/7.17.2/firebase-messaging.js");

firebase.initializeApp(firebaseConfig);
const messaging = firebase.messaging();

messaging.setBackgroundMessageHandler(function(payload) {
  const data = { ...payload.notification, ...payload.data };
  const notificationTitle = data.title;
  const notificationOptions = {
    body: data.body,
    icon: data.icon,
    image: data.image,
    requireInteraction: true,
    click_action: data.click_action,
    data
  };
  self.registration.showNotification(notificationTitle, notificationOptions);
});

self.addEventListener("notificationclick", function(event) {
  const target = event.notification.data.click_action;
  event.notification.close();
  event.waitUntil(
    clients
      .matchAll({
        type: "window",
        includeUncontrolled: true
      })
      .then(function(clientList) {
        for (var i = 0; i < clientList.length; i++) {
          var client = clientList[i];
          console.log(client.url, client.focus);
          if (client.url === target && "focus" in client) {
            return client.focus();
          }
        }
        return clients.openWindow(target);
      })
  );
});


Option to handle onMessage event with service worker



Let me remind you that in the section onMessage Event - a simplified version, we have already described how to handle web push messages. But this method had one significant drawback - it did not work on mobile devices due to the peculiarities of the web browser protection system. To overcome this drawback, a service worker option was invented, in which the Notification object is already embedded, and it does not need to be created with the new operator:



  messaging.onMessage(function(payload) {
    play();
    navigator.serviceWorker.register("/firebase-messaging-sw.js");
    Notification.requestPermission(function(result) {
      if (result === "granted") {
        navigator.serviceWorker.ready
          .then(function(registration) {
            const data = { ...payload.notification, ...payload.data };
            const notificationTitle = data.title;
            const notificationOptions = {
              body: data.body,
              icon: data.icon,
              image: data.image,
              click_action: data.click_action,
              requireInteraction: true,
              data
            };
            return registration.showNotification(
              notificationTitle,
              notificationOptions
            );
          })
          .catch(function(error) {
            console.log("ServiceWorker registration failed", error);
          });
      }
    });
  });


Beep when receiving a web push message



I must say that we have practically no control over how push notifications will be displayed on various devices. In some cases, this will be a pop-up message, in others, the push will immediately go to the system "plate", and if there is no voice acting yet, it will simply be lost to the client. Everything is very complicated with a svuk. Earlier specifications included a sound field, which was previously responsible for the sound when receiving a web push message, but currently there is no such property. In this regard, I set myself the goal of making an audio recording of the push.



The sometimes encountered description with the creation of an html audio element and calling its play () method in reality does not work due to the security features of the web browser (it can only be called upon a click from a real user). But there is also AudioContext () - we will work with it:



const play = () => {
  try {
    const context = new AudioContext();
    window
      .fetch(soundUrl)
      .then(response => response.arrayBuffer())
      .then(arrayBuffer => context.decodeAudioData(arrayBuffer))
      .then(audioBuffer => {
        const source = context.createBufferSource();
        source.buffer = audioBuffer;
        source.connect(context.destination);
        source.start();
      });
  } catch (ex) {
    console.log(ex);
  }
};


Everything is fine, but we still have a service worker that does not have an AudioContext () object. Let's remember that all workers communicate through messages. And then receiving events from the service worker will look like this:



try {
  const broadcast = new BroadcastChannel("play");
  broadcast.onmessage = play;
} catch (ex) {
  console.log(ex)  ;
}


Of course, for this code to work, you need 1) The browser is open 2) The site is open (although not necessarily on the active tab). But there is no other way.



Instead of an afterword



Now you can kind of exhale and say, that's it. But ... All of this does not work on safari - and this is another separate and poorly documented topic, although several articles can be found.



Useful links



1) habr.com/ru/post/321924



apapacy@gmail.com

August 24, 2020



All Articles