In my last post, “How I Got my Website to Use HTML5 Manifest”, I explained how to use the Manifest as an offline web application. The result was that the Manifest has been deprecated and removed from the Web standard and is now being replaced by the Service Worker. Some ideas from Manifest can still be borrowed. The author also upgraded the website to Service Worker. If you use Chrome or other browsers, you can use Service Worker to do offline caching. If you use Safari, you can still use Manifest. Off the network is also normal open.

1. What is Service Worker

Service Worker is a key role in the implementation of Progressive Web App (PWA) initiated by Google, which aims to solve the disadvantages of traditional Web App:

(1) No desktop entrance

(2) It cannot be used offline

(3) There is no Push

How does a Service Worker behave? As shown below:

Service Worker is a Service Worker thread started in the background. In the figure above, I opened two tabs, so two clients are displayed. However, no matter how many pages are opened, only one Worker is responsible for management. This Worker’s job is to cache some resources, and then intercept the request of the page. First check whether there is any in the cache. If there is, it will fetch from the cache and respond with 200. Specifically, the Service Worker combined with the Web App Manifest can accomplish the following tasks (which are also PWA’s test criteria) :

These include the ability to use the site offline, return 200 when disconnected, and prompt users to add an icon to the desktop.

2. Service Worker support

Service workers are currently only supported by Chrome/Firfox/Opera:

Safari and Edge are also preparing to support Service Worker. Since Service Worker is a standard dominated by Google, Safari is also preparing to support Service Worker under the pressure of the situation. In Safari TP version, you can see:

There is already a menu item for Service Worker in Experimental Features, but it cannot be used even if opened, prompting you that it has not been implemented:

Either way, at least Safari is ready to support Service workers. We can also see that Safari 11.0.1, released in September 2017, already supports WebRTC, so Safari is still a progressive kid.

Edge is also ready to support it, so the future of Service workers is bright.

3. Use Service Worker

The usage routine of Service Worker is to register a Worker first, and then a thread will start in the background. When this thread starts, it can load some resources and cache them, and then listen to the fetch event. In this event, the request of the page will be intercepted, first check whether it is in the cache. Or you don’t cache the resource at first, you make a copy of it after each resource request, and it’s in the cache for the next request.

(1) Register a Service Worker

The Service Worker object is in window.navigator, with the following code:

window.addEventListener("load", function() { console.log("Will the service worker register?" ); navigator.serviceWorker.register('/sw-3.js') .then(function(reg){ console.log("Yes, it did."); }).catch(function(err) { console.log("No it didn't. This happened: ", err) }); });Copy the code

This JS file is the operating environment of the Service Worker. If the registration fails, an exception will be thrown. For example, Safari TP has this object, but it will throw an exception and cannot be used, so it can be handled in catch. So the question here is why do YOU need to start on the load event? Because you want to start a thread, extra start after you might have to let it go to load resources, these are all need CPU and bandwidth, we should ensure the normal order of the page to finish loading, and then start our background threads, cannot compete with normal page load to produce, this meant in the low-end mobile devices is larger.

If you set A Cookie named time Path =/page/A, the page/ page/B will not be able to obtain the Cookie. If the path of the cookie is set to root /, all pages will be retrieved. Similarly, if the js path used when registering is /page/sw.js, the Service Worker can only manage pages and resources in /page, but not in/API. Therefore, the Service Worker is usually registered in the top-level directory. Such as “/sw-3.js” in the code above, so that the Service Worker can take over all resources on the page.

(2) Service Worker installation and activation

After the registration, the Service Worker installs, which triggers the install event. In the install event, you can cache some resources like sw-3.js:

const CACHE_NAME = "fed-cache"; this.addEventListener("install", function(event) { this.skipWaiting(); console.log("install service worker"); // Create and open a cache aches. Open (CACHE_NAME); // let cacheResources = ["https://fed.renren.com/?launcher=true"]; Event.waituntil (// Requests the resource and adds it to the cache. CACHE_NAME). Then (cache => {cache.addall (cacheResources); })); });Copy the code

We created and added a fed-cache as shown in the Chrome console:

Service Worker apis basically return Promise objects to avoid congestion, so use Promise writing. The first page requests are cached when the Service Worker is installed. In the operating environment of a Service Worker, it has a caches global object, which is the entry point to the cache, and a common clients global object, where a client corresponds to a TAB.

Fetch and other APIS can be used in Service workers, which are isolated from DOM. There is no Windows/Document object, so DOM cannot be directly manipulated and pages cannot be directly interoperated. A Service Worker does not know if the page is currently open or what the url of the page is, because a Service Worker manages several tabs that are currently open and can know the URLS of all pages through clients. There is also the possibility to do some control by sending messages and data to and from the main page via postMessage.

When install is complete, the Service Worker’s active event is triggered:

this.addEventListener("active", function(event) {
    console.log("service worker is active");
});Copy the code

Once the Service Worker is activated, it can listen for fetch events, and we want to cache each resource we fetch, so that we don’t have to make a list like the Manifest mentioned in the previous article.

You may ask, did I re-register a Service Worker to install and activate when I refreshed the page? It does not register again. It finds that “sw-3. Js” is already registered, it does not register again, and thus does not trigger install and active events because the Service Worker is already active. When the Service Worker needs to be updated, such as “sw-4.js” or changing the text content of SW-3. js, it will be re-registered. The new Service Worker will first install and then enter the waiting state. The old Service Worker will be replaced and the new Service Worker will enter the active state.

this.skipWaiting();Copy the code

(3) Fetch resources from cache

Listen for the fetch event to do something like this:

this.addEventListener("fetch", function(event) { event.respondWith( caches.match(event.request).then(response => { // cache hit if (response) { return response; } return util.fetchPut(event.request.clone()); })); });Copy the code

Check caches. Match to see if there is a response in the cache, otherwise request the resource and put it in the cache. The key value of the resource in the cache is the Request object. The second parameter, ignoreVary, can be set to match only if the requested URL and header are the same:

caches.match(event.request, {ignoreVary: true})Copy the code

Represents the same resource as long as the request URL is the same.

FetchPut (util. FetchPut)

Let util = {fetchPut: function (request, callback) {return fetch(request). Then (response => { response || response.status ! == 200 || response.type ! == "basic") { return response; } util.putCache(request, response.clone()); typeof callback === "function" && callback(); return response; }); }, putCache: Function (request, resource) {// If (request.method === "GET" && request.url.indexof ("wp-admin") < 0 && request.url.indexof ("preview_id") < 0) { caches.open(CACHE_NAME).then(cache => { cache.put(request, resource); }); }}};Copy the code

It should be noted that cross-domain resources cannot be cached. Response. status will return 0. If cross-domain resources support CORS, you can change the mod of request to CORS. If the request fails, such as 404 or timeout, then the response is directly returned to the main page for processing, otherwise, the load is successful, clone the response into the cache, and then return the response to the main page thread. Note that the only resources that can be saved are usually GET, and can not be cached via POST, so make a judgment (of course, you can manually change the method of the Request object to GET), and also make a judgment on some resources that you do not want cached.

In this way, once the user opens the page once, the Service Worker will be installed. When he refreshes the page or opens the second page, he can cache the requested resources one by one, including pictures, CSS, JS, etc. As long as the cache has them, the user can access them normally no matter whether they are online or offline. So the natural question is, how big is this cache? In the Manifest, you can see how much local storage is available in Chrome 61, and how much local storage is available in Chrome 61.

Cache Storage refers to the sum of the space occupied by the Service Worker and the Manifest. As shown in the figure above, the total space is 20GB, almost unlimited, so there is no need to worry about running out of Cache.

Cache HTML

Step (3) above caches images, JS and CSS, but if the HTML of the page is also cached, such as the home page, there will be an embarrassing problem — the Service Worker is registered on the page, but now the page is cached, so it is the same every time. As a result, the Service Worker cannot be updated, such as sw-5.js, but PWA requires us to cache the page HTML. So what to do? Google’s developer documentation just mentions this problem, but doesn’t say how to fix it. The solution to this problem requires that we have a mechanism to know when the HTML is updated and replace the HTML in the cache.

The mechanism for updating the Manifest cache is to check whether the Manifest text content has changed. If so, the cache will be updated. The Service Worker also depends on whether the sw.js text content has changed. If the request is HTML and it is retrieved from the cache, send a request to get a file to see if the HTML update time has changed, and if it has changed, it has changed, and then the cache is deleted. So you can update the client cache by controlling this file on the server. The following code:

this.addEventListener("fetch", function(event) { event.respondWith( caches.match(event.request).then(response => { // cache hit if (response) { // If HTML is selected, If (response.headers. Get (" content-type ").indexof ("text/ HTML ") >= 0) {console.log("update HTML "); let url = new URL(event.request.url); util.updateHtmlPage(url, event.request.clone(), event.clientId); } return response; } return util.fetchPut(event.request.clone()); })); });Copy the code

If the content-type of the header is text/ HTML, then send a request to retrieve a file and determine whether the cache needs to be removed based on the contents of the file.

let pageUpdateTime = { }; let util = { updateHtmlPage: function (url, htmlRequest) { let pageName = util.getPageName(url); let jsonRequest = new Request("/html/service-worker/cache-json/" + pageName + ".sw.json"); fetch(jsonRequest).then(response => { response.json().then(content => { if (pageUpdateTime[pageName] ! == content.updateTime) { console.log("update page html"); // Re-fetch HTML util.fetchPut(htmlRequest) if there is an update; pageUpdateTime[pageName] = content.updateTime; }}); }); }, delCache: function (url) { caches.open(CACHE_NAME).then(cache => { console.log("delete cache " + url); cache.delete(url, {ignoreVary: true}); }); }};Copy the code

The code first retrieves a JSON file. Each page will correspond to a JSON file. The content of the JSON file looks like this:

{"updateTime":"10/2/2017, 3:23:57 PM","resources": {img: [], css: []}}Copy the code

There is an updateTime field. If there is no updateTime data for the page in local memory or it is not the same as the latest updateTime, fetch the HTML again and store it in the cache. Then you need to notify the page thread that the data has changed and refresh the page. So you don’t have to wait for the user to refresh the page. So notify the page with postMessage when the page is refreshed:

let util = { postMessage: async function (msg) { const allClients = await clients.matchAll(); allClients.forEach(client => client.postMessage(msg)); }}; util.fetchPut(htmlRequest, false, function() { util.postMessage({type: 1, desc: "html found updated", url: url.href}); });Copy the code

Specify type: 1 to indicate that this is an HTML update message, and then listen for the message event on the page:

if("serviceWorker" in navigator) { navigator.serviceWorker.addEventListener("message", function(event) { let msg = event.data; if (msg.type === 1 && window.location.href === msg.url) { console.log("recv from service worker", event.data); window.location.reload(); }}); }Copy the code

We then update the JSON file when we need to update the HTML so that the user can see the latest page. Or when the user restarts the browser, the running memory of the Service Worker will be cleared, that is, the variable storing the page update time will be cleared, and the page will be requested again.

Note that the HTTP cache time of this JSON file is set to 0, so that the browser does not cache it, as shown in nginx:

location ~* .sw.json$ {
    expires 0;
}Copy the code

Because this file needs to be retrieved in real time, it cannot be cached, firefox caches by default, Chrome does not, plus HTTP cache time is 0, Firefox does not cache.

Another type of update is made by the user. For example, if the user posts a comment, the service worker needs to be notified on the page to delete the HTML cache and retrieve it again. This is a reverse message notification:

if ("serviceWorker" in navigator) { document.querySelector(".comment-form").addEventListener("submit", function() { navigator.serviceWorker.controller.postMessage({ type: 1, desc: "remove html cache", url: window.location.href} ); }}); }Copy the code

The Service Worker also listens for message events:

Const messageProcess = {// delete HTML index 1: function (url) {util.delcache (url); }}; let util = { delCache: function (url) { caches.open(CACHE_NAME).then(cache => { console.log("delete cache " + url); cache.delete(url, {ignoreVary: true}); }); }}; this.addEventListener("message", function(event) { let msg = event.data; console.log(msg); if (typeof messageProcess[msg.type] === "function") { messageProcess[msg.type](msg.url); }});Copy the code

Call a different callback function depending on the message type, if it is 1, delete the cache. After the user posts a comment, the page will be refreshed, and the request will be renewed after the cache has been deleted.

This solves the problem of real-time updates.

4. Http/Manifest/Service Worker Relationships among three caches

There are three ways to Cache: Http Cache for Cache duration, Manifest Application Cache, and Service Worker Cache. What if all three were used?

Because the Service Worker intercepts the request, it will process it first. If it has a request in its cache, it will return it directly. If it does not, the normal request will be equivalent to no Service Worker. If it’s in the Manifest cache then it gets that cache, if it’s not there then it doesn’t have the Manifest, so it gets it from the Http cache, and if it’s not in the Http cache then it sends a request to get it, The server may return 304 Not Modified based on the Http ETAG or Modified Time, otherwise it normally returns 200 and the data content. That’s the whole acquisition process.

So using the Manifest and the Service Worker should result in the same resource being stored twice. However, you can have browsers that support Service workers use Service workers and those that do not use the Manifest.

5. Add a desktop entry using the Web App Manifest

Notice that this is another Manifest. This Manifest is a JSON file that puts the site icon name and other information to add an icon to the desktop and create the effect of opening the page as if it were an App. The other Manifest mentioned above is the Manifest of the abolished Application Cache.

The maifest. json file could be written like this:

{" short_name ":" all the FED ", "name" : "renren FED, focusing on the front end technology", "ICONS" : [{" SRC ":"/HTML/app - manifest/logo_48. PNG ", "type" : "image/png", "sizes": "48x48" }, { "src": "/html/app-manifest/logo_96.png", "type": "image/png", "sizes": "96x96" }, { "src": "/html/app-manifest/logo_192.png", "type": "image/png", "sizes": "192x192" }, { "src": "/html/app-manifest/logo_512.png", "type": "image/png", "sizes": "512x512" } ], "start_url": "/? launcher=true", "display": "standalone", "background_color": "#287fc5", "theme_color": "#fff" }Copy the code

Icon needs to be prepared in a variety of sizes, up to 512px by 512px, so Chrome will automatically select the appropriate image. If you change display to standalone, opening from the generated icon will act like an App, without the browser address bar. Start_url specifies the entry link to open.

Then add a meta tag to point to the manifest file:

<link rel="manifest" href="/html/app-manifest/manifest.json">Copy the code

Combined with the Service Worker cache:

Caches the page pointed to by start_URL with the Service Worker, so that when the user opens the page with Chrome, Chrome will pop a prompt at the bottom asking the user to add the page to the desktop. Clicking “Add” will generate a desktop icon. Entering from this diagram is like opening an App. The feelings are as follows:

Awkwardly, Manifest is currently only supported by Chrome and is only available on Android. IOS browsers can’t add a desktop icon because IOS doesn’t have an API, but Safari does.

 

To sum up, this paper introduces how to use Service Worker to make a PWA offline Web APP combined with Manifest. It mainly uses Service Worker to control cache. Since it is to write JS, it is flexible and can also communicate with the page. In addition, the request page update time is used to determine whether the HTML cache needs to be updated. Service Worker compatibility is not particularly good, but the future is promising and browsers are getting ready to support it. At present, offline applications can be combined with offline Cache Manifest.

 

Related reading:

  1. Why upgrade to HTTPS
  2. How to upgrade to HTTP /2
  3. How do I get my website to use HTML5 Manifest

 

Directory: Basic technology, page optimization

comment