关于service-worker:网易云课堂-Service-Worker-运用与实践

1次阅读

共计 19835 个字符,预计需要花费 50 分钟才能阅读完成。

前言

本文首先会简略介绍下前端的常见缓存形式,再引入 Service Worker 的概念,针对其原理和如何使用进行介绍。而后基于 google 推出的第三方库 Workbox,在产品中进行使用实际,并对其原理进行简要分析。

作者:刘放

编辑:Ein

前端缓存简介

先简略介绍一下现有的前端缓存技术计划,次要分为 http 缓存和浏览器缓存。

http 缓存

http 缓存都是第二次申请时开始的,这也是个陈词滥调的话题了。无非也是那几个 http 头的问题:

Expires

HTTP1.0 的内容,服务器应用 Expires 头来通知 Web 客户端它能够应用以后正本,直到指定的工夫为止。

Cache-Control

HTTP1.1 引入了 Cathe-Control,它应用 max-age 指定资源被缓存多久,次要是解决了 Expires 一个重大的缺点,就是它设置的是一个固定的工夫点,客户端工夫和服务端工夫可能有误差。
所以个别会把两个头都带上,这种缓存称为强缓存,表现形式为:

Last-Modified / If-Modified-Since

Last-Modified 是服务器通知浏览器该资源的最初批改工夫,If-Modified-Since 是申请头带上的,上次服务器给本人的该资源的最初批改工夫。而后服务器拿去比照。

若资源的最初批改工夫大于 If-Modified-Since,阐明资源又被改变过,则响应整片资源内容,返回状态码 200;

若资源的最初批改工夫小于或等于 If-Modified-Since,阐明资源无新批改,则响应 HTTP 304,告知浏览器持续应用以后版本。

Etag / If-None-Match

后面提到由文件的批改工夫来判断文件是否改变,还是会带来肯定的误差,比方正文等无关紧要的批改等。所以推出了新的形式。

Etag 是由服务端特定算法生成的该文件的惟一标识,而申请头把返回的 Etag 值通过 If-None-Match 再带给服务端,服务端通过比对从而决定是否响应新内容。这也是 304 缓存。

浏览器缓存

Storage

简略的缓存形式有 cookie,localStorage 和 sessionStorage。这里就不具体介绍他们的区别了,这里说下通过 localStorage 来缓存动态资源的优化计划。
localStorage 通常有 5MB 的存储空间,咱们以微信文章页为例。
查看申请发现,根本没有 js 和 css 的申请,因为它把全副的不须要改变的资源都放到了 localStorage 中:

所以微信的文章页加载十分的快。

前端数据库

前端数据库有 WebSql 和 IndexDB,其中 WebSql 被标准废除,他们都有大概 50MB 的最大容量,能够了解为 localStorage 的加强版。

利用缓存

利用缓存次要是通过 manifest 文件来注册被缓存的动态资源,曾经被废除,因为他的设计有些不合理的中央,他在缓存动态文件的同时,也会默认缓存 html 文件。这导致页面的更新只能通过 manifest 文件中的版本号来决定。所以,利用缓存只适宜那种长年不变动的动态网站。如此的不不便,也是被废除的重要起因。

PWA 也使用了该文件,不同于 manifest 简略的将文件通过是否缓存进行分类,PWA 用 manifest 构建了本人的 APP 骨架,并使用 Servie Worker 来管制缓存,这也是明天的配角。

Service Worker

Service Worker 实质上也是浏览器缓存资源用的,只不过他不仅仅是 Cache,也是通过 worker 的形式来进一步优化。
他基于 h5 的 web worker,所以相对不会妨碍以后 js 线程的执行,sw 最重要的工作原理就是:

1、后盾线程:独立于以后网页线程;

2、网络代理:在网页发动申请时代理,来缓存文件。

兼容性


能够看到,基本上新版浏览器还是兼容滴。之前是只有 chrome 和 firefox 反对,当初微软和苹果也相继反对了。

成熟水平

判断一个技术是否值得尝试,必定要思考下它的成熟水平,否则过一段时间又和利用缓存一样被标准摈弃就难堪了。
所以这里我列举了几个应用 Service Worker 的页面:

  • 淘宝
  • 网易新闻
  • 考拉

所以说还是能够尝试下的。

调试办法

一个网站是否启用 Service Worker,能够通过开发者工具中的 Application 来查看:

被 Service Worker 缓存的文件,能够在 Network 中看到 Size 项为 from Service Worker:

也能够在 Application 的 Cache Storage 中查看缓存的具体内容:

如果是具体的断点调试,须要应用对应的线程,不再是 main 线程了,这也是 webworker 的通用调试办法:

应用条件

sw 是基于 HTTPS 的,因为 Service Worker 中波及到申请拦挡,所以必须应用 HTTPS 协定来保障平安。如果是本地调试的话,localhost 是能够的。
而咱们刚好全站强制 https 化,所以正好能够应用。

生命周期

大略能够用如下图片来解释:

注册

要应用 Service Worker,首先须要注册一个 sw,告诉浏览器为该页面调配一块内存,而后 sw 就会进入装置阶段。
一个简略的注册形式:

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

当然也能够思考全面点,参考网易新闻的注册形式:

"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)
        })
    })

后面提到过,因为 sw 会监听和代理所有的申请,所以 sw 的作用域就显得额定的重要了,比如说咱们只想监听咱们专题页的所有申请,就在注册时指定门路:

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

这样就只会对 topics/ 上面的门路进行优化。

installing

咱们注册后,浏览器就会开始装置 sw,能够通过事件监听:

//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);
            })
    );

});

装置时,sw 就开始缓存文件了,会查看所有文件的缓存状态,如果都曾经缓存了,则装置胜利,进入下一阶段。

activated

如果是第一次加载 sw,在装置后,会间接进入 activated 阶段,而如果 sw 进行更新,状况就会显得简单一些。流程如下:

首先老的 sw 为 A,新的 sw 版本为 B。
B 进入 install 阶段,而 A 还处于工作状态,所以 B 进入 waiting 阶段。只有等到 A 被 terminated 后,B 能力失常替换 A 的工作。

这个 terminated 的机会有如下几种形式:

1、敞开浏览器一段时间;

2、手动革除 Service Worker;

3、在 sw 装置时间接跳过 waiting 阶段

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

而后就进入了 activated 阶段,激活 sw 工作。

activated 阶段能够做很多有意义的事件,比方更新存储在 Cache 中的 key 和 value:

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

这个闲暇状态个别是不可见的,这种个别阐明 sw 的事件都处理完毕了,而后处于闲置状态了。

浏览器会周期性的轮询,去开释处于 idle 的 sw 占用的资源。

fetch

该阶段是 sw 最为要害的一个阶段,用于拦挡代理所有指定的申请,并进行对应的操作。

所有的缓存局部,都是在该阶段,这里举一个简略的例子:

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

生命周期大略讲清楚了,咱们就以一个具体的例子来阐明下原生的 serviceworker 是如何在生产环境中应用的吧。

举个栗子

咱们能够以网易新闻的 wap 页为例, 其针对不怎么变动的动态资源开启了 sw 缓存,具体的 sw.js 逻辑和解读如下:

'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);
                })
            );
        }
    }
});

这里的策略大略就是优先在 Cache 中寻找资源,如果找不到再申请资源。能够看出,为了实现一个较为简单的缓存,还是比较复杂和繁琐的,所以很多工具就应运而生了。

Workbox

因为间接写原生的 sw.js,比拟繁琐和简单,所以一些工具就呈现了,而 Workbox 是其中的佼佼者,由 google 团队推出。

简介

在 Workbox 之前,GoogleChrome 团队较早工夫推出过 sw-precache 和 sw-toolbox 库,然而在 GoogleChrome 工程师们看来,workbox 才是真正能不便对立的解决离线能力的更完满的计划,所以进行了对 sw-precache 和 sw-toolbox 的保护。

使用者

有很多团队也是启用该工具来实现 serviceworker 的缓存,比如说:

  • 淘宝首页
  • 网易新闻 wap 文章页
  • 百度的 Lavas

根本配置

首先,须要在我的项目的 sw.js 文件中,引入 Workbox 的官网 js,这里用了咱们本人的动态资源:

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

其中 importScripts 是 webworker 中加载 js 的形式。

引入 Workbox 后,全局会挂载一个 Workbox 对象

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

而后须要在应用其余的 api 前,提前应用配置

// 敞开控制台中的输入
workbox.setConfig({debug: false});

也能够对立指定存储时 Cache 的名称:

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

precache

Workbox 的缓存分为两种,一种的 precache,一种的 runtimecache。

precache 对应的是在 installing 阶段进行读取缓存的操作。它让开发人员能够确定缓存文件的工夫和长度,以及在不进入网络的状况下将其提供给浏览器,这意味着它能够用于创立 Web 离线工作的利用。

工作原理

首次加载 Web 应用程序时,Workbox 会下载指定的资源,并存储具体内容和相干订正的信息在 indexedDB 中。

当资源内容和 sw.js 更新后,Workbox 会去比对资源,而后将新的资源存入 Cache,并批改 indexedDB 中的版本信息。

咱们举一个例子:

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

indexedDB 中会保留其相干信息

这个时候咱们把 main.css 的内容扭转后,再刷新页面,会发现除非强制刷新,否则 Workbox 还是会读取 Cache 中存在的老的 main.css 内容。

即便咱们把 main.css 从服务器上删除,也不会对页面造成影响。

所以这种形式的缓存都须要配置一个版本号。在批改 sw.js 时,对应的版本也须要变更。

应用实际

当然了,个别咱们的一些不常常变的资源,都会应用 cdn,所以这里天然就须要反对域外资源了,配置形式如下:

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": [/./]
});

这里须要对应的资源配置跨域容许头,否则是不能失常加载的。且文件都要以版本文件名的形式,来确保批改后 Cache 和 indexDB 会失去更新。

了解了原理和实际后,阐明这种形式适宜于上线后就不会常常变动的动态资源。

runtimecache

运行时缓存是在 install 之后,activated 和 fetch 阶段做的事件。

既然在 fetch 阶段发送,那么 runtimecache 往往应答着各种类型的资源,对于不同类型的资源往往也有不同的缓存策略。

缓存策略

Workbox 提供的缓存策动有以下几种,通过不同的配置能够针对本人的业务达到不同的成果:

Stale While Revalidate

这种策略的意思是当申请的路由有对应的 Cache 缓存后果就间接返回,

在返回 Cache 缓存后果的同时会在后盾发动网络申请拿到申请后果并更新 Cache 缓存,如果原本就没有 Cache 缓存的话,间接就发动网络申请并返回后果,这对用户来说是一种十分平安的策略,能保障用户最疾速的拿到申请的后果。

然而也有肯定的毛病,就是还是会有网络申请占用了用户的网络带宽。能够像如下的形式应用 State While Revalidate 策略:

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

这种策略就是当申请路由是被匹配的,就采纳网络优先的策略,也就是优先尝试拿到网络申请的返回后果,如果拿到网络申请的后果,就将后果返回给客户端并且写入 Cache 缓存。

如果网络申请失败,那最初被缓存的 Cache 缓存后果就会被返回到客户端,这种策略个别实用于返回后果不太固定或对实时性有要求的申请,为网络申请失败进行兜底。能够像如下形式应用 Network First 策略:

// 自定义要缓存的 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

这个策略的意思就是当匹配到申请之后间接从 Cache 缓存中获得后果,如果 Cache 缓存中没有后果,那就会发动网络申请,拿到网络申请后果并将后果更新至 Cache 缓存,并将后果返回给客户端。这种策略比拟适宜后果不怎么变动且对实时性要求不高的申请。能够像如下形式应用 Cache First 策略:

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

比拟间接的策略,间接强制应用失常的网络申请,并将后果返回给客户端,这种策略比拟适宜对实时性要求十分高的申请。

Cache Only

这个策略也比拟间接,间接应用 Cache 缓存的后果,并将后果返回给客户端,这种策略比拟适宜一上线就不会变的动态资源申请。

举个栗子

又到了举个栗子的阶段了,这次咱们用淘宝好了,看看他们是如何通过 Workbox 来配置 Service Worker 的:

// 首先是异样解决
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
      })
    ]
  })
);

能够看出,应用 Workbox 比起间接手撸来,要快很多,也明确很多。

原理

目前剖析 Service Worker 和 Workbox 的文章不少,然而介绍 Workbox 原理的文章却不多。这里简略介绍下 Workbox 这个工具库的原理。

首先将几个咱们产品用到的模块图奉上:

简略提几个 Workbox 源码的亮点。

通过 Proxy 按需依赖

相熟了 Workbox 后会得悉,它是有很多个子模块的,各个子模块再通过用到的时候按需 importScript 到线程中。

做到按需依赖的原理就是通过 Proxy 对全局对象 Workbox 进行代理:

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];
  }
})

如果找不到对应模块,则通过 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 解冻对外裸露 api

Workbox.core 模块中提供了几个外围操作模块,如封装了 indexedDB 操作的 DBWrapper、对 Cache Storage 进行读取的 Cache Wrapper,以及发送申请的 fetchWrapper 和日志治理的 logger 等等。

为了避免内部对外部模块裸露进来的 api 进行批改,导致呈现不可预估的谬误,外部模块能够通过 Object.freeze 将 api 进行解冻爱护:

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

总结

通过对 Service Worker 的了解和 Workbox 的利用,能够进一步晋升产品的性能和弱网状况下的体验。有趣味的同学也能够对 Workbox 的源码细细评读,其中还有很多不错的设计模式和编程格调值得学习。
-END-


有道技术沙龙

正文完
 0