The Offline Cookbook by Jake Archibald

Front-end Offline Guide (Part 2)

Using AppCache provides several modes for content to work offline. If these patterns are what you need, congratulations, you hit the APPCache jackpot (though the top prize is still unclaimed), but the rest of us are rocking in the corner. The author refers to the fact that AppCache was gradually removed from Web standards for design reasons, and while some browsers still support it, it’s best not to use it anymore.)

For ServiceWorker (introduction), we give up trying to solve offline problems and provide developers with flexible components to solve offline problems on their own. It gives you control over caching and handling requests. This means you can create your own schema. Let’s take a look at several possible patterns in isolated environments, but in practice, you might use multiple of them in tandem, depending on the URL and context.

Currently, all of the sample code runs in Chrome and Firefox browsers unless otherwise noted. For complete details on ServiceWorker support, see “Is ServiceWorker Ready?” .

For a running demo of some of these modes, check out sand-to-thrill, and the video here will show you the performance impact.

Cache – When to start storing resources?

You can handle requests from the cache independently through the ServiceWorker, so we’ll examine them separately first. First, when should we cache?

Install – as a dependency

ServiceWorker provides you with an Install event that you can use to prepare resources, which must be prepared in advance before any other event can be processed. But while these operations are in progress, any older versions of ServiceWorker are still running and available to the page, so what you do here must not interrupt them.

For: CSS, images, fonts, JS files, templates, etc., basically all the static resources you think your site should need in the current “version”.

If you do not get these resources, your site will not work at all and the corresponding native application will include these objects in the initial download.

self.addEventListener('install'.function(event) {
  event.waitUntil(
    caches.open('mysite-static-v3').then(function(cache) {
      return cache.addAll([
        '/css/whatever-v3.css'.'/css/imgs/sprites-v6.png'.'/css/fonts/whatever-v8.woff'.'/js/all-min-v4.js'
        // etc]); })); });Copy the code

Event. waitUntil accepts a Promise object to define the installation duration and whether the installation is successful. If the Promise state is Rejected, the installation fails. And discard the ServiceWorker (if an older version of ServiceWorker is running, it will remain the same). Caches. Open and caches. AddAll both return promise objects, and caches. AddAll calls reject if either resource fails. In the case of sand-to-thrill, I used this method to cache static resources.

Install – not as a dependency

This approach is similar to the above, but the difference is that even if the cache fails, the installation is neither delayed nor caused to fail.

Good for: large, temporarily unavailable resources, such as higher-level resources for games.

self.addEventListener('install'.function(event) {
  event.waitUntil(
    caches.open('mygame-core-v1').then(function(cache) {
      cache.addAll(
        // levels 11-20
      );
      return cache.addAll(
        // core assets & levels 1-10); })); });Copy the code

We did not return the cache.addAll Promise object with levels 11-20 to Event. waitUntil, so the event can be used offline even if it fails. Of course, you must allow for the absence of these levels and try to re-cache them if they are missing.

The ServiceWorker may terminate while Level 11-20 is downloading because it has finished processing the event. This means they won’t be cached. In the future, we plan to add an API for downloading in the background to handle situations like this, as well as downloading large files like movies.

When activated

Applicable to: Cleanup and migration

If the new ServiceWorker has been installed and an earlier version of the SW is not in use, the new ServiceWorker will be activated and you will get an Activate event. This is a good time to deal with schema migration and removal of unused caches in IndexedDB due to the retirement of the older version.

self.addEventListener('activate'.function(event) {
  event.waitUntil(
    caches.keys().then(function(cacheNames) {
      return Promise.all(
        cacheNames.filter(function(cacheName) {
          // Return true if you want to delete the cache,
          // But remember to cache between all pages in this domain
          // It is shared
        }).map(function(cacheName) {
          returncaches.delete(cacheName); })); })); });Copy the code

During activation, events such as fetch are placed in a queue, so a long activation can block page loading. Keep your activation as clean as possible and only for actions that cannot be performed when the older version is active.

On sand-to-thrill, I used this method to remove the old cache.

During user interaction

For: If the entire site doesn’t work offline, you can allow users to select available content that needs to be offline, for example, a YouTube video, a Wikipedia article, a Photo from Flickr, and so on.

Provide users with a “read later” or “save offline” button. When you click the button, get the content you need from the network and put it in the cache.

document.querySelector('.cache-article').addEventListener('click'.function(event) {
  event.preventDefault();

  var id = this.dataset.articleId;
  caches.open('mysite-article-' + id).then(function(cache) {
    fetch('/get-article-urls? id=' + id).then(function(response) {
      // /get-article-urls returns a JSON-encoded array of
      // resource URLs that a given article depends on
      return response.json();
    }).then(function(urls) {
      cache.addAll(urls);
    });
  });
});
Copy the code

CacheAPI is available on the page from the ServiceWorker as well as on the page, which means you don’t have to use the ServiceWorker to add content to the cache.

Network response time

Good for: frequently updated resources, such as user inboxes, or article content. The same applies to content that is not important but needs to be handled with care, such as user avatars.

If the requested resource does not match any of the resources in the cache, it is fetched from the network, sent to the page, and added to the cache.

If you do this for a range of urls, such as avatars, you need to be careful not to overpopulate storage under domain names, and you don’t want to be a prime candidate if users need to reclaim disk space. Be sure to remove items that are no longer needed from the cache.

self.addEventListener('fetch'.function(event) {
  event.respondWith(
    caches.open('mysite-dynamic').then(function(cache) {
      return cache.match(event.request).then(function (response) {
        return response || fetch(event.request).then(function(response) {
          cache.put(event.request, response.clone());
          returnresponse; }); }); })); });Copy the code

For efficient use of memory, it is only allowed to read the body of a response or request once. In the above code, use.clone to create an additional copy of the data that can be read separately.

On sand-to-thrill, I used this method to cache Flickr images.

Stale-while-revalidate

Good for: Frequent updates, but no need to get the latest resources. User avatars fall into this category.

If a version is already available in the cache, it is used directly, but an updated version is retrieved for the next request.

self.addEventListener('fetch'.function(event) {
  event.respondWith(
    caches.open('mysite-dynamic').then(function(cache) {
      return cache.match(event.request).then(function(response) {
        var fetchPromise = fetch(event.request).then(function(networkResponse) {
          cache.put(event.request, networkResponse.clone());
          return networkResponse;
        })
        returnresponse || fetchPromise; })})); });Copy the code

It is very similar to HTTP stale-while-revalidate.

When pushing a message

Note: Chrome doesn’t support Push yet. (Chrome 50 and later support it, see Can I Use for more information)

The Push API is another feature built on ServiceWorker. It allows the ServiceWorker to wake up in response to messages from system services, and the Push API works even if the user doesn’t have a label open for your site. Only ServiceWorker is awakened. The user is prompted when you request permission to perform this action from the page.

Good for: notification related content, such as chat messages, breaking news, or emails. The same applies to instantly synchronized content that changes infrequently, such as to-do list updates or calendar changes.

A common page representation for users is a notification that, when clicked, opens or focuses on the relevant page, but be sure to update the cache before clicking. Obviously, users must be online when they receive a push message, but they may be offline when they finally interact with the notification, so it is important to allow offline access to this content. The Twitter native app is a great offline first example in most cases, but it’s a bit of a problem here.

Twitter can’t provide content related to its push messages without an Internet connection. But clicking on the notification removes the link, leaving the user with even less information than before clicking on the notification. Don’t do it!

The following code updates the cache before displaying the notification.

self.addEventListener('push', (event) => {
  if (event.data.text() == 'new-email') {
    event.waitUntil(async function() {
      const cache = await caches.open('mysite-dynamic');
      const response = await fetch('/inbox.json');
      await cache.put('/inbox.json', response.clone());
      const emails = await response.json();
      registration.showNotification("New email", {
        body: "From " + emails[0].from.name
        tag: "new-email"
      });
    }());
  }
});

self.addEventListener('notificationclick'.function(event) {
  if (event.notification.tag == 'new-email') {
    // Assume that all of the resources needed to render
    // /inbox/ have previously been cached, e.g. as part
    // of the install handler.
    new WindowClient('/inbox/'); }});Copy the code

Background synchronization

Note: Background synchronization has not been added to the stable version of Chrome. (Can I Use is available in Chrome 49 and later, but not in FireFox or Safari.)

Background synchronization is another feature built on ServiceWorker. It allows you to request background data synchronization at one time or at (very instructive) intervals. Background synchronization works even if the user doesn’t open a TAB for your site. Only ServiceWorker is awakened. The user is prompted when you request permission to perform this action from the page.

Good for: non-urgent updates, especially those that are regularly updated and sending a push message every time is too frequent, such as social schedules and news feeds.

Read on: Front-end Offline Guide (II)