[TOC]
Imagining the example of an typical e-commerce site with multiple kinds of data, there are images and numeric data, as well as HTML and CSS resources, and probably JavaScript as well. The question here is, which data should go where?
put in Cache resources addressed through a URL will be stored in the cache; i.e assets, static, URL addressable
App->Cache API: data (urls)
Network->App: data (urls)
put in IndexedDB dynamic data, json data
App->IndexedDb: data (json)
Network->App: data (json)
Tip: Start by storing data locally.
Here we're using the cache API and the IndexedDB API, also known as IDB. Let's look at storing JSON data with IDB. So what does IDB look like? It does not look like a conventional SQL table. IDB data is stored as key value pairs in objects stores. A single IDB database can have multiple object stores. This could be a clothing object store inside a products database.
# | Key (key-path 'id') | Value |
---|---|---|
0 | 123 | {id:123, name:'jacket', price: 22.13, quantity: 10} |
1 | 987 | {id:987, name:'sweater', price: 50.20, quantity: 20} |
2 | 456 | {id:456, name:'hoodie', price:45.65, quantity: 5} |
Now the activate event is a good place to create an IDB database.
self.addEventListener('activate',
function (event) {
event.waitUntil(
createDB()
);
});
- you don't want to create it more often than needed for efficiency's sake.
- doing that during installation could cause issues with the existing service worker.
Note: event.waitUntil ensures that the service worker does not terminate preemptively during async actions.
function createDB() {
productsDB = idb.open('products', 1
function (upgradeDB) {
var store = upgradeDB.createObjectStore(
'clothing', {keyPath: 'id'}
);
store.put(
{id: 123, name: 'jacket',
price: 12.15, quantity: 20}
);
});
}
Here we create an IDB products database. It is version one. Inside the products database we create a clothing object store. This will hold all of the clothing objects. The clothing object store has a key path of ID. Now this means that the objects in this store will be organized and accessed by the ID property of the clothing objects.
Note: that we're using Jake Archibald's IndexedDB promised library to enable promised syntax with IDB.
self.addEventListener('install',
function (event) {
event.waitUntil(
cacheAssets()
);
});
So let's look at storing resources with the cache API. Now, a common pattern is to cash assets on service worker installation.
Note: that event.waitUntil ensures that the service worker does not terminate preemptively during async actions
###Caching assets
function cacheAssets() {
return caches.open('cache-v1')
.then(function (cache) {
return cache.addAll([
'index.html',
'styles/main.css',
'img/jacket.jpg'
]);
});
}
In this example, we create a cache v1 cache and store static assets-- that's HTML, CSS, JavaScript, images and so on-- with the cache API.
Now we can get data from IDB instead of the network.
App->IndexedDb: data (json)
IndexedDb->App: data (json)
Network-->App: X
function readDB() {
productsDB
.then(function (db) {
var tx = db.transaction(
['clothing'], 'readonly'
);
var store = tx.objectStore('clothing');
return store.getAll();
})
.then(function(items) {
/* Use clothing data */
});
}
Above in the example, we open the products database and create a new transaction on the clothing store of read only type. We don't need to write data. We can then access the store and retrieve all of the items. These items can then be used to update the UI, or whatever is needed.
note: a word on transactions. These are a wrapper around an operation, or group of operations, to ensure database integrity. If one of the actions within a transaction fails none of them are applied and the database returns to the state it was in before the transaction began. All read or write operations in IndexedDB must be part of a transaction. This allows for atomic read, modify, or write operations without worrying about other threads acting on the database at the same time.
App->Cache API: data (urls)
Cache API->App: data (urls)
Network-->App: X
self.addEventListener('fetch', function (event) {
event.respondWith(
caches.match(event.request).then(
function(response) {
// check cache but fallback to network
return response || fetch(event.request);
})
);
});
Now we can also get resources like HTML, CSS, JavaScript, and images from the cache instead of the network. Here we add a fetch listener to the service worker. When a fetch is made for a resource, we try to find the resource in the cache and return it. However, if it isn't in the cache we still want to go to the network for it.
App->IndexedDB: data (urls)
App-->Network: X
Network-->App: X
What can we do about actions that a user takes when they're offline? For example, purchasing a product. We can record them in IDB. It's possible to record actions a user takes while offline.
####Record failed requests
fetch('purchaseUrl', {
method: 'post',
body: 'item-123'}
)
.then(function (response) {
// normal tx
})
.catch(function (error) {
// save item/action in IDB
});
In the example above a user is trying to make an item purchase via HTTP request, which will fail if offline. If the purchase fails, the catch block will execute. This is where we could store the item or user action in IDB.
Question: How do we use this once connectivity returns?
IndexedDB->App: data
Network->App: data
// Get save ditesm from IDB, then
fetch('purchaseUrl', {
method: 'post',
body: item
})
.then(function (response) {
// Success. Delete IDB/action
});
Well, once connectivity returns the recorded items or actions in IDB can be retrieved and sent. If the requests are successful, the corresponding item action can be deleted from the IDB records.
- Offline Storage PWA's
- Detailed support for testing
- support for IndexedDB
- IndexedDB Promise
- Pokedex.org
- Cache API support