© Carlos Rojas 2020
C. RojasBuilding Progressive Web Applications with Vue.js https://doi.org/10.1007/978-1-4842-5334-2_7

7. Background Sync

Carlos Rojas1 
(1)
Medellin, Colombia
 

There are scenarios when our users expect an app to react automatically during disconnection. If they do something in the app while they are offline, the system resumes the state when a connection is available. In these cases, Background Sync is useful to us.

Think, for a second, about a chat app. As a user, I hope that, if I have a few spare seconds, I can see the messages I get from friends and have time to answer each message, close the app, then continue on with my life. However, what if I’m in a place with flaky or no connectivity? I probably can’t answer the messages because, if I try, a blank page or a loader or an error message will show up (if I’m using a regular web app). However, with Background Sync in our PWA, users can answer messages and let the app handle them when connection returns to the device.

Using Background Sync

Background Sync, combined with a service worker, grants us great power in building PWAs. Using Background Sync is relatively easy. We simply register our event with SyncManager and react to the sync event:
navigator.serviceWorker.ready
.then(function(registration){
       registration.sync.register('my-event-sync');
});
After we register our event, we can use it from our service worker in the sync event:
self.addEventListener("sync",
function(event) {
       if (event.tag === "my-event-sync") {
       // The logic of our event goes here.
       }
}
);

SyncManager

SyncManager is the service worker API that handles and registers synchronized events (Figure 7-1).
../images/483082_1_En_7_Chapter/483082_1_En_7_Fig1_HTML.jpg
Figure 7-1

SyncManager workflow

A sync event is triggered when
  • A sync event is registered

  • When the user goes from offline to online

  • Every few minutes if there is a registry that has not been completed successfully

To work with SyncManager, we use three things: the event tags, the getTags() method, and the lastChance() method.

Event Tags

The event tags allow us to register events in SyncManager. They are unique and, if they are repeated, SyncManager ignores them.
self.addEventListener('sync', function(event) {
 cont tag = event.tag
});

Obtaining the Event List

We can obtain all registered events in SyncManager using the getTags() method , which returns a promise:
navigator.serviceWorker.ready.then(
 (registration) => {
 registration.sync.getTags().then((tag)=> console.log(tag));
 }
 );

Obtaining the Last Chance of an Event

In some cases, SyncManager decides to discard an event that has failed multiple times. However, when this occurs, SyncManager allows you to react to this event using the lastChance() method :
self.addEventListener('sync', function(event) {
 console.log('I am in sync', event.tag);
 if(event.lastChance) {
 // Do something; last opportunity.
 }
});

As you can see, Background Sync is pretty easy to use, and it opens an infinite world of possibilities to add a positive experience for our users. Also, we can mix it with IndexedDB and make our apps more reliable. Continuing with the chat app example I mentioned at the beginning of the chapter, we can do something like this:

index.html
<!DOCTYPE html>
<html lang="en">
<head>
 <meta charset="UTF-8">
 <meta name="viewport" content="width=device-width, initial-scale=1.0">
 <meta http-equiv="X-UA-Compatible" content="ie=edge">
 <title>Background Sync</title>
</head>
<body>
 <h1>Background Sync Demo</h1>
 <p>Open Chrome Dev Tools</p>
 <button id="myButton">Send Message</button>
 <script>
 if ('serviceWorker' in navigator) {
 // Register a service worker hosted at the root of the
 // site using a more restrictive scope.
 navigator.serviceWorker.register('/sw.js', {scope: './'}).then(
 (registration) => {
 console.log('Service worker registration succeeded:', registration);
 },
 (error) => {
 console.log('Service worker registration failed:', error);
 }
 );
 } else {
 console.log('Service workers are not supported.');
 }
 </script>
 <script src="./indexedDB.js"></script>
 <script src="./app.js"></script>
</body>
</html>
Next, we simulate a chat app. We add reliability by saving messages in a queue in IndexedDB, then send some messages to a fake API (see https://jsonplaceholder.typicode.com/guide.html for more information). When the response is successful, we delete the messages from the queue; otherwise, we try sending them later with Background Sync (Figure 7-2).
../images/483082_1_En_7_Chapter/483082_1_En_7_Fig2_HTML.jpg
Figure 7-2

Chat app workflow

We need a basic IndexedDB implementation to handle the queue logic.

indexedDB.js
var DB_VERSION = 1;
var DB_NAME = "messages";
var db;
var connection;
function openDatabase(){
 return new Promise(
 (resolve, reject) => {
 connection = self.indexedDB.open(DB_NAME, DB_VERSION);
 connection.onupgradeneeded = (upgradeDb) => {
 var db = upgradeDb.target.result;
 if (!db.objectStoreNames.contains('message-queue')) {
 db.createObjectStore('message-queue', {keyPath: 'title'});
 }
 };
 connection.onsuccess = (event) => {
 // Opening successful process.
 db = event.target.result;
 resolve(db);
 };
 connection.onerror = (event) => {
 // We handle the opening DB error.
 reject(event.target.errorCode);
 };
 }
 );
}
function writingObjectStore(data) {
 // It can be read-only or readwrite.
 var transaction = db.transaction(['message-queue'], 'readwrite');
 // Adding the data in objectStore.
 let objectStore = transaction.objectStore("message-queue");
 let request = objectStore.add(data);
 request.onerror = (e) => {
 };
 request.onsuccess = (e) => {
 console.log('Item added to indexedDB');
 };
}
function getAllMessagesFromQueue() {
 return new Promise(
 (resolve, reject) => {
 let transaction = db.transaction(["message-queue"], "readonly");
 // Adding the data in objectStore.
 let objectStore = transaction.objectStore('message-queue') ;
 let request = objectStore.getAll();
 request.onsuccess = () => {
 resolve(request.result);
 };
 request.onerror = () => {
 reject(request.error);
 }
 }
 );
}
function deleteMessageFromQueue(item) {
 console.log('deleteMessageFromQueue()');
 return new Promise(
 (resolve, reject) => {
 // Deleting a registry.
 let request = db.transaction(["message-queue"], "readwrite")
 .objectStore("message-queue")
 .delete(item.title);
 request.onsuccess = () => {
 // It was deleted successfully.
 console.log('message deleted', item);
 resolve(request.result);
 };
 request.onerror = () => {
 // It was deleted successfully.
 reject(request.error);
 };
 }
 );
}

Next we add the sync event to our service worker.

sw.js
importScripts("/indexedDB.js");
const url = 'https://jsonplaceholder.typicode.com';
self.addEventListener('install', function(event) {
 // Perform install steps.
});
self.addEventListener('sync', function(event) {
 console.log('I am in sync', event.tag);
 if(event.tag === 'message-queue') {
 event.waitUntil(syncMessages());
 }
});
function syncMessages() {
 console.log('syncMessages()');
 openDatabase()
 .then(() => {
 getAllMessagesFromQueue()
 .then((messages) => {
 console.log('messages', messages);
 solveMessages(messages);
 });
 })
 .catch((e)=>{console.log(e)});
}
function solveMessages(messages) {
 Promise.all(
 messages.map(
 (message) => {
 console.log('a message', message);
 fetch(`${url}/posts`,
 {
 method: 'post',
 body: JSON.stringify({
 title: message.title,
 body: message.body,
 userId: message.userId
 }),
 headers: {
 "Content-type": "application/json; charset=UTF-8"
 }
 }
 )
 .then((response) => {
 console.log('response>>>', response);
 return deleteMessageFromQueue(message);
 });
 }
 )
 )
}

In the service worker in the previous code, note the inclusion of importScripts() (for more information, go to https://developer.mozilla.org/en-US/docs/Web/API/WorkerGlobalScope/importScripts). With it, we can find libraries and other scripts in the service worker scope—in our case, the indexedDB.js script. We separate our code using syncMessages(), where we get all the messages in our queue, then call to solveMessages(), where we use fetch() to make a request to our API. If this call is successful, we delete the messages from our queue.

Last, we need to add functionality to our app.

App.js
let button = document.getElementById('myButton');
const messagesToSend = [
 {
 title: 'new Message',
 body: 'Hello there',
 userId: 1
 },
 {
 title: 'new Message 2',
 body: 'Hello there again',
 userId: 1
 },
 {
 title: 'new Message 3',
 body: 'Hello there again again',
 userId: 1
 },
 {
 title: 'new Message 4',
 body: 'Hello there again 4 times',
 userId: 1
 }
];
button.addEventListener('click', function(){
 messagesToSend.forEach(
 (item) => {
 sendMessage(item);
 }
 );
 messageQueueSync();
});
// Background Sync Mechanism Functions
function sendMessage(message) {
 let messageItem = {
 title: message.title,
 body: message.body,
 userId: message.userId
 };
 openDatabase()
 .then(() => {
 writingObjectStore(messageItem);
 })
 .catch((e)=>{console.log(e)});
}
function messageQueueSync() {
 navigator.serviceWorker.ready.then(
 (registration) => {
 registration.sync.register('message-queue');
 }
 );
}

In the previous code, we simulated sending four messages and continuing the process with our queue (Figure 7-3).

You can go there from the repo (https://github.com/carlosrojaso/appress-book-pwa) with
$ git checkout backgroundSync-plain
$ serve -S indexedDB
../images/483082_1_En_7_Chapter/483082_1_En_7_Fig3_HTML.jpg
Figure 7-3

Simulating chat messages online

Now we can simulate an offline chat by selecting the Offline check box from the Service Workers panel (Figure 7-4).
../images/483082_1_En_7_Chapter/483082_1_En_7_Fig4_HTML.jpg
Figure 7-4

Simulating chat messages offline

Then we bring back connectivity, as shown in Figure 7-5.
../images/483082_1_En_7_Chapter/483082_1_En_7_Fig5_HTML.jpg
Figure 7-5

Connectivity is back

Using Background Sync in VueNoteApp

Workbox makes it easy for us to implement Background Sync in our service worker. We need only modify our src/service-worker.js file and add workbox.backgroundSync.Plugin() (for more information, go to https://developers.google.com/web/tools/workbox/modules/workbox-background-sync).

../images/483082_1_En_7_Chapter/483082_1_En_7_Figa_HTML.gif

Now let’s create a notification that shows up when all the pending POST requests are resolved, which is handled by workbox.backgroundSync.Plugin() in the queueDidReplay callback.

In addition, we need to use the NetworkOnly() method because our API request call and and we are using workbox.routing. registerRoute(//*/, networkWithBackgroundSync, "POST"); to catch all the POST calls.

Then we add addtoApiNote() in Dashboard.vue to simulate a call to an API.

Dashboard.vue
<template>
 <div class="dashboard">
 <v-content>
 <Notes :pages="pages" @new-note="newNote" @delete-note="deleteNote"/>
 </v-content>
 <v-dialog v-model="dialog">
 <v-card>
 <v-card-title>
 <span class="headline">New Note</span>
 </v-card-title>
 <v-card-text>
 <v-container grid-list-md>
 <v-layout wrap>
 <v-flex xs12 sm12 md12>
 <v-text-field v-model="newTitle" value="" label="Title*" required></v-text-field>
 </v-flex>
 <v-flex xs12 sm12 md12>
 <v-textarea v-model="newContent" value="" label="Content"></v-textarea>
 </v-flex>
 </v-layout>
 </v-container>
 <small>*indicates required field</small>
 </v-card-text>
 <v-card-actions>
 <v-spacer></v-spacer>
 <v-btn color="blue darken-1" flat @click="closeModal()">Close</v-btn>
 <v-btn color="blue darken-1" flat @click="saveNote()">Save</v-btn>
 </v-card-actions>
 </v-card>
 </v-dialog>
 </div>
</template>
<script>
import {fireApp} from'../firebase.js'
import Notes from './Notes.vue'
const db = fireApp.database().ref();
export default {
 name: 'Dashboard',
 components: {
 Notes
 },
 data: () => ({
 pages:[],
 newTitle: ",
 newContent: ",
 index: 0,
 dialog: false
 }),
 mounted() {
 db.once('value', (notes) => {
 notes.forEach((note) => {
 this.pages.push({
 title: note.child('title').val(),
 content: note.child('content').val(),
 ref: note.ref
 })
 })
 })
 },
 methods: {
 newNote () {
 this.dialog = true;
 },
 saveNote () {
 const newItem = {
 title: this.newTitle,
 content: this.newContent
 };
 this.pages.push(newItem);
 this.index = this.pages.length - 1;
 db.push(newItem);
 this.addtoAPINote(newItem);
 this.resetForm();
 this.closeModal();
 },
 addtoAPINote(note) {
 fetch('https://jsonplaceholder.typicode.com/posts',
 {
 method: 'POST',
 body: JSON.stringify({
 title: note.title,
 body: note.body,
 userId: 1
 }),
 headers: {
 "Content-type": "application/json; charset=UTF-8"
 }
 }
 ).then(
 (response) => {
 // eslint-disable-next-line
 console.log('fetch call:', response);
 }
 ).catch(
 () => {
 // eslint-disable-next-line
 console.log('Error sending to API...');
 }
 );
 },
 closeModal () {
 this.dialog = false;
 },
 deleteNote (item) {
 let noteRef = this.pages[item].ref;
 if(noteRef) { noteRef.remove(); }
 this.pages.splice( item, 1);
 this.index = Math.max(this.index - 1, 0);
 },
 resetForm () {
 this.newTitle = ";
 this.newContent = ";
 }
 }
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>
With this method, when someone adds a note, the app makes an API call (Figure 7-6). You can go there from the repo (https://github.com/carlosrojaso/appress-book-pwa) with
$ git checkout backgroundSync
$ npm run build
$ serve -S dist
../images/483082_1_En_7_Chapter/483082_1_En_7_Fig6_HTML.jpg
Figure 7-6

Cleaning and rerunning our app with Background Sync enabled

Next we need to go to Chrome DevTools ➤ Application ➤ Background Sync and activate recording Background Sync (Figure 7-7).
../images/483082_1_En_7_Chapter/483082_1_En_7_Fig7_HTML.jpg
Figure 7-7

Activating recording Background Sync activity in Chrome DevTools

Now we need to turn off the computer network (Figure 7-8).
../images/483082_1_En_7_Chapter/483082_1_En_7_Fig8_HTML.jpg
Figure 7-8

Turning off networking in OS X

Now try to add a note (Figure 7-9).
../images/483082_1_En_7_Chapter/483082_1_En_7_Fig9_HTML.jpg
Figure 7-9

Background Sync activity

Figure 7-10 shows that Background Sync detects that the API calls were unsuccessful and saves then in IndexedDB.
../images/483082_1_En_7_Chapter/483082_1_En_7_Fig10_HTML.jpg
Figure 7-10

IndexedDB queue

Now let’s turn on the computer network (Figure 7-11).
../images/483082_1_En_7_Chapter/483082_1_En_7_Fig11_HTML.jpg
Figure 7-11

Turning on networking in OS X

Figure 7-12 shows the notification and the empty queue.
../images/483082_1_En_7_Chapter/483082_1_En_7_Fig12_HTML.jpg
Figure 7-12

Empty queue

Summary

Background Sync, combined with a service worker, grants us the ability to build PWAs. Using Background Sync is relatively easy. We simply register our event with SyncManager and react to the sync event. SyncManager is the service worker API that handles and registers synchronized events.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.17.5.68