美文网首页web前端-javascript
浏览器缓存--ServiceWorker

浏览器缓存--ServiceWorker

作者: 老鼠AI大米_Java全栈 | 来源:发表于2021-01-10 21:19 被阅读0次

    Service Worker 是 Chrome 团队提出和力推的一个 WEB API,用于给 web 应用提供高级的可持续的后台处理能力。

    什么是service worker

    image.png

    Service Workers 就像介于服务器和网页之间的拦截器,能够拦截进出的HTTP 请求,从而完全控制你的网站。

    最主要的特点

    • 在页面中注册并安装成功后,运行于浏览器后台,不受页面刷新的影响,可以监听和截拦作用域范围内所有页面的 HTTP 请求。
    • 网站必须使用 HTTPS。除了使用本地开发环境调试时(如域名使用 localhost)
    • 运行于浏览器后台,可以控制打开的作用域范围下所有的页面请求
    • 单独的作用域范围,单独的运行环境和执行线程
    • 不能操作页面 DOM。但可以通过事件机制来处理
    • 事件驱动型服务线程

    为什么要求网站必须是HTTPS的,大概是因为service worker权限太大能拦截所有页面的请求吧,如果http的网站安装service worker很容易被攻击

    支持的浏览器

    image.png

    HTTP缓存与service worker缓存

    • HTTP缓存
      Web 服务器可以使用 Expires 首部来通知 Web 客户端,它可以使用资源的当前副本,直到指定的“过期时间”。反过来,浏览器可以缓存此资源,并且只有在有效期满后才会再次检查新版本。
      使用 HTTP 缓存意味着你要依赖服务器来告诉你何时缓存资源和何时过期。

    • service worker缓存
      Service Workers 的强大在于它们拦截 HTTP 请求的能力
      进入任何传入的 HTTP 请求,并决定想要如何响应。在你的 Service Worker 中,可以编写逻辑来决定想要缓存的资源,以及需要满足什么条件和资源需要缓存多久。一切尽归你掌控!

    生命周期

    image.png

    当用户首次导航至 URL 时,服务器会返回响应的网页。

    • 第1步:当你调用 register() 函数时, Service Worker 开始下载。
    • 第2步:在注册过程中,浏览器会下载、解析并执行 Service Worker ()。如果在此步骤中出现任何错误,register() 返回的 promise 都会执行 reject 操作,并且 Service Worker 会被废弃。
    • 第3步:一旦 Service Worker 成功执行了,install 事件就会激活
    • 第4步:安装完成,Service Worker 便会激活,并控制在其范围内的一切。如果生命周期中的所有事件都成功了,Service Worker 便已准备就绪,随时可以使用了!
      chrome://serviceworker-internals 来了解当前浏览器中所有已安装Service Worker的详细情况

    1. 注册

    要使用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/下面的路径进行优化。

    2. 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就开始缓存文件了,会检查所有文件的缓存状态,如果都已经缓存了,则安装成功,进入下一阶段。

    3. activated

    如果是第一次加载sw,在安装后,会直接进入activated阶段,而如果sw进行更新,情况就会显得复杂一些。流程如下:

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


    image.png

    这个terminated的时机有如下几种方式:

    • 关闭浏览器一段时间;
    • 手动清除serviceworker;
    • 在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)
        );
    });
    

    4. idle

    这个空闲状态一般是不可见的,这种一般说明sw的事情都处理完毕了,然后处于闲置状态了。

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

    5. 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是如何在生产环境中使用的吧。

    6. 举个例子

    我们可以以网易新闻的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团队推出。

    基本配置

    首先,需要在项目的sw.js文件中,引入workbox的官方js,这里用了我们自己的静态资源:

    importScripts(
        "https://edu-cms.nosdn.127.net/topics/js/workbox_9cc4c3d662a4266fe6691d0d5d83f4dc.js"
    );
    //引入workbox后,全局会挂载一个workbox对象
    if (workbox) {
        console.log('workbox加载成功');
    } else {
        console.log('workbox加载失败');
    }
    //关闭控制台中的输出
    workbox.setConfig({ debug: false });
    //设置缓存cachestorage的名称 统一指定存储时cache的名称:
    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'
    ]);
    
    image.png

    indexedDB中会保存其相关信息


    image.png

    这个时候我们把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提供的缓存策划有以下几种,通过不同的配置可以针对自己的业务达到不同的效果:

    staleWhileRevalidate

    这种策略的意思是当请求的路由有对应的 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
                })
            ]
        })
    );
    

    networkFirst

    这种策略就是当请求路由是被匹配的,就采用网络优先的策略,也就是优先尝试拿到网络请求的返回结果,如果拿到网络请求的结果,就将结果返回给客户端并且写入 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
                })
            ]
        })
    );
    

    cacheFirst
    这个策略的意思就是当匹配到请求之后直接从 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
                })
            ]
        })
    );
    

    networkOnly

    比较直接的策略,直接强制使用正常的网络请求,并将结果返回给客户端,这种策略比较适合对实时性要求非常高的请求。

    cacheOnly

    这个策略也比较直接,直接使用 Cache 缓存的结果,并将结果返回给客户端,这种策略比较适合一上线就不会变的静态资源请求。

    举个例子
    又到了举个栗子的阶段了,这次我们用淘宝好了,看看他们是如何通过workbox来配置serviceworker的:

    //首先是异常处理
    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
          })
        ]
      })
    );
    

    原理
    目前分析serviceworker和workbox的文章不少,但是介绍workbox原理的文章却不多。这里简单介绍下workbox这个工具库的原理。

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


    image.png

    简单提几个workbox源码的亮点。

    通过Proxy按需依赖

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


    image.png

    做到按需依赖的原理就是通过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、对cacheStorage进行读取的cacheWrapper,以及发送请求的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
      });
    

    小结
    通过对serviceworker的理解和workbox的应用,可以进一步提升产品的性能和弱网情况下的体验。有兴趣的同学也可以对workbox的源码细细评读,其中还有很多不错的设计模式和编程风格值得学习。

    相关文章

      网友评论

        本文标题:浏览器缓存--ServiceWorker

        本文链接:https://www.haomeiwen.com/subject/jkjlaktx.html