PWA Learning and Practice series articles have been compiled into gitbook-PWA Learning Handbook, and the text has been synchronized to learning-PWA-ebook. Please indicate the author and source of reprint.

This is the third article in the PWA Learning & Practice series. All the code in this article can be found in the sw-cache branch of learning-pWA (after git clone, note to switch to sw-cache branch).

PWA, as one of the hottest technology concepts at present, has great significance for improving the security, performance and experience of Web applications, and is worth our understanding and learning. If you are interested in PWA, please pay attention to the PWA Learning and Practice series.

1. The introduction

One of the fascinating features of PWA is its ability to make it offline.

Offline is just one of its functions. Specifically, it can:

  • Make our Web App accessible and even use some of its functions when offline, instead of showing the “No Network connection” error page;
  • In the case of weak network, we can use cache to quickly access our application and improve the experience;
  • Under normal network conditions, we can also save some request bandwidth through various self-controlled caching methods.

All of these are actually attributed to the Service Worker, the hero behind PWA.

So what is a Service Worker? You can think of a Service Worker simply as a process running in the background, independent of the front end. Therefore, it does not block the execution of browser scripts and does not directly access browser-specific apis (such as DOM, localStorage, etc.). In addition, your Web App will work even after you leave it, or even after you close the browser. It’s like a busy bee working behind the Web application, handling caching, push, notification, and synchronization. Therefore, the only way to learn PWA is Service Worker.

In the next few articles, I will introduce related principles and technical implementation from the perspectives of how to use Service Worker to realize resource caching, message push, message notification and background synchronization. These parts will be the focus of PWA technology. It is important to note that the specification states that a Service Worker can only run in an HTTPS domain due to its powerful capabilities. What if we didn’t have HTTPS when we were developing? Don’t worry, the nice thing is that the Service Worker can also run in the localhost (127.0.0.1) domain for local development.

Ok, after a brief understanding of Service Worker and its functions, we will return to the theme of this article, namely, the first part of Service Worker — how to use Service Worker to realize the caching of front-end resources, so as to improve the access speed of products and make them available offline.

2. How does Service Worker become available offline?

This section shows you how the Service Worker allows us to access the Web App while offline. Of course, offline access is just one manifestation.

First, let’s think about what we’re actually doing when we visit a Web site. In general, we get resources by connecting to the server, and then some of the resources we get will request new resources (such as CSS and JS used in HTML). So, in coarse-grained terms, when we visit a website, we are getting/accessing these resources.

As you can imagine, when we are offline or in a weak network environment, we cannot access these resources effectively, which is a key constraint for us. Therefore, the most intuitive thought is: If we cache these resources and, in some cases, make network requests local, would this solve the problem? Yes. However, this requires that we have a local cache, which can flexibly access various resources locally.

Having a local cache is not enough. We also need to be able to use, update, and clear caches efficiently, and further apply various personalized cache strategies. This requires that we have a “worker” that can control caching — which is part of what a Service worker does. By the way, some of you may remember the ApplicationCache API. It was also designed to cache Web resources, but it has been replaced by Service Worker and cache apis due to its lack of flexibility and other shortcomings.

The Service Worker has one very important feature: you can listen for all client (Web) requests in the Service Worker, and then proxy them through the Service Worker to initiate requests to the back-end Service. By listening for user request information, the Service Worker can decide whether to use caching as a return for Web requests.

The following figure shows the difference in network requests between ordinary Web App and Web App with Service Worker:

It is important to note that although the browser, the SW(Service Worker) and the back-end services appear to be placed side by side, both the browser (your Web application) and the SW are actually running on your local machine, so the SW in this scenario is like a “client proxy”.

Now that we understand the basic concepts, we can look at how we can apply this technique to achieve an offline usable Web application.

3. How to use Service Worker to realize offline “second on” application

Remember our demo Web App for book search? For those of you who don’t know, read the first article in this series, but you can ignore the details and continue to understand how the technology works.

That’s right, I’m still working on it this time. After adding manifest in the last post, it already has its own desktop icon and a Native app-like shell; And today, I’m gonna make it even cooler.

If you want to follow along, you can download all the code you need here. Remember to switch to the MANIFEST branch, because this article is based on the final code from the previous article. After all, our ultimate goal is to upgrade this normal “book search” demo to PWA.

3.1. Register a Service Worker

Note that our application should always be progressively available, even in environments that do not support Service workers. To do this, register our Service Worker (sw.js) in index.js with the feature check:

// index.js
// Register the service worker. The service worker script file is sw.js
if ('serviceWorker' in navigator) {
    navigator.serviceWorker.register('./sw.js').then(function () {
        console.log('Service Worker registered successfully ');
    });
}
Copy the code

Here we register the sw.js file as a Service Worker. Be careful not to write the path of the file incorrectly.

It is worth mentioning that all kinds of Service Worker operations are designed to be asynchronous to avoid some long blocking operations. These apis are called in the form of promises. So you’ll see Promise used over and over again in the following sections of code. If you don’t know anything about promises, you can learn about the basic Promise concepts here: Promise (MDN) and JavaScript Promise: Introduction.

3.2. Service Worker life cycle

When we register a Service Worker, it goes through stages of its life cycle and triggers events. The entire lifecycle includes installing –> installed –> activating –> activated –> redundant. When the Service Worker is installed, the install event is triggered. After activated, an Activate event is triggered.

The following example listens for the install event:

// Listen for the install event
self.addEventListener('install'.function (e) {
    console.log('Service Worker status: install');
});
Copy the code

Self is a special global variable in a Service Worker, similar to our most common Window object. Self references the current Service Worker.

3.3. Cache static resources

In the previous section, we learned how to add event listeners to trigger the appropriate action on the Service Worker. Now, to make our Web App available offline, we need to cache the resources we need. We need a list of resources that will be cached when the Service Worker is activated.

// sw.js
var cacheName = 'bs-0-2-0';
var cacheFiles = [
    '/'.'./index.html'.'./index.js'.'./style.css'.'./img/book.png'.'./img/loading.svg'
];

// Listen for install events and cache files after installation
self.addEventListener('install'.function (e) {
    console.log('Service Worker status: install');
    var cacheOpenPromise = caches.open(cacheName).then(function (cache) {
        return cache.addAll(cacheFiles);
    });
    e.waitUntil(cacheOpenPromise);
});
Copy the code

As you can see, we first list all static resource dependencies in cacheFiles. Note the ‘/’, and since the root path also accesses our application, don’t forget to cache that as well. When a Service Worker installs, we cache resources with the caches.open() and cache.addall () methods. So we’ve given the cache a cacheName, which is going to be the key of these caches.

In this code, caches are a global variable that allows us to manipulate cache-specific interfaces.

The Cache interface provides a storage mechanism for cached Request/Response pairs. Like Workers, the Cache interface is exposed to the window scope. Although it is defined in the service worker standard, it does not have to be used in conjunction with service workers. – the MDN

3.4 Using cached Static Resources

So far, we’ve just registered a Service Worker and cached some static resources when it installs. However, if you run the demo at this point, you will find that the Book Search Web App still cannot be used offline.

Why is that? Because we only cache these resources, the browser doesn’t know how to use them; In other words, the browser still waits and consumes these resources by sending a request to the server. So what?

You’ll be smart enough to recall that we talked about “client proxy” in the first half of this article when we introduced Service workers — using Service workers to help us decide how to use caching.

Here’s a simple strategy:

  1. The browser initiates a request to request various static resources (HTML/JS/CSS/IMG);
  2. The Service Worker intercepts browser requests and queries the current cache;
  3. If there is a cache, the end is displayed.
  4. If no cache exists, the device passesfetchMethod makes a request to the server and returns the result to the browser
// sw.js
self.addEventListener('fetch'.function (e) {
    // If there is a cache, return directly, otherwise fetch
    e.respondWith(
        caches.match(e.request).then(function (cache) {
            return cache || fetch(e.request);
        }).catch(function (err) {
            console.log(err);
            returnfetch(e.request); })); });Copy the code

The FETCH event listens for requests from all browsers. The e.espondwith () method takes a Promise as an argument and lets the Service Worker return data to the browser. Caches. Match (e.equest) checks to see if the current request has a local cache: if so, it returns the cache directly to the browser; Otherwise, the Service Worker initiates a FETCH (E. test) request for the back-end Service and returns the result to the browser.

Running our demo so far: when the book Search Web App is opened on the first network, the dependent static resources are cached locally; When accessed later, these caches are used instead of making network requests. As a result, we seem to be able to “access” the app even when we are off the Internet.

3.5. Update static cache resources

However, if you are careful, you will notice a small problem: when we cache the resource, the new static resource will not be cached unless we unregister sw.js and manually clear the cache.

An easy way to solve this problem is to modify cacheName. Because the browser determines whether sw.js is updated by byte, changing cacheName retriggers install and caches resources. In addition, in the Activate event, we need to check if the cacheName has changed. If it has changed, we need to delete the existing cache.

// sw.js
// Listen for the activate event and check whether static resources in the cache are updated by the cache key
self.addEventListener('activate'.function (e) {
    console.log('Service Worker status: activate');
    var cachePromise = caches.keys().then(function (keys) {
        return Promise.all(keys.map(function (key) {
            if(key ! == cacheName) {returncaches.delete(key); }})); }) e.waitUntil(cachePromise);return self.clients.claim();
});
Copy the code

3.6. “Offline search” of cache API Data

At this point, our application is almost completely offline. However, if you notice the image at the beginning of this article, we can not only access it offline, but also use the search function.

What’s going on here? The secret is that the Web App also caches a copy of the XHR request. The next time a request is made, the local cache (if any) is preferred; It then requests data from the server. The server returns the data and displays it based on the data. The general process is as follows:

First we modify the code in the previous section to cache API data in the FETCH event of sw.js

// sw.js
var apiCacheName = 'api-0-1-1';
self.addEventListener('fetch'.function (e) {
    // XHR requests to cache
    var cacheRequestUrls = [
        '/book? '
    ];
    console.log('Now requesting:' + e.request.url);

    // Determine whether the current request needs caching
    var needCache = cacheRequestUrls.some(function (url) {
        return e.request.url.indexOf(url) > - 1;
    });

    /**** Here are operations related to XHR data caching ****/
    if (needCache) {
        // Cache is required
        // Use fetch to request data, and clone a copy of the result to cache
        // After this part is cached, use the global caches variable in browser
        caches.open(apiCacheName).then(function (cache) {
            return fetch(e.request).then(function (response) {
                cache.put(e.request.url, response.clone());
                return response;
            });
        });
    }
    / * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /

    else {
        // Query cache directly for non-API requests
        // If there is a cache, return directly, otherwise fetch
        e.respondWith(
            caches.match(e.request).then(function (cache) {
                return cache || fetch(e.request);
            }).catch(function (err) {
                console.log(err);
                returnfetch(e.request); })); }});Copy the code

Here, we also create a special cache location for the data cached by the API, with the key variable apiCacheName. In the FETCH event, we first determine if the current request is XHR request data that needs to be cached by comparing it with cacheRequestUrls, and if so, we use the FETCH method to initiate the request back end.

In fetch. Then we update the cache of the data returned by the current request with the requested URL as the key: cache.put(e.equest. URL, response.clone()). Here we use the.clone() method to make a copy of the response data so that we can do all sorts of things with the response cache without worrying about the original response information being modified.

3.7. Apply offline XHR data to complete “Offline search” and improve response speed

If you follow this step, congratulations, you are one step away from our cool offline app!

So far, we are done modifying the Service Worker (sw.js). All that remains is how to use caching strategically on XHR requests, and this part of the transformation is all about index.js, which is our front-end script.

So let’s go back to this picture from the last video:

Instead, our front-end browser will first try to fetch cached data and use it to render the interface; At the same time, the browser also makes an XHR request, and the Service Worker returns the data to the front-end Web application by updating the returned data to the storage (this step is the caching strategy mentioned in the previous section). Finally, if it is determined that the returned data is inconsistent with the original cache, the interface is re-rendered. Otherwise, the interface is ignored.

To make the code clearer, we stripped out the original XHR request as a method called getApiDataRemote() and transformed it into a Promise. To save space, SOME of my code is relatively simple and will not be posted separately.

The most important part of this section is actually the read cache. We know that in a Service Worker it is possible to call in the Caches variable to ask for cache objects. Happily, caches can still be accessed in our front-end applications, too. Of course, to ensure progressive usability, we need to judge ‘caches’ in window first. For code uniformity, I’ve also wrapped the cache data that gets the request into a Promise method:

function getApiDataFromCache(url) {
    if ('caches' in window) {
        return caches.match(url).then(function (cache) {
            if(! cache) {return;
            }
            return cache.json();
        });
    }
    else {
        return Promise.resolve(); }}Copy the code

Whereas in our queryBook() method, we would have requested the back-end data and rendered the page; Now, we add cache-based rendering:

function queryBook() {
    / /...
    // Remote request
    var remotePromise = getApiDataRemote(url);
    var cacheData;
    // Use cached data rendering first
    getApiDataFromCache(url).then(function (data) {
        if (data) {
            loading(false);
            input.blur();            
            fillList(data.books);
            document.querySelector('#js-thanks').style = 'display: block';
        }
        cacheData = data || {};
        return remotePromise;
    }).then(function (data) {
        if (JSON.stringify(data) ! = =JSON.stringify(cacheData)) {
            loading(false);                
            input.blur();
            fillList(data.books);
            document.querySelector('#js-thanks').style = 'display: block'; }});/ /...
}
Copy the code

If getApiDataFromCache(URL).then returns cached data, it is used to render first. When remotePromise’s data is returned, it is compared to cacheData and the page needs to be rerendered only if the data is inconsistent (note here that the json.stringify () method is used roughly to compare objects for simplicity). This has two advantages:

  1. Offline availability. If we have visited some URL before, the page can still be displayed by repeating the same operation while offline;
  2. Optimize the experience and increase the speed of access. Local cache reads take much less time than network requests, giving our users a “second on”, “second response” feeling.

4. Testing our application with Lighthouse

At this point, we have completed the two basic functions of PWA: the Offline cache of the Web App Manifest and the Service Worker. These two features can greatly improve user experience and application performance. Let’s use Lighthouse in Chrome to examine the current app:

As you can see, on the PWA score, our Web App is already pretty good. The only penalty is for HTTPS: http://127.0.0.1:8085 is used for local debugging, which will definitely be replaced with HTTPS in production.

5. That’s cool, but what about compatibility?

With Apple’s support for Service workers in iOS 11.3 at the beginning of this year (2018), and Apple’s consistently good system upgrade rate, the overall PWA has made a major breakthrough in compatibility issues.

Although Some other functions of Service workers (such as push and background synchronization) are not endorsed by Apple, the offline cache of The Web App Manifest and Service Worker is supported by iOS 11.3. These two core features not only work well, but also seem to have good compatibility so far that they are very suitable for production.

What’s more, one of the most important features of progressive web apps is the automatic upgrade of functionality and experience when compatibility is supported; When it is not supported, it will silently roll back some new functions. Under the condition of ensuring our normal service, we try our best to make use of browser features to provide better service.

6. Write at the end

All of the code examples in this article can be found at learn-pwa/sw-cache. Note that after Git clone, switch to the SW-cache branch, where all the code in this article exists. Toggle other scores to see different versions:

  • Basic branch: Basic project Demo, a general book search application (website);
  • Manifest branch: Based on the basic branch, add manifest and other functions, specific can see the article to understand;
  • Sw-cache branch: based on the MANIFEST branch, add caching and offline functions;
  • Master branch: The latest code for the application.

If you like or want to learn more about PWA, please feel free to follow me and follow the PWA Learning & Practice series. I will summarize the questions and technical points I have encountered in the process of learning PWA, and practice with everyone through actual codes.

Finally, as a demo, the code in this article is mainly used to understand and learn the principle of PWA technology. There may be some imperfections, so it is not recommended to use directly in the production environment.

PWA Technology Learning and Practice series

  • Start your PWA learning journey
  • Learn to Use Manifest to Make your WebApp More “Native”
  • Make your WebApp Available Offline from today (article)
  • Article 4: TroubleShooting: TroubleShooting FireBase Login Authentication Failures
  • Keep in touch with Your Users: The Web Push feature
  • How to Debug? Debug your PWA in Chrome
  • Enhanced Interaction: Use the Notification API for reminders
  • Chapter 8: Background Data Synchronization using Service Worker
  • Chapter nine: Problems and solutions in PWA practice
  • Resource Hint – Improving page loading performance and Experience
  • Part 11: Learning offline Strategies from workbox

The resources

  • Using Service Workers (MDN)
  • The Cache (MDN)
  • Service Worker usage
  • JavaScript Promise: Introduction
  • Promise (MDN)