4
头图

Preface

This article will first briefly introduce the common caching methods of the front-end, then introduce the concept of Service Worker, and introduce its principle and how to use it. Then, based on the third-party library Workbox launched by google, the application practice in the product, and a brief analysis of its principle.

Author: Liu Fang

Editor: Ein

Introduction to front-end caching

First, briefly introduce the existing front-end caching technology solutions, which are mainly divided into http caching and browser caching.

http cache

http caching is started on the second request, which is also a common topic. It's nothing more than a few http headers:

Expires

For HTTP1.0 content, the server uses the Expires header to tell the Web client that it can use the current copy until the specified time.

Cache-Control

HTTP1.1 introduces Cathe-Control, which uses max-age to specify how long resources are cached. It mainly solves a major defect of Expires, that is, it sets a fixed point in time, client time and server time may be error.
Therefore, both headers are usually brought on. This kind of cache is called a strong cache, and its manifestation is:

Last-Modified / If-Modified-Since

Last-Modified is the last modification time of the resource that the server tells the browser. If-Modified-Since is on the request header, the last modification time of the resource that the server gave itself last time. Then the server compares it.

If the last modification time of the resource is greater than If-Modified-Since, indicating that the resource has been modified again, it will respond to the entire resource content and return a status code of 200;

If the last modification time of the resource is less than or equal to If-Modified-Since, indicating that there is no new modification to the resource, it responds with HTTP 304 to inform the browser to continue using the current version.

Etag / If-None-Match

As mentioned earlier, the modification time of the file is used to determine whether the file is changed, or it will bring certain errors, such as insignificant modifications such as comments. So a new way was introduced.

Etag is the unique identifier of the file generated by the specific algorithm of the server, and the request header brings the returned Etag value to the server through If-None-Match, and the server determines whether to respond to the new content through comparison. This is also the 304 cache.

Browser cache

Storage

Simple caching methods include cookie, localStorage and sessionStorage. I won't introduce their differences in detail here. Here, I will talk about the optimization scheme of caching static resources through localStorage.
LocalStorage usually has 5MB of storage space. Let's take the WeChat article page as an example.
Looking at the request, it is found that there are basically no js and css requests, because it puts all the resources that do not need to be changed in localStorage:

So the WeChat article page loads very quickly.

Front-end database

The front-end databases include WebSql and IndexDB. Among them, WebSql is obsolete by the specification. They have a maximum capacity of about 50MB, which can be understood as an enhanced version of localStorage.

App cache

Application caching is mainly to register cached static resources through the manifest file, which has been abandoned because of some unreasonable aspects of his design. While caching static files, it also caches html files by default. As a result, page updates can only be determined by the version number in the manifest file. Therefore, application caching is only suitable for static websites that do not change all the year round. Such inconvenience is also an important reason for being abandoned.

PWA also uses this file. Unlike the manifest that simply classifies files by whether they are cached or not, PWA uses the manifest to build its own APP skeleton and uses Servie Worker to control the cache. This is also the protagonist of today.

Service Worker

Service Workers are essentially used by browsers to cache resources, but it is not only Cache, but also further optimization by means of workers.
It is based on h5 web worker, so it will never hinder the execution of the current js thread. The most important working principle of sw is:

1. Background thread: independent of the current web page thread;

2. Web proxy: proxy to cache files when the web page initiates a request.

compatibility


As you can see, basically the new version of the browser is still compatible. Previously, it was only supported by Chrome and Firefox, but now Microsoft and Apple have also supported it.

Maturity

To judge whether a technology is worth trying, you must consider its maturity, otherwise it will be embarrassing to be abandoned by the specification just like application caching after a while.
So here I have listed a few pages that use Service Workers:

  • Taobao
  • Netease News
  • Koala

So you can still try it.

Debugging method

Whether a website is enabled with Service Worker can be checked through Application in the developer tools:

For files cached by Service Worker, you can see the Size item in Network as from Service Worker:

You can also view the specific content of the cache in the Cache Storage of the Application:

If it is a specific breakpoint debugging, the corresponding thread needs to be used instead of the main thread. This is also the general debugging method of webworker:

Conditions of Use

sw is based on HTTPS. Because the service worker involves request interception, the HTTPS protocol must be used to ensure security. If it is local debugging, localhost is fine.
And we happen to be forced to https on the whole site, so it happens to be usable.

Life cycle

It can probably be explained with the following picture:

registered

To use Service Worker, you first need to register a sw, notify the browser to allocate a block of memory for the page, and then sw will enter the installation phase.
A simple way to register:

(function() {
    if('serviceWorker' in navigator) {
        navigator.serviceWorker.register('./sw.js');
    }
})()

Of course, you can also consider a comprehensive point, refer to the registration method of NetEase News:

"serviceWorker" in navigator && window.addEventListener("load",
    function() {
        var e = location.pathname.match(/\/news\/[a-z]{1,}\//)[0] + "article-sw.js?v=08494f887a520e6455fa";
        navigator.serviceWorker.register(e).then(function(n) {
            n.onupdatefound = function() {
                var e = n.installing;
                e.onstatechange = function() {
                    switch (e.state) {
                        case "installed":
                            navigator.serviceWorker.controller ? console.log("New or updated content is available.") : console.log("Content is now available offline!");
                            break;
                        case "redundant":
                            console.error("The installing service worker became redundant.")
                    }
                }
            }
        }).
        catch(function(e) {
            console.error("Error during service worker registration:", e)
        })
    })

As mentioned earlier, because sw will monitor and proxy all requests, the scope of sw becomes extra important. For example, we only want to monitor all requests of our theme page, and specify the path when registering:

navigator.serviceWorker.register('/topics/sw.js');

In this way, only the path under topics/ will be optimized.

installing

After we register, the browser will start to install sw, which can be monitored through events:

//service worker安装成功后开始缓存所需的资源
var CACHE_PREFIX = 'cms-sw-cache';
var CACHE_VERSION = '0.0.20';
var CACHE_NAME = CACHE_PREFIX+'-'+CACHE_VERSION;
var allAssets = [
    './main.css'
];
self.addEventListener('install', function(event) {

    //调试时跳过等待过程
    self.skipWaiting();


    // Perform install steps
    //首先 event.waitUntil 你可以理解为 new Promise,
    //它接受的实际参数只能是一个 promise,因为,caches 和 cache.addAll 返回的都是 Promise,
    //这里就是一个串行的异步加载,当所有加载都成功时,那么 SW 就可以下一步。
    //另外,event.waitUntil 还有另外一个重要好处,它可以用来延长一个事件作用的时间,
    //这里特别针对于我们 SW 来说,比如我们使用 caches.open 是用来打开指定的缓存,但开启的时候,
    //并不是一下就能调用成功,也有可能有一定延迟,由于系统会随时睡眠 SW,所以,为了防止执行中断,
    //就需要使用 event.waitUntil 进行捕获。另外,event.waitUntil 会监听所有的异步 promise
    //如果其中一个 promise 是 reject 状态,那么该次 event 是失败的。这就导致,我们的 SW 开启失败。
    event.waitUntil(
        caches.open(CACHE_NAME)
            .then(function(cache) {
                console.log('[SW]: Opened cache');
                return cache.addAll(allAssets);
            })
    );

});

During installation, sw will begin to cache files, and will check the cache status of all files. If all files have been cached, the installation is successful and enter the next stage.

activated

If it is the first time to load sw, after installation, it will directly enter the activated stage, but if sw is updated, the situation will be more complicated. The process is as follows:

First, the old sw is A, and the new sw version is B.
B enters the install phase, and A is still working, so B enters the waiting phase. Only after A is terminated, can B replace A's work normally.

The timing of this terminated has the following ways:

1. Close the browser for a period of time;

2. Manually clear the Service Worker;

3. Skip the waiting phase directly during sw installation

//service worker安装成功后开始缓存所需的资源
self.addEventListener('install', function(event) {
    //跳过等待过程
    self.skipWaiting();
});

Then it enters the activated phase to activate sw work.

The activated stage can do many meaningful things, such as updating the key and value stored in the Cache:

var CACHE_PREFIX = 'cms-sw-cache';
var CACHE_VERSION = '0.0.20';
/**
 * 找出对应的其他key并进行删除操作
 * @returns {*}
 */
function deleteOldCaches() {
    return caches.keys().then(function (keys) {
        var all = keys.map(function (key) {
            if (key.indexOf(CACHE_PREFIX) !== -1 && key.indexOf(CACHE_VERSION) === -1){
                console.log('[SW]: Delete cache:' + key);
                return caches.delete(key);
            }
        });
        return Promise.all(all);
    });
}
//sw激活阶段,说明上一sw已失效
self.addEventListener('activate', function(event) {


    event.waitUntil(
        // 遍历 caches 里所有缓存的 keys 值
        caches.keys().then(deleteOldCaches)
    );
});

idle

This idle state is generally invisible. This generally means that the sw's things have been processed, and then it is in an idle state.

The browser will poll periodically to release the resources occupied by the idle sw.

fetch

This stage is the most critical stage of sw, which is used to intercept all specified requests of the agent and perform corresponding operations.

All the caching parts are at this stage. Here is a simple example:

//监听浏览器的所有fetch请求,对已经缓存的资源使用本地缓存回复
self.addEventListener('fetch', function(event) {
    event.respondWith(
        caches.match(event.request)
            .then(function(response) {
                //该fetch请求已经缓存
                if (response) {
                    return response;
                }
                return fetch(event.request);
                }
            )
    );
});

The life cycle is probably clear, let's take a specific example to illustrate how the native serviceworker is used in the production environment.

Give a chestnut

We can take the wap page of Netease News as an example, which enables sw caching for static resources that do not change much. The specific sw.js logic and interpretation are as follows:

'use strict';
//需要缓存的资源列表
var precacheConfig = [
    ["https://static.ws.126.net/163/wap/f2e/milk_index/bg_img_sm_minfy.png",
        "c4f55f5a9784ed2093009dadf1e954f9"],
    ["https://static.ws.126.net/163/wap/f2e/milk_index/change.png",
        "9af1b102ef784b8ff08567ba25f31d95"],
    ["https://static.ws.126.net/163/wap/f2e/milk_index/icon-download.png",
        "1c02c724381d77a1a19ca18925e9b30c"],
    ["https://static.ws.126.net/163/wap/f2e/milk_index/icon-login-dark.png",
        "b59ba5abe97ff29855dfa4bd3a7a9f35"],
    ["https://static.ws.126.net/163/wap/f2e/milk_index/icon-refresh.png",
        "a5b1084e41939885969a13f8dbc88abd"],
    ["https://static.ws.126.net/163/wap/f2e/milk_index/icon-video-play.png",
        "065ff496d7d36345196d254aff027240"],
    ["https://static.ws.126.net/163/wap/f2e/milk_index/icon.ico",
        "a14e5365cc2b27ec57e1ab7866c6a228"],
    ["https://static.ws.126.net/163/wap/f2e/milk_index/iconfont_1.eot",
        "e4d2788fef09eb0630d66cc7e6b1ab79"],
    ["https://static.ws.126.net/163/wap/f2e/milk_index/iconfont_1.svg",
        "d9e57c341608fddd7c140570167bdabb"],
    ["https://static.ws.126.net/163/wap/f2e/milk_index/iconfont_1.ttf",
        "f422407038a3180bb3ce941a4a52bfa2"],
    ["https://static.ws.126.net/163/wap/f2e/milk_index/iconfont_1.woff",
        "ead2bef59378b00425779c4ca558d9bd"],
    ["https://static.ws.126.net/163/wap/f2e/milk_index/index.5cdf03e8.js",
        "6262ac947d12a7b0baf32be79e273083"],
    ["https://static.ws.126.net/163/wap/f2e/milk_index/index.bc729f8a.css",
        "58e54a2c735f72a24715af7dab757739"],
    ["https://static.ws.126.net/163/wap/f2e/milk_index/logo-app-bohe.png",
        "ac5116d8f5fcb3e7c49e962c54ff9766"],
    ["https://static.ws.126.net/163/wap/f2e/milk_index/logo-app-mail.png",
        "a12bbfaeee7fbf025d5ee85634fca1eb"],
    ["https://static.ws.126.net/163/wap/f2e/milk_index/logo-app-manhua.png",
        "b8905b119cf19a43caa2d8a0120bdd06"],
    ["https://static.ws.126.net/163/wap/f2e/milk_index/logo-app-open.png",
        "b7cc76ba7874b2132f407049d3e4e6e6"],
    ["https://static.ws.126.net/163/wap/f2e/milk_index/logo-app-read.png",
        "e6e9c8bc72f857960822df13141cbbfd"],
    ["https://static.ws.126.net/163/wap/f2e/milk_index/logo-site.png",
        "2b0d728b46518870a7e2fe424e9c0085"],
    ["https://static.ws.126.net/163/wap/f2e/milk_index/version_no_pic.png",
        "aef80885188e9d763282735e53b25c0e"],
    ["https://static.ws.126.net/163/wap/f2e/milk_index/version_pc.png",
        "42f3cc914eab7be4258fac3a4889d41d"],
    ["https://static.ws.126.net/163/wap/f2e/milk_index/version_standard.png",
        "573408fa002e58c347041e9f41a5cd0d"]
];
var cacheName = 'sw-precache-v3-new-wap-index-' + (self.registration ? self.registration.scope : '');

var ignoreUrlParametersMatching = [/^utm_/];

var addDirectoryIndex = function(originalUrl, index) {
    var url = new URL(originalUrl);
    if (url.pathname.slice(-1) === '/') {
        url.pathname += index;
    }
    return url.toString();
};
var cleanResponse = function(originalResponse) {
    // If this is not a redirected response, then we don't have to do anything.
    if (!originalResponse.redirected) {
        return Promise.resolve(originalResponse);
    }
    // Firefox 50 and below doesn't support the Response.body stream, so we may
    // need to read the entire body to memory as a Blob.
    var bodyPromise = 'body' in originalResponse ?
        Promise.resolve(originalResponse.body) :
        originalResponse.blob();
    return bodyPromise.then(function(body) {
        // new Response() is happy when passed either a stream or a Blob.
        return new Response(body, {
            headers: originalResponse.headers,
            status: originalResponse.status,
            statusText: originalResponse.statusText
        });
    });
};
var createCacheKey = function(originalUrl, paramName, paramValue,
                              dontCacheBustUrlsMatching) {
    // Create a new URL object to avoid modifying originalUrl.
    var url = new URL(originalUrl);
    // If dontCacheBustUrlsMatching is not set, or if we don't have a match,
    // then add in the extra cache-busting URL parameter.
    if (!dontCacheBustUrlsMatching ||
        !(url.pathname.match(dontCacheBustUrlsMatching))) {
        url.search += (url.search ? '&' : '') +
            encodeURIComponent(paramName) + '=' + encodeURIComponent(paramValue);
    }
    return url.toString();
};
var isPathWhitelisted = function(whitelist, absoluteUrlString) {
    // If the whitelist is empty, then consider all URLs to be whitelisted.
    if (whitelist.length === 0) {
        return true;
    }
    // Otherwise compare each path regex to the path of the URL passed in.
    var path = (new URL(absoluteUrlString)).pathname;
    return whitelist.some(function(whitelistedPathRegex) {
        return path.match(whitelistedPathRegex);
    });
};
var stripIgnoredUrlParameters = function(originalUrl,
                                         ignoreUrlParametersMatching) {
    var url = new URL(originalUrl);
    // Remove the hash; see https://github.com/GoogleChrome/sw-precache/issues/290
    url.hash = '';
    url.search = url.search.slice(1) // Exclude initial '?'
        .split('&') // Split into an array of 'key=value' strings
        .map(function(kv) {
            return kv.split('='); // Split each 'key=value' string into a [key, value] array
        })
        .filter(function(kv) {
            return ignoreUrlParametersMatching.every(function(ignoredRegex) {
                return !ignoredRegex.test(kv[0]); // Return true iff the key doesn't match any of the regexes.
            });
        })
        .map(function(kv) {
            return kv.join('='); // Join each [key, value] array into a 'key=value' string
        })
        .join('&'); // Join the array of 'key=value' strings into a string with '&' in between each
    return url.toString();
};

var hashParamName = '_sw-precache';
//定义需要缓存的url列表
var urlsToCacheKeys = new Map(
    precacheConfig.map(function(item) {
        var relativeUrl = item[0];
        var hash = item[1];
        var absoluteUrl = new URL(relativeUrl, self.location);
        var cacheKey = createCacheKey(absoluteUrl, hashParamName, hash, false);
        return [absoluteUrl.toString(), cacheKey];
    })
);
//把cache中的url提取出来,进行去重操作
function setOfCachedUrls(cache) {
    return cache.keys().then(function(requests) {
        //提取url
        return requests.map(function(request) {
            return request.url;
        });
    }).then(function(urls) {
        //去重
        return new Set(urls);
    });
}
//sw安装阶段
self.addEventListener('install', function(event) {
    event.waitUntil(
        //首先尝试取出存在客户端cache中的数据
        caches.open(cacheName).then(function(cache) {
            return setOfCachedUrls(cache).then(function(cachedUrls) {
                return Promise.all(
                    Array.from(urlsToCacheKeys.values()).map(function(cacheKey) {
                        //如果需要缓存的url不在当前cache中,则添加到cache
                        if (!cachedUrls.has(cacheKey)) {
                            //设置same-origin是为了兼容旧版本safari中其默认值不为same-origin,
                            //只有当URL与响应脚本同源才发送 cookies、 HTTP Basic authentication 等验证信息
                            var request = new Request(cacheKey, {credentials: 'same-origin'});
                            return fetch(request).then(function(response) {
                                //通过fetch api请求资源
                                if (!response.ok) {
                                    throw new Error('Request for ' + cacheKey + ' returned a ' +
                                        'response with status ' + response.status);
                                }
                                return cleanResponse(response).then(function(responseToCache) {
                                    //并设置到当前cache中
                                    return cache.put(cacheKey, responseToCache);
                                });
                            });
                        }
                    })
                );
            });
        }).then(function() {

            //强制跳过等待阶段,进入激活阶段
            return self.skipWaiting();

        })
    );
});
self.addEventListener('activate', function(event) {
    //清除cache中原来老的一批相同key的数据
    var setOfExpectedUrls = new Set(urlsToCacheKeys.values());
    event.waitUntil(
        caches.open(cacheName).then(function(cache) {
            return cache.keys().then(function(existingRequests) {
                return Promise.all(
                    existingRequests.map(function(existingRequest) {
                        if (!setOfExpectedUrls.has(existingRequest.url)) {
                            //cache中删除指定对象
                            return cache.delete(existingRequest);
                        }
                    })
                );
            });
        }).then(function() {
            //self相当于webworker线程的当前作用域
            //当一个 service worker 被初始注册时,页面在下次加载之前不会使用它。 claim() 方法会立即控制这些页面
            //从而更新客户端上的serviceworker
            return self.clients.claim();

        })
    );
});

self.addEventListener('fetch', function(event) {
    if (event.request.method === 'GET') {
        // 标识位,用来判断是否需要缓存
        var shouldRespond;
        // 对url进行一些处理,移除一些不必要的参数
        var url = stripIgnoredUrlParameters(event.request.url, ignoreUrlParametersMatching);
        // 如果该url不是我们想要缓存的url,置为false
        shouldRespond = urlsToCacheKeys.has(url);
        // 如果shouldRespond未false,再次验证
        var directoryIndex = 'index.html';
        if (!shouldRespond && directoryIndex) {
            url = addDirectoryIndex(url, directoryIndex);
            shouldRespond = urlsToCacheKeys.has(url);
        }
        // 再次验证,判断其是否是一个navigation类型的请求
        var navigateFallback = '';
        if (!shouldRespond &&
            navigateFallback &&
            (event.request.mode === 'navigate') &&
            isPathWhitelisted([], event.request.url)) {
            url = new URL(navigateFallback, self.location).toString();
            shouldRespond = urlsToCacheKeys.has(url);
        }
        // 如果标识位为true
        if (shouldRespond) {
            event.respondWith(
                caches.open(cacheName).then(function(cache) {
                    //去缓存cache中找对应的url的值
                    return cache.match(urlsToCacheKeys.get(url)).then(function(response) {
                        //如果找到了,就返回value
                        if (response) {
                            return response;
                        }
                        throw Error('The cached response that was expected is missing.');
                    });
                }).catch(function(e) {
                    // 如果没找到则请求该资源
                    console.warn('Couldn\'t serve response for "%s" from cache: %O', event.request.url, e);
                    return fetch(event.request);
                })
            );
        }
    }
});

The strategy here is probably to look for resources in the Cache first, and then request resources if they cannot be found. It can be seen that in order to implement a simpler cache, it is more complicated and cumbersome, so many tools have emerged.

Workbox

Because writing native sw.js directly is more cumbersome and complicated, some tools have appeared, and Workbox is one of the best, launched by the google team.

Introduction

Before Workbox, the GoogleChrome team launched the sw-precache and sw-toolbox libraries earlier, but in the eyes of GoogleChrome engineers, the workbox is a more perfect solution that can really facilitate the unified processing of offline capabilities, so it stopped the sw-precache and sw-toolbox libraries. -Maintenance of precache and sw-toolbox.

user

Many teams also enable this tool to implement serviceworker caching, for example:

  • Taobao Home
  • Netease news wap article page
  • Baidu's Lavas

basic configuration

First of all, we need to introduce the official js of Workbox in the sw.js file of the project. Here we use our own static resources:

importScripts(
    "https://edu-cms.nosdn.127.net/topics/js/workbox_9cc4c3d662a4266fe6691d0d5d83f4dc.js"
);

ImportScripts is the way to load js in webworker.

After the introduction of Workbox, a Workbox object will be mounted globally

if (workbox) {
    console.log('workbox加载成功');
} else {
    console.log('workbox加载失败');
}

Then you need to use the configuration in advance before using other APIs

//关闭控制台中的输出
workbox.setConfig({ debug: false });

You can also specify the name of the Cache during storage:

//设置缓存cachestorage的名称
workbox.core.setCacheNameDetails({
    prefix:'edu-cms',
    suffix:'v1'
});

precache

Workbox's cache is divided into two types, one type of precache and one type of runtimecache.

Precache corresponds to the operation of reading the cache during the installation phase. It allows developers to determine the time and length of the cached file and provide it to the browser without entering the network, which means it can be used to create applications that work offline on the Web.

working principle

When the web application is loaded for the first time, Workbox will download the specified resources and store the specific content and related revision information in indexedDB.

When the resource content and sw.js are updated, Workbox will compare the resources, then store the new resources in the Cache, and modify the version information in indexedDB.

Let's give an example:

workbox.precaching.precacheAndRoute([
    './main.css'
]);

Related information will be saved in indexedDB

At this time, after we change the content of main.css and refresh the page, we will find that Workbox will still read the old main.css content in the Cache unless it is forced to refresh.

Even if we delete main.css from the server, it will not affect the page.

Therefore, all caches in this way need to be configured with a version number. When modifying sw.js, the corresponding version also needs to be changed.

Use practice

Of course, generally some of our resources that do not change frequently will use cdn, so naturally we need to support external resources here. The configuration method is as follows:

var fileList = [
    {
        url:'https://edu-cms.nosdn.127.net/topics/js/cms_specialWebCommon_js_f26c710bd7cd055a64b67456192ed32a.js'
    },
    {
        url:'https://static.ws.126.net/163/frontend/share/css/article.207ac19ad70fd0e54d4a.css'
    }
];


//precache 适用于支持跨域的cdn和域内静态资源
workbox.precaching.suppressWarnings();
workbox.precaching.precacheAndRoute(fileList, {
    "ignoreUrlParametersMatching": [/./]
});

The corresponding resource configuration cross-domain permission header is required here, otherwise it will not be loaded normally. And the file must use the version file name to ensure that the Cache and indexDB will be updated after modification.

After understanding the principle and practice, it shows that this method is suitable for static resources that do not change frequently after going online.

runtimecache

Runtime caching is done in the activated and fetch phases after install.

Since it is sent in the fetch phase, runtimecache often deals with various types of resources, and there are often different caching strategies for different types of resources.

Caching strategy

There are several types of cache planning provided by Workbox, and different configurations can achieve different effects for your own business:

Stale While Revalidate

This strategy means that when the requested route has a corresponding Cache result, it will be returned directly.

While returning the cache result, it will initiate a network request in the background to get the request result and update the Cache cache. If there is no Cache cache, it will directly initiate the network request and return the result, which is very safe for users. Strategies to ensure that users get the requested results as quickly as possible.

But there are also certain disadvantages, that is, there will still be network requests that occupy the user's network bandwidth. You can use the State While Revalidate strategy as follows:

workbox.routing.registerRoute(
    new RegExp('https://edu-cms\.nosdn\.127\.net/topics/'),
    workbox.strategies.staleWhileRevalidate({
        //cache名称
        cacheName: 'lf-sw:static',
        plugins: [
            new workbox.expiration.Plugin({
                //cache最大数量
                maxEntries: 30
            })
        ]
    })
);

Network First

This strategy is that when the request route is matched, the network-first strategy is adopted, which is to give priority to trying to get the return result of the network request. If the result of the network request is obtained, the result is returned to the client and written to the Cache Cache.

If the network request fails, the last cached Cache result will be returned to the client. This strategy is generally suitable for requests that return results are not fixed or require real-time performance, and make a case for network request failures. You can use the Network First strategy as follows:

//自定义要缓存的html列表
var cacheList = [
    '/Hexo/public/demo/PWADemo/workbox/index.html'
];
workbox.routing.registerRoute(
    //自定义过滤方法
    function(event) {
        // 需要缓存的HTML路径列表
        if (event.url.host === 'localhost:63342') {
            if (~cacheList.indexOf(event.url.pathname)) return true;
            else return false;
        } else {
            return false;
        }
    },
    workbox.strategies.networkFirst({
        cacheName: 'lf-sw:html',
        plugins: [
            new workbox.expiration.Plugin({
                maxEntries: 10
            })
        ]
    })
);

Cache First

This strategy means that when the request is matched, the result is directly obtained from the Cache cache. If there is no result in the Cache cache, a network request will be initiated, the network request result will be obtained and the result will be updated to the Cache cache, and the result will be returned to Client. This strategy is more suitable for requests that do not change much and do not require high real-time performance. The Cache First strategy can be used as follows:

workbox.routing.registerRoute(
    new RegExp('https://edu-image\.nosdn\.127\.net/'),
    workbox.strategies.cacheFirst({
        cacheName: 'lf-sw:img',
        plugins: [
            //如果要拿到域外的资源,必须配置
            //因为跨域使用fetch配置了
            //mode: 'no-cors',所以status返回值为0,故而需要兼容
            new workbox.cacheableResponse.Plugin({
                statuses: [0, 200]
            }),
            new workbox.expiration.Plugin({
                maxEntries: 40,
                //缓存的时间
                maxAgeSeconds: 12 * 60 * 60
            })
        ]
    })
);

Network Only

A more straightforward strategy that directly forces the use of normal network requests and returns the results to the client. This strategy is more suitable for requests that require very high real-time performance.

Cache Only

This strategy is also relatively straightforward. It directly uses the results of the Cache and returns the results to the client. This strategy is more suitable for static resource requests that will not change as soon as they go online.

Give a chestnut

It's time to give a chestnut again. This time we use Taobao to see how they configure Service Worker through Workbox:

//首先是异常处理
self.addEventListener('error', function(e) {
  self.clients.matchAll()
    .then(function (clients) {
      if (clients && clients.length) {
        clients[0].postMessage({ 
          type: 'ERROR',
          msg: e.message || null,
          stack: e.error ? e.error.stack : null
        });
      }
    });
});

self.addEventListener('unhandledrejection', function(e) {
  self.clients.matchAll()
    .then(function (clients) {
      if (clients && clients.length) {
        clients[0].postMessage({
          type: 'REJECTION',
          msg: e.reason ? e.reason.message : null,
          stack: e.reason ? e.reason.stack : null
        });
      }
    });
})
//然后引入workbox
importScripts('https://g.alicdn.com/kg/workbox/3.3.0/workbox-sw.js');
workbox.setConfig({
  debug: false,
  modulePathPrefix: 'https://g.alicdn.com/kg/workbox/3.3.0/'
});
//直接激活跳过等待阶段
workbox.skipWaiting();
workbox.clientsClaim();
//定义要缓存的html
var cacheList = [
  '/',
  '/tbhome/home-2017',
  '/tbhome/page/market-list'
];
//html采用networkFirst策略,支持离线也能大体访问
workbox.routing.registerRoute(
  function(event) {
    // 需要缓存的HTML路径列表
    if (event.url.host === 'www.taobao.com') {
      if (~cacheList.indexOf(event.url.pathname)) return true;
      else return false;
    } else {
      return false;
    }
  },
  workbox.strategies.networkFirst({
    cacheName: 'tbh:html',
    plugins: [
      new workbox.expiration.Plugin({
        maxEntries: 10
      })
    ]
  })
);
//静态资源采用staleWhileRevalidate策略,安全可靠
workbox.routing.registerRoute(
  new RegExp('https://g\.alicdn\.com/'),
  workbox.strategies.staleWhileRevalidate({
    cacheName: 'tbh:static',
    plugins: [
      new workbox.expiration.Plugin({
        maxEntries: 20
      })
    ]
  })
);
//图片采用cacheFirst策略,提升速度
workbox.routing.registerRoute(
  new RegExp('https://img\.alicdn\.com/'),
  workbox.strategies.cacheFirst({
    cacheName: 'tbh:img',
    plugins: [
      new workbox.cacheableResponse.Plugin({
        statuses: [0, 200]
      }),
      new workbox.expiration.Plugin({
        maxEntries: 20,
        maxAgeSeconds: 12 * 60 * 60
      })
    ]
  })
);

workbox.routing.registerRoute(
  new RegExp('https://gtms01\.alicdn\.com/'),
  workbox.strategies.cacheFirst({
    cacheName: 'tbh:img',
    plugins: [
      new workbox.cacheableResponse.Plugin({
        statuses: [0, 200]
      }),
      new workbox.expiration.Plugin({
        maxEntries: 30,
        maxAgeSeconds: 12 * 60 * 60
      })
    ]
  })
);

It can be seen that using Workbox is much faster and clearer than using it directly.

principle

At present, there are many articles analyzing Service Worker and Workbox, but there are not many articles introducing the principle of Workbox. Here is a brief introduction to the principle of Workbox, a tool library.

First of all, we will provide a few module diagrams used in our products:

Briefly mention a few highlights of the Workbox source code.

On-demand dependency through Proxy

After getting acquainted with Workbox, you will know that it has many sub-modules, and each sub-module can be imported into the thread through on-demand importScript when it is used.

The principle of achieving on-demand dependence is to proxy the global object Workbox through Proxy:

new Proxy(this, {
  get(t, s) {
    //如果workbox对象上不存在指定对象,就依赖注入该对象对应的脚本
    if (t[s]) return t[s];
    const o = e[s];
    return o && t.loadModule(`workbox-${o}`), t[s];
  }
})

If the corresponding module cannot be found, it will be actively loaded through importScripts:

/**
 * 加载前端模块
 * @param {Strnig} t 
 */
loadModule(t) {
  const e = this.o(t);
  try {
    importScripts(e), (this.s = !0);
  } catch (s) {
    throw (console.error(`Unable to import module '${t}' from '${e}'.`), s);
  }
}

Freeze the external exposure API through freeze

The Workbox.core module provides several core operation modules, such as the DBWrapper that encapsulates the indexedDB operation, the Cache Wrapper that reads the Cache Storage, the fetchWrapper that sends the request, and the logger that logs the management.

In order to prevent the external modification of the api exposed by the internal module, resulting in unpredictable errors, the internal module can freeze the api through Object.freeze:

var _private = /*#__PURE__*/Object.freeze({
    DBWrapper: DBWrapper,
    WorkboxError: WorkboxError,
    assert: finalAssertExports,
    cacheNames: cacheNames,
    cacheWrapper: cacheWrapper,
    fetchWrapper: fetchWrapper,
    getFriendlyURL: getFriendlyURL,
    logger: defaultExport
  });

to sum up

Through the understanding of Service Worker and the application of Workbox, the performance of the product and the experience under weak network conditions can be further improved. Interested students can also review the source code of Workbox. There are many good design patterns and programming styles that are worth learning.
-END-


Youdao Technology Salon


有道AI情报局
788 声望7.9k 粉丝

引用和评论

0 条评论