Home | Send Feedback

A closer look at the Cache API

Published: January 09, 2018  •  pwa, javascript

The Cache API is a new interface that comes together with the Service Worker to the browsers. The main purpose is to enable Service Workers to cache network request so they can send back responses to foreground scripts even when the browser is offline.
It is primarily designed for storing network requests and their responses but you can store anything in the cache and use it as a general purpose storage mechanism.

The main entry point to the API is the global caches object. The API is not limited to Service Workers. You can access caches from a foreground script (window) a Web Worker and iframe. All have access to the same caches from the same origin.

CacheStorage

caches is an instance of type CacheStorage and provides the following 5 methods. All methods in the Cache API are asynchronous and return a Promise.

open(cacheName)
Opens or creates a cache. An origin can have multiple, named caches. If the specified cache does not exist, a new cache is created with the provided cacheName.

const cache = await caches.open('testCache');

has(cacheName)
Checks if a cache with the specified cacheName exists. Returns true when it exists.

const exist = await caches.has('testCache');
// true

delete(cacheName)
Deletes the cache with the specified cacheName. When the cache exist, deletes the cache and returns true, otherwise returns false.

const deleted = await caches.delete('testCache');
// true

keys()
Returns the names (string) of all available caches in an array.

const cacheNames = await caches.keys();
// ["testCache"]

match(request, options)
This method is a convenience method, that calls the match() method on each existing cache. See cache.match() description a bit further below.
This method processes the caches in the same order as caches.keys() returns the names.
The second parameter is optional and supports the same options as cache.match() to control the matching process.
This method supports one additional option: cacheName specifies the cache to search within.

  const chuckCache = await caches.open('chuckCache');
  chuckCache.addAll([
        'https://api.icndb.com/jokes/15',
        'https://api.icndb.com/jokes/16',
        'https://api.icndb.com/jokes/17'
  ]);
      
  const response1 = await caches.match('https://api.icndb.com/jokes/15', {cacheName: 'chuckCache'});
  const response2 = await chuckCache.match('https://api.icndb.com/jokes/15');

These two match calls are equivalent. caches.match() is useful when you have many caches and need to search a resource in all of them.

Cache

The Cache object that caches.open() returns provides the following seven methods. All of these methods work with Request and Response objects. Instead of a Request you can provide a string. The Cache API internally converts the string to a Request object with new Request(string).

put(request, response)
Adds a key value pair to the cache. put() overwrites an existing entry with the same key.

  const chuckCache = await caches.open('chuckCache');      
  const url = 'https://api.icndb.com/jokes/23';
  const response = await fetch(url);
  chuckCache.put(url, response);

add(request)
addAll([request, request, request, ...])
Takes one or more URLs, fetches them and adds the response(s) to the cache.

  const chuckCache = await caches.open('chuckCache');      
  chuckCache.add('https://api.icndb.com/jokes/24');
  chuckCache.addAll([
        'https://api.icndb.com/jokes/25',
        'https://api.icndb.com/jokes/26',
        'https://api.icndb.com/jokes/27'
  ]);

add() is equivalent to the following code

  const response = await fetch(url);
  if (response.ok) {
    await cache.put(url, response);
  }

add() and addAll() do not cache responses with a status that is not in the 200 range. put() on the other hand stores any request / response pair.


match(request, options)
Returns a response associated with the first matching request. Returns undefined when no matching request exist. The second parameter is optional and is an object with options that control the matching process. The following options are supported:


matchAll(request,options)
Works the same as match() but instead of returning the first response that matches (response[0]), matchAll() returns all matching responses in an array. You can also call matchAll() without any parameter and the method returns all cached responses.

  const chuckCache = await caches.open('chuckCache');      
  await chuckCache.add('https://api.icndb.com/jokes/30');
  await chuckCache.add('https://api.icndb.com/jokes/40');
  await chuckCache.add('https://api.icndb.com/jokes/50');
  const first = await chuckCache.match('https://api.icndb.com/jokes/30');
  // Response {type: "cors", url: "https://api.icndb.com/jokes/30"
      
  const all = await chuckCache.matchAll()
  /*
  [
     Response {type: "cors", url: "https://api.icndb.com/jokes/30", …}
     Response {type: "cors", url: "https://api.icndb.com/jokes/40", …}
     Response {type: "cors", url: "https://api.icndb.com/jokes/50", …}
  ]
  */

delete(request,options)
Returns true when it found a matching entry and deleted it, otherwise returns false.

const chuckCache = await caches.open('chuckCache');      
const request = new Request('https://api.icndb.com/jokes/24');
await chuckCache.add(request);
const deleted = await chuckCache.delete(request)
// true

This method also takes the same options object as cache.match() as the second optional parameter which allows you to delete multiple Request/Response pairs for the same URL.


keys(request,options)
Returns an array of keys (requests). You can either call the keys() method without a parameter then it returns all keys that are stored in the cache or you can specify a request and it will only return matching keys. keys() support the same options as the cache.match() method as the second optional parameter. The keys are returned in the same order that they were inserted.

 const chuckCache = await caches.open('chuckCache');      
 await chuckCache.add('https://api.icndb.com/jokes/30');
 await chuckCache.add('https://api.icndb.com/jokes/40');
 await chuckCache.add('https://api.icndb.com/jokes/50');
 const allKeys = await chuckCache.keys();
 /*
      [
        Request {method: "GET", url: "https://api.icndb.com/jokes/30", …},
        Request {method: "GET", url: "https://api.icndb.com/jokes/40", …}
        Request {method: "GET", url: "https://api.icndb.com/jokes/50",  …}
      ]
 */
      
 const oneKey = await chuckCache.keys('https://api.icndb.com/jokes/40')
 /*
      [
        Request {method: "GET", url: "https://api.icndb.com/jokes/40", …}
      ]
 */

General purpose cache

In the previous section all the example use the Fetch API to request a resource from a server and then store the response in the cache. The Cache API is primarily built for this use case and therefore can only store a Request object as the key and a Response object as the value.

But Request and Response objects can contain any kind of data that can be transferred over HTTP. The Response constructor supports different types like Blobs, ArrayBuffers, FormData and strings.

The following example maps a string key to a string value

  const languageCache = await caches.open('languages');
  await languageCache.put('de', new Response('German'));
  await languageCache.put('en', new Response('English'));
  await languageCache.put('fr', new Response('French'));
  
  const allKeys = await languageCache.keys();
  /*
  [
    Request {method: "GET", url: "http://localhost:8100/de", …}
    Request {method: "GET", url: "http://localhost:8100/en", …}
    Request {method: "GET", url: "http://localhost:8100/fr", …}
  ]
  */
  
  const en = await languageCache.match('en');
  const enText = await en.text();
  // English
  
  const deleted = await languageCache.delete('fr');
  // true
  
  const fr = await languageCache.match('fr');
  // undefined

Example with JSON.

  const usersCache = await caches.open('users');
  await usersCache.put('1', new Response('{"id": 1, "name": "John", "age": 27 }'));
  const user1 = await usersCache.match('1');
  const user1json = await user1.json();
  // {id: 1, name: "John", age: 27}

The Response object provides several methods to access the body: arrayBuffer(), blob(), text(), json(), formData().
Depending on the type of the Response body you need to call the appropriate method. All these methods are asynchronous and return a Promise.

Example

As mentioned at the beginning of the blog post the global caches object is not only accessible in the Service Worker, you can also access it from the window object. And both have access to the same caches of the same origin.

This example shows you a use case for that. It's an Ionic app that fetches and stores four pictures in the Service Worker and then in the foreground script accesses the cache and displays all stored pictures.

This could be useful for applications that support an offline mode and need a way to only display items that are cached.

Implementation

The Service Worker listens for the install event and then calls the loadPictures() method. In that method it creates a cache images, fetches and stores four pictures with the cache.addAll() method. After that, it sends a message imagesCached to all clients that are associated with this Service Worker.

self.addEventListener('install', event => event.waitUntil(loadPictures()));

async function loadPictures() {
  const cache = await caches.open('images');

  const pictures = [
    'https://demo.rasc.ch/img/pexels-photo-127753.jpeg',
    'https://demo.rasc.ch/img/pexels-photo-132037.jpeg',
    'https://demo.rasc.ch/img/pexels-photo-248771.jpeg',
    'https://demo.rasc.ch/img/pexels-photo-248797.jpeg'
  ];
  await cache.addAll(pictures);

  const allClients = await clients.matchAll({includeUncontrolled: true});
  for (const client of allClients) {
    client.postMessage('imagesCached');
  }

}




service-worker.js

In the Ionic app we call the listCache() method from the ngOnInit method. The very first time when listCache() is called it is possible that the cache is empty because the Service Worker is not installed yet or it is installed but the images are not downloaded yet. Therefore, we install a message event listener and wait for the imagesCached message. The Service Worker will send this message as soon as all pictures are fetched and stored. When that happens, we can call the listCache() method to display the pictures. The message handler is called each time we install a new Service Worker.

import {Component, OnInit} from '@angular/core';

@Component({
  selector: 'app-home',
  templateUrl: 'home.page.html',
  styleUrls: ['home.page.scss']
})
export class HomePage implements OnInit {

  pictures: string[] = [];

  constructor() {
    navigator.serviceWorker.addEventListener('message', event => {
      if (event.data === 'imagesCached') {
        this.listCache();
      }
    });
  }

  ngOnInit() {
    this.listCache();
  }

  async listCache() {
    this.pictures = [];
    const cache = await caches.open('images');
    const responses = await cache.matchAll();
    responses.forEach(async response => {
      const ab = await response.arrayBuffer();
      const imageStr = 'data:image/jpeg;base64,' + btoa(String.fromCharCode.apply(null, new Uint8Array(ab)));
      this.pictures.push(imageStr);
    });
  }


}

home.page.ts

The listCache() method opens the images cache, retrieves all entries with matchAll() and then loops over each response with forEach.
Inside the loop it extracts the ArrayBuffer of the Response and creates a data URL. On the HTML template it loops over all these data URLs and creates an img tag for each entry.

<ion-header>
  <ion-toolbar>
    <ion-title>
      Ionic Blank
    </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content padding>
  <ion-slides pager="true">
    <ion-slide *ngFor="let pic of pictures">
      <img [src]="pic">
    </ion-slide>
  </ion-slides>
</ion-content>

home.page.html

You find the source code for this example on GitHub: https://github.com/ralscha/blog/tree/master/sw-cache

For further information about the Cache API, visit these pages: