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:
- workbox-sw Workbox
- Workbox-background-sync is used by Workbox for background synchronization, which will be covered later
- Gulp and Workbox-build are build tools
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-sw
Contains theprecache
And adding routes to the service workerworkbox-background-sync
Is 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.