Build an offline-first, Data-driven PWA

An overview of the

In this article, you’ll learn how to use Workbox and IndexedDB to create offline first, data-driven progressive Web applications (PWA). You can also use background synchronization to synchronize applications with the server while offline.

Will learn

  • How do I use Workbox caching applications
  • How do I use IndexedDB to store data
  • How do I retrieve and display data from IndexedDB when the user is offline
  • How do I save data offline
  • How do I use background synchronization to update applications offline

Should know

  • HTML, CSS, and JavaScript
  • ES2015 Promises
  • How to use the command line
  • Familiarize yourself with Workbox
  • Get familiar with Gulp
  • Familiarize yourself with IndexedDB

Required conditions

  • A computer with terminal/shell access permission
  • Chrome 52 or later
  • The editor
  • Nodejs and NPM

Set up the

You will need to install Nodejs if you don’t have one

Clone the warehouse quickly by using the following method

git clone https://github.com/googlecodelabs/workbox-indexeddb.gitCopy the code

Or download the zip package directly

Install dependencies and start the service

Go to your downloaded Git repository directory and go to the project folder

cd workbox-indexeddb/project/Copy the code

Then install the dependencies and start the service

npm install
npm startCopy the code

instructions

Json file. There are many dependencies, most of which are required by the development environment (you can ignore them). The main dependencies are:

NPM start builds and prints to the build folder, starts dev Server, and starts a gulp Watch task. Gulp Watch automatically builds files by listening for changes. Concurrently, concurrently run gulp and Dev Server

Open the application

Open Chrome and jump to Localhost :8081 and you’ll see a console with a list of events. In the permissions confirmation menu that pops up, click Allow

We used a notification system to inform users that the app’s background sync had been updated, and tried testing the add feature at the bottom of the page

instructions

The goal of this small project is to save the user’s event calendar offline. You can check the loadContentNetworkFirst method in app/js/main.js file to see how it currently works. It will first request the server, update the page if it succeeds, and print a message on the console if it fails. Next, let’s add some methods to make it available offline.

Cache app shell

Write the service worker

To work offline, you need a server worker.

Add the following code to app/ SRC /sw.js

ImportScripts (' workbox - sw. Dev. V2.0.0. Js'); ImportScripts (' workbox - background - sync. Dev. V2.0.0. Js'); const workboxSW = new WorkboxSW(); workboxSW.precache([]);Copy the code

instructions

We introduced workbox-SW and workbox-background-sync at the beginning

  • workbox-swContains theprecacheAnd adding routes to the service worker
  • workbox-background-syncIs a library that synchronizes behind the scenes in the service worker, as discussed later

The precache method takes an array of file lists, first with an empty one, and then computes the array using workbox-build.

Build the service worker

Workbox’s building blocks, such as workbox-build, are recommended

Add the following code to project/gulpfile.js

gulp.task('build-sw', () => {
  return wbBuild.injectManifest({
    swSrc: 'app/src/sw.js',
    swDest: 'build/service-worker.js',
    globDirectory: 'build',
    staticFileGlobs: [
      'style/main.css',
      'index.html',
      'js/idb-promised.js',
      'js/main.js',
      'images/**/*.*',
      'manifest.json'
    ],
    templatedUrls: {
      '/': ['index.html']
    }
  }).catch((err) => {
    console.log('[ERROR] This happened: ' + err);
  });
});Copy the code

Now uncomment some comments:

gulpfile.js:

// uncomment the line below:
const wbBuild = require('workbox-build');
// ...
gulp.task('default', ['clean'], cb => {
  runSequence(
    'copy',
    // uncomment the line below:
    'build-sw',
    cb
  );
});Copy the code

Ctrl + C to exit the current process and run NPM start again. You can see that the service worker file is generated in build/service-worker.js

Uncomment the service worker registration code in app/index.html

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

Save the changes, refresh the browser and the service worker is installed. Ctrl + C to close dev Server, go back to the browser to refresh the page, and you are ready to run offline!

instructions

In this step, the workbox-build and build-sw tasks are merged into our gulp file, SwSrc (app/ SRC /sw.js) generates service work into swDest(build/service-worker.js). The staticFileGlobs file from globDirectory(build) is injected into build/service-worker.js for precache invocation, along with a revised hash for each file. The templatedUrls option tells Workbox that our site responds to the request with the content of index.html.

Post a link to injectManifest by the way

Install the generated service worker cache app shell resource file, Workbox will automatically delete:

  • Set a cache priority policy for cache resources to allow applications to load offline
  • When service work is updated, a revision hash is used to update cached files

Create the IndexedDB database

So far we have not been able to load data offline, so we will create an IndexDB to hold the program’s data, named Dashboardr

Add the following code to app/js/main.js

function createIndexedDB() { if (! ('indexedDB' in window)) {return null; } return idb.open('dashboardr', 1, function(upgradeDb) { if (! upgradeDb.objectStoreNames.contains('events')) { const eventsOS = upgradeDb.createObjectStore('events', {keyPath: 'id'}); }})}Copy the code

Uncomment the call to createIndexedDB:

const dbPromise = createIndexedDB();Copy the code

Save the file and restart the server:

npm startCopy the code

Go back to the browser refresh page, activate skipWaiting and refresh the page again. In Chrome, you can select Service Workers from the Application panel in developer Tools and click On skipWaiting. Then use developer tools to check if the database exists. In Chrome you can see if the Events object exists by clicking IndexedDB in the Application panel and selecting dashboardr.

Note: The Developer Tools IndexedDB UI may not accurately reflect the state of your database. In Chrome you can refresh the database to view, or reopen the Developer Tools

instructions

In the above code, we create a Dashboardr database, set its version number to 1, and then check if the Events object exists. This check is to avoid potential errors, and we also provide a unique key path ID for events.

Since we have modified the app/main.js file, the Gulp Watch task is automatically built, the Workbox automatically updates and modifies the hash, and then intelligently updates main.js in the cache.

Save data to IndexedDB

Now we save the data to the Event object in our newly created database Dashboardr.

function saveEventDataLocally(events) { if (! ('indexedDB' in window)) {return null; } return dbPromise.then(db => { const tx = db.transaction('events', 'readwrite'); const store = tx.objectStore('events'); return Promise.all(events.map(event => store.put(event))) .catch(() => { tx.abort(); throw Error('Events were not added to the store'); }); }); }Copy the code

Then update the loadContentNetworkFirst method, now this is the complete method:

function loadContentNetworkFirst() {
  getServerData()
  .then(dataFromNetwork => {
    updateUI(dataFromNetwork);
    saveEventDataLocally(dataFromNetwork)
    .then(() => {
      setLastUpdated(new Date());
      messageDataSaved();
    }).catch(err => {
      messageSaveError(); 
      console.warn(err);
    });
  }).catch(err => { // if we can't connect to the server...
    console.log('Network requests have failed, this is expected if offline');
  });
}Copy the code

Uncomment the saveEventDataLocally call in addAndPostEvent

function addAndPostEvent() {
  // ...
  saveEventDataLocally([data]);
  // ...
}Copy the code

Save the file and refresh the page to reactivate the service worker. Refresh the page again to check that the data from the web is saved to events (you may need to refresh the IndexedDB in the developer tools)

instructions

SaveEventDataLocally takes an array and stores it one by one into the IndexedDB database. We write store. Put in promise.all so that we can terminate the transaction if an update fails.

The loadContentNetworkFirst method updates the IndexedDB and the page as soon as it receives data from the server. Then, when the data is saved successfully, the timestamp is stored and the user is notified that the data is available for offline use.

Calling the saveEventDataLocally method in addAndPostEvent ensures that the latest data is stored locally when a new event is added.

Get data from IndexedDB

When offline, we need to query locally cached data.

Add the following code to app/js/main.js:

function getLocalEventData() { if (! ('indexedDB' in window)) {return null; } return dbPromise.then(db => { const tx = db.transaction('events', 'readonly'); const store = tx.objectStore('events'); return store.getAll(); }); }Copy the code

Then update the loadContentNetworkFirst method as follows:

function loadContentNetworkFirst() {
  getServerData()
  .then(dataFromNetwork => {
    updateUI(dataFromNetwork);
    saveEventDataLocally(dataFromNetwork)
    .then(() => {
      setLastUpdated(new Date());
      messageDataSaved();
    }).catch(err => {
      messageSaveError();
      console.warn(err);
    });
  }).catch(err => {
    console.log('Network requests have failed, this is expected if offline');
    getLocalEventData()
    .then(offlineData => {
      if (!offlineData.length) {
        messageNoData();
      } else {
        messageOffline();
        updateUI(offlineData); 
      }
    });
  });
}Copy the code

Save the file, refresh the browser to activate the updated service worker, now Ctrl + C to close dev Server, go back to the browser to refresh the page, now app and data can be loaded offline!

instructions

If loadContentNetworkFirst is called without a network connection, getServerData is rejected and then thrown into a catch, and getLocalEventData calls the locally cached data. If there is a network connection, the server and updateUI will be requested normally

Using workbox – background – sync

Our app can already save and browse data offline. Now we will use Workbox-background-sync to synchronize data saved offline to the server.

Add the following code to app/ SRC /sw.js

let bgQueue = new workbox.backgroundSync.QueuePlugin({ callbacks: { replayDidSucceed: async(hash, res) => { self.registration.showNotification('Background sync demo', { body: 'Events have been updated! '}); }}}); workboxSW.router.registerRoute('/api/add', workboxSW.strategies.networkOnly({plugins: [bgQueue]}), 'POST' );Copy the code

Save, now go to the command line:

npm run startCopy the code

Refresh the browser and activate the updated service worker

Ctrl + C takes the app offline and adds an Event to confirm that the request/API /add has been added to the QueueStore object of bgQueueSyncDB.

instructions

When a user tries to add an event offline, workbox-background-sync saves the failed request as an offline queue, and backgroundSync resends the request when the user reconnects, without even opening the app! However, the process from networking to resending the request takes about 5 minutes. In the next section, we will show how to send the request immediately in the app.

Retransmission request

Because there is a delay in resending the request, the user may not have synchronized the data after returning to the APP, so we send these requests immediately when the user is connected to the Internet.

Add the following code to app/ SRC /sw.js

workboxSW.router.registerRoute('/api/getAll', () => {
  return bgQueue.replayRequests().then(() => {
    return fetch('/api/getAll');
  }).catch(err => {
    return err;
  });
});Copy the code

Whenever the user requests server data (when a page is loaded or refreshed), the route replays the queued request and returns the latest server data. That’s fine, but the user still has to refresh the page to retrieve the data, and we can do better.

Add the following code to app/js/main.js

window.addEventListener('online', () => {
  container.innerHTML = '';
  loadContentNetworkFirst();
});Copy the code

Restart the server

npm startCopy the code

Refresh the browser to activate the new service worker and refresh the page again.

Ctrl + C takes app offline

Add an Event

Restart the server

npm startCopy the code

You should immediately receive a notification of data updates to check if the data in server-data/events.json has been updated.

instructions

The page loads with a request/API /getAll. We intercept this request and do two main things:

  • Synchronize local offline data
  • To request/api/getAll

This means synchronizing data before retrieving it from the server

Note: The network request design in this example is very simple, in reality you may need to consider more factors to reduce the number of requests.

Add delete function

Now it’s up to you to add a delete function, remember to delete data in IndexedDB.