网易云课堂 Service Worker 运用与实践

{"type":"doc","content":[{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"前言","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本文首先会简单介绍下前端的常见缓存方式,再引入Service Worker的概念,针对其原理和如何运用进行介绍。然后基于google推出的第三方库Workbox,在产品中进行运用实践,并对其原理进行简要剖析。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"作者:刘放","attrs":{}}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"编辑:Ein","attrs":{}}]}],"attrs":{}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"前端缓存简介","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"先简单介绍一下现有的前端缓存技术方案,主要分为http缓存和浏览器缓存。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"http缓存","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"http缓存都是第二次请求时开始的,这也是个老生常谈的话题了。无非也是那几个http头的问题:","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"Expires","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"HTTP1.0的内容,服务器使用Expires头来告诉Web客户端它可以使用当前副本,直到指定的时间为止。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"Cache-Control","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"HTTP1.1引入了Cathe-Control,它使用max-age指定资源被缓存多久,主要是解决了Expires一个重大的缺陷,就是它设置的是一个固定的时间点,客户端时间和服务端时间可能有误差。所以一般会把两个头都带上,这种缓存称为强缓存,表现形式为:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/5f/5fa07317a3bf2804ae3594950a6388fc.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"Last-Modified / If-Modified-Since","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Last-Modified是服务器告诉浏览器该资源的最后修改时间,If-Modified-Since是请求头带上的,上次服务器给自己的该资源的最后修改时间。然后服务器拿去对比。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"若资源的最后修改时间大于If-Modified-Since,说明资源又被改动过,则响应整片资源内容,返回状态码200;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"若资源的最后修改时间小于或等于If-Modified-Since,说明资源无新修改,则响应HTTP 304,告知浏览器继续使用当前版本。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"Etag / If-None-Match","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"前面提到由文件的修改时间来判断文件是否改动,还是会带来一定的误差,比如注释等无关紧要的修改等。所以推出了新的方式。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Etag是由服务端特定算法生成的该文件的唯一标识,而请求头把返回的Etag值通过If-None-Match再带给服务端,服务端通过比对从而决定是否响应新内容。这也是304缓存。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"浏览器缓存","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"Storage","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"简单的缓存方式有cookie,localStorage和sessionStorage。这里就不详细介绍他们的区别了,这里说下通过localStorage来缓存静态资源的优化方案。localStorage通常有5MB的存储空间,我们以微信文章页为例。查看请求发现,基本没有js和css的请求,因为它把全部的不需要改动的资源都放到了localStorage中:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/51/518c246ae2a16f8af7d94fcd7dcb786b.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"所以微信的文章页加载非常的快。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"前端数据库","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"前端数据库有WebSql和IndexDB,其中WebSql被规范废弃,他们都有大约50MB的最大容量,可以理解为localStorage的加强版。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"应用缓存","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"应用缓存主要是通过manifest文件来注册被缓存的静态资源,已经被废弃,因为他的设计有些不合理的地方,他在缓存静态文件的同时,也会默认缓存html文件。这导致页面的更新只能通过manifest文件中的版本号来决定。所以,应用缓存只适合那种常年不变化的静态网站。如此的不方便,也是被废弃的重要原因。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"PWA也运用了该文件,不同于manifest简单的将文件通过是否缓存进行分类,PWA用manifest构建了自己的APP骨架,并运用Servie Worker来控制缓存,这也是今天的主角。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"Service Worker","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Service Worker本质上也是浏览器缓存资源用的,只不过他不仅仅是Cache,也是通过worker的方式来进一步优化。他基于h5的web worker,所以绝对不会阻碍当前js线程的执行,sw最重要的工作原理就是:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1、后台线程:独立于当前网页线程;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2、网络代理:在网页发起请求时代理,来缓存文件。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"兼容性","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/2c/2c672c3052b44ba0cef305e990d0a7dd.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"可以看到,基本上新版浏览器还是兼容滴。之前是只有chrome和firefox支持,现在微软和苹果也相继支持了。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"成熟程度","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"判断一个技术是否值得尝试,肯定要考虑下它的成熟程度,否则过一段时间又和应用缓存一样被规范抛弃就尴尬了。所以这里我列举了几个使用Service Worker的页面:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"淘宝","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"网易新闻","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"考拉","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"所以说还是可以尝试下的。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"调试方法","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一个网站是否启用Service Worker,可以通过开发者工具中的Application来查看:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/3c/3c9109a8ddb4c470131052c6256fc08c.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"被Service Worker缓存的文件,可以在Network中看到Size项为from Service Worker:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/5c/5c868b26aae44b56f78d9eab020f87e1.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"也可以在Application的Cache Storage中查看缓存的具体内容:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/c9/c94bf24f3b649ce0ce6a4aa16aa082cd.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果是具体的断点调试,需要使用对应的线程,不再是main线程了,这也是webworker的通用调试方法:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/1c/1c885217cd99eaf92e1826791d00104f.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"使用条件","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"sw 是基于 HTTPS 的,因为Service Worker中涉及到请求拦截,所以必须使用HTTPS协议来保障安全。如果是本地调试的话,localhost是可以的。而我们刚好全站强制https化,所以正好可以使用。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"生命周期","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"大概可以用如下图片来解释:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/bf/bfce3db76772bdeb7bc5c1a972ab1239.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"注册","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"要使用Service Worker,首先需要注册一个sw,通知浏览器为该页面分配一块内存,然后sw就会进入安装阶段。一个简单的注册方式:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"(function() {\n if('serviceWorker' in navigator) {\n navigator.serviceWorker.register('./sw.js');\n }\n})()\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"当然也可以考虑全面点,参考网易新闻的注册方式:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"\"serviceWorker\" in navigator && window.addEventListener(\"load\",\n function() {\n var e = location.pathname.match(/\\/news\\/[a-z]{1,}\\//)[0] + \"article-sw.js?v=08494f887a520e6455fa\";\n navigator.serviceWorker.register(e).then(function(n) {\n n.onupdatefound = function() {\n var e = n.installing;\n e.onstatechange = function() {\n switch (e.state) {\n case \"installed\":\n navigator.serviceWorker.controller ? console.log(\"New or updated content is available.\") : console.log(\"Content is now available offline!\");\n break;\n case \"redundant\":\n console.error(\"The installing service worker became redundant.\")\n }\n }\n }\n }).\n catch(function(e) {\n console.error(\"Error during service worker registration:\", e)\n })\n })\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"前面提到过,由于sw会监听和代理所有的请求,所以sw的作用域就显得额外的重要了,比如说我们只想监听我们专题页的所有请求,就在注册时指定路径:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"navigator.serviceWorker.register('/topics/sw.js');\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"这样就只会对topics/下面的路径进行优化。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"installing","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我们注册后,浏览器就会开始安装sw,可以通过事件监听:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"//service worker安装成功后开始缓存所需的资源\nvar CACHE_PREFIX = 'cms-sw-cache';\nvar CACHE_VERSION = '0.0.20';\nvar CACHE_NAME = CACHE_PREFIX+'-'+CACHE_VERSION;\nvar allAssets = [\n './main.css'\n];\nself.addEventListener('install', function(event) {\n\n //调试时跳过等待过程\n self.skipWaiting();\n\n\n // Perform install steps\n //首先 event.waitUntil 你可以理解为 new Promise,\n //它接受的实际参数只能是一个 promise,因为,caches 和 cache.addAll 返回的都是 Promise,\n //这里就是一个串行的异步加载,当所有加载都成功时,那么 SW 就可以下一步。\n //另外,event.waitUntil 还有另外一个重要好处,它可以用来延长一个事件作用的时间,\n //这里特别针对于我们 SW 来说,比如我们使用 caches.open 是用来打开指定的缓存,但开启的时候,\n //并不是一下就能调用成功,也有可能有一定延迟,由于系统会随时睡眠 SW,所以,为了防止执行中断,\n //就需要使用 event.waitUntil 进行捕获。另外,event.waitUntil 会监听所有的异步 promise\n //如果其中一个 promise 是 reject 状态,那么该次 event 是失败的。这就导致,我们的 SW 开启失败。\n event.waitUntil(\n caches.open(CACHE_NAME)\n .then(function(cache) {\n console.log('[SW]: Opened cache');\n return cache.addAll(allAssets);\n })\n );\n\n});\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"安装时,sw就开始缓存文件了,会检查所有文件的缓存状态,如果都已经缓存了,则安装成功,进入下一阶段。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"activated","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果是第一次加载sw,在安装后,会直接进入activated阶段,而如果sw进行更新,情况就会显得复杂一些。流程如下:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首先老的sw为A,新的sw版本为B。B进入install阶段,而A还处于工作状态,所以B进入waiting阶段。只有等到A被terminated后,B才能正常替换A的工作。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/f9/f91c95334cb6c14dd680286f923a9aac.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"这个terminated的时机有如下几种方式:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1、关闭浏览器一段时间;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2、手动清除Service Worker;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"3、在sw安装时直接跳过waiting阶段","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"//service worker安装成功后开始缓存所需的资源\nself.addEventListener('install', function(event) {\n //跳过等待过程\n self.skipWaiting();\n});\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"然后就进入了activated阶段,激活sw工作。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"activated阶段可以做很多有意义的事情,比如更新存储在Cache中的key和value:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"var CACHE_PREFIX = 'cms-sw-cache';\nvar CACHE_VERSION = '0.0.20';\n/**\n * 找出对应的其他key并进行删除操作\n * @returns {*}\n */\nfunction deleteOldCaches() {\n return caches.keys().then(function (keys) {\n var all = keys.map(function (key) {\n if (key.indexOf(CACHE_PREFIX) !== -1 && key.indexOf(CACHE_VERSION) === -1){\n console.log('[SW]: Delete cache:' + key);\n return caches.delete(key);\n }\n });\n return Promise.all(all);\n });\n}\n//sw激活阶段,说明上一sw已失效\nself.addEventListener('activate', function(event) {\n\n\n event.waitUntil(\n // 遍历 caches 里所有缓存的 keys 值\n caches.keys().then(deleteOldCaches)\n );\n});\n","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"idle","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"这个空闲状态一般是不可见的,这种一般说明sw的事情都处理完毕了,然后处于闲置状态了。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"浏览器会周期性的轮询,去释放处于idle的sw占用的资源。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"fetch","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"该阶段是sw最为关键的一个阶段,用于拦截代理所有指定的请求,并进行对应的操作。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"所有的缓存部分,都是在该阶段,这里举一个简单的例子:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"//监听浏览器的所有fetch请求,对已经缓存的资源使用本地缓存回复\nself.addEventListener('fetch', function(event) {\n event.respondWith(\n caches.match(event.request)\n .then(function(response) {\n //该fetch请求已经缓存\n if (response) {\n return response;\n }\n return fetch(event.request);\n }\n )\n );\n});\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"生命周期大概讲清楚了,我们就以一个具体的例子来说明下原生的serviceworker是如何在生产环境中使用的吧。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"举个栗子","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我们可以以网易新闻的wap页为例,其针对不怎么变化的静态资源开启了sw缓存,具体的sw.js逻辑和解读如下:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"'use strict';\n//需要缓存的资源列表\nvar precacheConfig = [\n [\"https://static.ws.126.net/163/wap/f2e/milk_index/bg_img_sm_minfy.png\",\n \"c4f55f5a9784ed2093009dadf1e954f9\"],\n [\"https://static.ws.126.net/163/wap/f2e/milk_index/change.png\",\n \"9af1b102ef784b8ff08567ba25f31d95\"],\n [\"https://static.ws.126.net/163/wap/f2e/milk_index/icon-download.png\",\n \"1c02c724381d77a1a19ca18925e9b30c\"],\n [\"https://static.ws.126.net/163/wap/f2e/milk_index/icon-login-dark.png\",\n \"b59ba5abe97ff29855dfa4bd3a7a9f35\"],\n [\"https://static.ws.126.net/163/wap/f2e/milk_index/icon-refresh.png\",\n \"a5b1084e41939885969a13f8dbc88abd\"],\n [\"https://static.ws.126.net/163/wap/f2e/milk_index/icon-video-play.png\",\n \"065ff496d7d36345196d254aff027240\"],\n [\"https://static.ws.126.net/163/wap/f2e/milk_index/icon.ico\",\n \"a14e5365cc2b27ec57e1ab7866c6a228\"],\n [\"https://static.ws.126.net/163/wap/f2e/milk_index/iconfont_1.eot\",\n \"e4d2788fef09eb0630d66cc7e6b1ab79\"],\n [\"https://static.ws.126.net/163/wap/f2e/milk_index/iconfont_1.svg\",\n \"d9e57c341608fddd7c140570167bdabb\"],\n [\"https://static.ws.126.net/163/wap/f2e/milk_index/iconfont_1.ttf\",\n \"f422407038a3180bb3ce941a4a52bfa2\"],\n [\"https://static.ws.126.net/163/wap/f2e/milk_index/iconfont_1.woff\",\n \"ead2bef59378b00425779c4ca558d9bd\"],\n [\"https://static.ws.126.net/163/wap/f2e/milk_index/index.5cdf03e8.js\",\n \"6262ac947d12a7b0baf32be79e273083\"],\n [\"https://static.ws.126.net/163/wap/f2e/milk_index/index.bc729f8a.css\",\n \"58e54a2c735f72a24715af7dab757739\"],\n [\"https://static.ws.126.net/163/wap/f2e/milk_index/logo-app-bohe.png\",\n \"ac5116d8f5fcb3e7c49e962c54ff9766\"],\n [\"https://static.ws.126.net/163/wap/f2e/milk_index/logo-app-mail.png\",\n \"a12bbfaeee7fbf025d5ee85634fca1eb\"],\n [\"https://static.ws.126.net/163/wap/f2e/milk_index/logo-app-manhua.png\",\n \"b8905b119cf19a43caa2d8a0120bdd06\"],\n [\"https://static.ws.126.net/163/wap/f2e/milk_index/logo-app-open.png\",\n \"b7cc76ba7874b2132f407049d3e4e6e6\"],\n [\"https://static.ws.126.net/163/wap/f2e/milk_index/logo-app-read.png\",\n \"e6e9c8bc72f857960822df13141cbbfd\"],\n [\"https://static.ws.126.net/163/wap/f2e/milk_index/logo-site.png\",\n \"2b0d728b46518870a7e2fe424e9c0085\"],\n [\"https://static.ws.126.net/163/wap/f2e/milk_index/version_no_pic.png\",\n \"aef80885188e9d763282735e53b25c0e\"],\n [\"https://static.ws.126.net/163/wap/f2e/milk_index/version_pc.png\",\n \"42f3cc914eab7be4258fac3a4889d41d\"],\n [\"https://static.ws.126.net/163/wap/f2e/milk_index/version_standard.png\",\n \"573408fa002e58c347041e9f41a5cd0d\"]\n];\nvar cacheName = 'sw-precache-v3-new-wap-index-' + (self.registration ? self.registration.scope : '');\n\nvar ignoreUrlParametersMatching = [/^utm_/];\n\nvar addDirectoryIndex = function(originalUrl, index) {\n var url = new URL(originalUrl);\n if (url.pathname.slice(-1) === '/') {\n url.pathname += index;\n }\n return url.toString();\n};\nvar cleanResponse = function(originalResponse) {\n // If this is not a redirected response, then we don't have to do anything.\n if (!originalResponse.redirected) {\n return Promise.resolve(originalResponse);\n }\n // Firefox 50 and below doesn't support the Response.body stream, so we may\n // need to read the entire body to memory as a Blob.\n var bodyPromise = 'body' in originalResponse ?\n Promise.resolve(originalResponse.body) :\n originalResponse.blob();\n return bodyPromise.then(function(body) {\n // new Response() is happy when passed either a stream or a Blob.\n return new Response(body, {\n headers: originalResponse.headers,\n status: originalResponse.status,\n statusText: originalResponse.statusText\n });\n });\n};\nvar createCacheKey = function(originalUrl, paramName, paramValue,\n dontCacheBustUrlsMatching) {\n // Create a new URL object to avoid modifying originalUrl.\n var url = new URL(originalUrl);\n // If dontCacheBustUrlsMatching is not set, or if we don't have a match,\n // then add in the extra cache-busting URL parameter.\n if (!dontCacheBustUrlsMatching ||\n !(url.pathname.match(dontCacheBustUrlsMatching))) {\n url.search += (url.search ? '&' : '') +\n encodeURIComponent(paramName) + '=' + encodeURIComponent(paramValue);\n }\n return url.toString();\n};\nvar isPathWhitelisted = function(whitelist, absoluteUrlString) {\n // If the whitelist is empty, then consider all URLs to be whitelisted.\n if (whitelist.length === 0) {\n return true;\n }\n // Otherwise compare each path regex to the path of the URL passed in.\n var path = (new URL(absoluteUrlString)).pathname;\n return whitelist.some(function(whitelistedPathRegex) {\n return path.match(whitelistedPathRegex);\n });\n};\nvar stripIgnoredUrlParameters = function(originalUrl,\n ignoreUrlParametersMatching) {\n var url = new URL(originalUrl);\n // Remove the hash; see https://github.com/GoogleChrome/sw-precache/issues/290\n url.hash = '';\n url.search = url.search.slice(1) // Exclude initial '?'\n .split('&') // Split into an array of 'key=value' strings\n .map(function(kv) {\n return kv.split('='); // Split each 'key=value' string into a [key, value] array\n })\n .filter(function(kv) {\n return ignoreUrlParametersMatching.every(function(ignoredRegex) {\n return !ignoredRegex.test(kv[0]); // Return true iff the key doesn't match any of the regexes.\n });\n })\n .map(function(kv) {\n return kv.join('='); // Join each [key, value] array into a 'key=value' string\n })\n .join('&'); // Join the array of 'key=value' strings into a string with '&' in between each\n return url.toString();\n};\n\nvar hashParamName = '_sw-precache';\n//定义需要缓存的url列表\nvar urlsToCacheKeys = new Map(\n precacheConfig.map(function(item) {\n var relativeUrl = item[0];\n var hash = item[1];\n var absoluteUrl = new URL(relativeUrl, self.location);\n var cacheKey = createCacheKey(absoluteUrl, hashParamName, hash, false);\n return [absoluteUrl.toString(), cacheKey];\n })\n);\n//把cache中的url提取出来,进行去重操作\nfunction setOfCachedUrls(cache) {\n return cache.keys().then(function(requests) {\n //提取url\n return requests.map(function(request) {\n return request.url;\n });\n }).then(function(urls) {\n //去重\n return new Set(urls);\n });\n}\n//sw安装阶段\nself.addEventListener('install', function(event) {\n event.waitUntil(\n //首先尝试取出存在客户端cache中的数据\n caches.open(cacheName).then(function(cache) {\n return setOfCachedUrls(cache).then(function(cachedUrls) {\n return Promise.all(\n Array.from(urlsToCacheKeys.values()).map(function(cacheKey) {\n //如果需要缓存的url不在当前cache中,则添加到cache\n if (!cachedUrls.has(cacheKey)) {\n //设置same-origin是为了兼容旧版本safari中其默认值不为same-origin,\n //只有当URL与响应脚本同源才发送 cookies、 HTTP Basic authentication 等验证信息\n var request = new Request(cacheKey, {credentials: 'same-origin'});\n return fetch(request).then(function(response) {\n //通过fetch api请求资源\n if (!response.ok) {\n throw new Error('Request for ' + cacheKey + ' returned a ' +\n 'response with status ' + response.status);\n }\n return cleanResponse(response).then(function(responseToCache) {\n //并设置到当前cache中\n return cache.put(cacheKey, responseToCache);\n });\n });\n }\n })\n );\n });\n }).then(function() {\n\n //强制跳过等待阶段,进入激活阶段\n return self.skipWaiting();\n\n })\n );\n});\nself.addEventListener('activate', function(event) {\n //清除cache中原来老的一批相同key的数据\n var setOfExpectedUrls = new Set(urlsToCacheKeys.values());\n event.waitUntil(\n caches.open(cacheName).then(function(cache) {\n return cache.keys().then(function(existingRequests) {\n return Promise.all(\n existingRequests.map(function(existingRequest) {\n if (!setOfExpectedUrls.has(existingRequest.url)) {\n //cache中删除指定对象\n return cache.delete(existingRequest);\n }\n })\n );\n });\n }).then(function() {\n //self相当于webworker线程的当前作用域\n //当一个 service worker 被初始注册时,页面在下次加载之前不会使用它。 claim() 方法会立即控制这些页面\n //从而更新客户端上的serviceworker\n return self.clients.claim();\n\n })\n );\n});\n\nself.addEventListener('fetch', function(event) {\n if (event.request.method === 'GET') {\n // 标识位,用来判断是否需要缓存\n var shouldRespond;\n // 对url进行一些处理,移除一些不必要的参数\n var url = stripIgnoredUrlParameters(event.request.url, ignoreUrlParametersMatching);\n // 如果该url不是我们想要缓存的url,置为false\n shouldRespond = urlsToCacheKeys.has(url);\n // 如果shouldRespond未false,再次验证\n var directoryIndex = 'index.html';\n if (!shouldRespond && directoryIndex) {\n url = addDirectoryIndex(url, directoryIndex);\n shouldRespond = urlsToCacheKeys.has(url);\n }\n // 再次验证,判断其是否是一个navigation类型的请求\n var navigateFallback = '';\n if (!shouldRespond &&\n navigateFallback &&\n (event.request.mode === 'navigate') &&\n isPathWhitelisted([], event.request.url)) {\n url = new URL(navigateFallback, self.location).toString();\n shouldRespond = urlsToCacheKeys.has(url);\n }\n // 如果标识位为true\n if (shouldRespond) {\n event.respondWith(\n caches.open(cacheName).then(function(cache) {\n //去缓存cache中找对应的url的值\n return cache.match(urlsToCacheKeys.get(url)).then(function(response) {\n //如果找到了,就返回value\n if (response) {\n return response;\n }\n throw Error('The cached response that was expected is missing.');\n });\n }).catch(function(e) {\n // 如果没找到则请求该资源\n console.warn('Couldn\\'t serve response for \"%s\" from cache: %O', event.request.url, e);\n return fetch(event.request);\n })\n );\n }\n }\n});\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"这里的策略大概就是优先在Cache中寻找资源,如果找不到再请求资源。可以看出,为了实现一个较为简单的缓存,还是比较复杂和繁琐的,所以很多工具就应运而生了。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"Workbox","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"由于直接写原生的sw.js,比较繁琐和复杂,所以一些工具就出现了,而Workbox是其中的佼佼者,由google团队推出。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"简介","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在 Workbox 之前,GoogleChrome 团队较早时间推出过 sw-precache 和 sw-toolbox 库,但是在 GoogleChrome 工程师们看来,workbox 才是真正能方便统一的处理离线能力的更完美的方案,所以停止了对 sw-precache 和 sw-toolbox 的维护。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"使用者","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"有很多团队也是启用该工具来实现serviceworker的缓存,比如说:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"淘宝首页","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"网易新闻wap文章页","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"百度的Lavas","attrs":{}}]}]}],"attrs":{}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"基本配置","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首先,需要在项目的sw.js文件中,引入Workbox的官方js,这里用了我们自己的静态资源:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"importScripts(\n \"https://edu-cms.nosdn.127.net/topics/js/workbox_9cc4c3d662a4266fe6691d0d5d83f4dc.js\"\n);\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"其中importScripts是webworker中加载js的方式。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"引入Workbox后,全局会挂载一个Workbox对象","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"if (workbox) {\n console.log('workbox加载成功');\n} else {\n console.log('workbox加载失败');\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"然后需要在使用其他的api前,提前使用配置","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"//关闭控制台中的输出\nworkbox.setConfig({ debug: false });\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"也可以统一指定存储时Cache的名称:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"//设置缓存cachestorage的名称\nworkbox.core.setCacheNameDetails({\n prefix:'edu-cms',\n suffix:'v1'\n});\n","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"precache","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Workbox的缓存分为两种,一种的precache,一种的runtimecache。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"precache对应的是在installing阶段进行读取缓存的操作。它让开发人员可以确定缓存文件的时间和长度,以及在不进入网络的情况下将其提供给浏览器,这意味着它可以用于创建Web离线工作的应用。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"工作原理","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首次加载Web应用程序时,Workbox会下载指定的资源,并存储具体内容和相关修订的信息在indexedDB中。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"当资源内容和sw.js更新后,Workbox会去比对资源,然后将新的资源存入Cache,并修改indexedDB中的版本信息。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我们举一个例子:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"workbox.precaching.precacheAndRoute([\n './main.css'\n]);\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/a4/a4e5f0476d7f979166a4bb7e7742fae3.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"indexedDB中会保存其相关信息","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/0b/0b982875813806025799c30460f3e531.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"这个时候我们把main.css的内容改变后,再刷新页面,会发现除非强制刷新,否则Workbox还是会读取Cache中存在的老的main.css内容。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"即使我们把main.css从服务器上删除,也不会对页面造成影响。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"所以这种方式的缓存都需要配置一个版本号。在修改sw.js时,对应的版本也需要变更。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"使用实践","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"当然了,一般我们的一些不经常变的资源,都会使用cdn,所以这里自然就需要支持域外资源了,配置方式如下:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"var fileList = [\n {\n url:'https://edu-cms.nosdn.127.net/topics/js/cms_specialWebCommon_js_f26c710bd7cd055a64b67456192ed32a.js'\n },\n {\n url:'https://static.ws.126.net/163/frontend/share/css/article.207ac19ad70fd0e54d4a.css'\n }\n];\n\n\n//precache 适用于支持跨域的cdn和域内静态资源\nworkbox.precaching.suppressWarnings();\nworkbox.precaching.precacheAndRoute(fileList, {\n \"ignoreUrlParametersMatching\": [/./]\n});\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"这里需要对应的资源配置跨域允许头,否则是不能正常加载的。且文件都要以版本文件名的方式,来确保修改后Cache和indexDB会得到更新。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"理解了原理和实践后,说明这种方式适合于上线后就不会经常变动的静态资源。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"runtimecache","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"运行时缓存是在install之后,activated和fetch阶段做的事情。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"既然在fetch阶段发送,那么runtimecache 往往应对着各种类型的资源,对于不同类型的资源往往也有不同的缓存策略。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"缓存策略","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Workbox提供的缓存策划有以下几种,通过不同的配置可以针对自己的业务达到不同的效果:","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"Stale While Revalidate","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"这种策略的意思是当请求的路由有对应的Cache缓存结果就直接返回,","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在返回Cache缓存结果的同时会在后台发起网络请求拿到请求结果并更新Cache缓存,如果本来就没有Cache缓存的话,直接就发起网络请求并返回结果,这对用户来说是一种非常安全的策略,能保证用户最快速的拿到请求的结果。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"但是也有一定的缺点,就是还是会有网络请求占用了用户的网络带宽。可以像如下的方式使用State While Revalidate策略:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"workbox.routing.registerRoute(\n new RegExp('https://edu-cms\\.nosdn\\.127\\.net/topics/'),\n workbox.strategies.staleWhileRevalidate({\n //cache名称\n cacheName: 'lf-sw:static',\n plugins: [\n new workbox.expiration.Plugin({\n //cache最大数量\n maxEntries: 30\n })\n ]\n })\n);\n","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"Network First","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"这种策略就是当请求路由是被匹配的,就采用网络优先的策略,也就是优先尝试拿到网络请求的返回结果,如果拿到网络请求的结果,就将结果返回给客户端并且写入Cache缓存。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果网络请求失败,那最后被缓存的Cache缓存结果就会被返回到客户端,这种策略一般适用于返回结果不太固定或对实时性有要求的请求,为网络请求失败进行兜底。可以像如下方式使用Network First策略:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"//自定义要缓存的html列表\nvar cacheList = [\n '/Hexo/public/demo/PWADemo/workbox/index.html'\n];\nworkbox.routing.registerRoute(\n //自定义过滤方法\n function(event) {\n // 需要缓存的HTML路径列表\n if (event.url.host === 'localhost:63342') {\n if (~cacheList.indexOf(event.url.pathname)) return true;\n else return false;\n } else {\n return false;\n }\n },\n workbox.strategies.networkFirst({\n cacheName: 'lf-sw:html',\n plugins: [\n new workbox.expiration.Plugin({\n maxEntries: 10\n })\n ]\n })\n);\n","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"Cache First","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"这个策略的意思就是当匹配到请求之后直接从Cache缓存中取得结果,如果Cache缓存中没有结果,那就会发起网络请求,拿到网络请求结果并将结果更新至Cache缓存,并将结果返回给客户端。这种策略比较适合结果不怎么变动且对实时性要求不高的请求。可以像如下方式使用Cache First策略:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"workbox.routing.registerRoute(\n new RegExp('https://edu-image\\.nosdn\\.127\\.net/'),\n workbox.strategies.cacheFirst({\n cacheName: 'lf-sw:img',\n plugins: [\n //如果要拿到域外的资源,必须配置\n //因为跨域使用fetch配置了\n //mode: 'no-cors',所以status返回值为0,故而需要兼容\n new workbox.cacheableResponse.Plugin({\n statuses: [0, 200]\n }),\n new workbox.expiration.Plugin({\n maxEntries: 40,\n //缓存的时间\n maxAgeSeconds: 12 * 60 * 60\n })\n ]\n })\n);\n","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"Network Only","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"比较直接的策略,直接强制使用正常的网络请求,并将结果返回给客户端,这种策略比较适合对实时性要求非常高的请求。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"Cache Only","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"这个策略也比较直接,直接使用 Cache 缓存的结果,并将结果返回给客户端,这种策略比较适合一上线就不会变的静态资源请求。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"举个栗子","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"又到了举个栗子的阶段了,这次我们用淘宝好了,看看他们是如何通过Workbox来配置Service Worker的:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"//首先是异常处理\nself.addEventListener('error', function(e) {\n self.clients.matchAll()\n .then(function (clients) {\n if (clients && clients.length) {\n clients[0].postMessage({ \n type: 'ERROR',\n msg: e.message || null,\n stack: e.error ? e.error.stack : null\n });\n }\n });\n});\n\nself.addEventListener('unhandledrejection', function(e) {\n self.clients.matchAll()\n .then(function (clients) {\n if (clients && clients.length) {\n clients[0].postMessage({\n type: 'REJECTION',\n msg: e.reason ? e.reason.message : null,\n stack: e.reason ? e.reason.stack : null\n });\n }\n });\n})\n//然后引入workbox\nimportScripts('https://g.alicdn.com/kg/workbox/3.3.0/workbox-sw.js');\nworkbox.setConfig({\n debug: false,\n modulePathPrefix: 'https://g.alicdn.com/kg/workbox/3.3.0/'\n});\n//直接激活跳过等待阶段\nworkbox.skipWaiting();\nworkbox.clientsClaim();\n//定义要缓存的html\nvar cacheList = [\n '/',\n '/tbhome/home-2017',\n '/tbhome/page/market-list'\n];\n//html采用networkFirst策略,支持离线也能大体访问\nworkbox.routing.registerRoute(\n function(event) {\n // 需要缓存的HTML路径列表\n if (event.url.host === 'www.taobao.com') {\n if (~cacheList.indexOf(event.url.pathname)) return true;\n else return false;\n } else {\n return false;\n }\n },\n workbox.strategies.networkFirst({\n cacheName: 'tbh:html',\n plugins: [\n new workbox.expiration.Plugin({\n maxEntries: 10\n })\n ]\n })\n);\n//静态资源采用staleWhileRevalidate策略,安全可靠\nworkbox.routing.registerRoute(\n new RegExp('https://g\\.alicdn\\.com/'),\n workbox.strategies.staleWhileRevalidate({\n cacheName: 'tbh:static',\n plugins: [\n new workbox.expiration.Plugin({\n maxEntries: 20\n })\n ]\n })\n);\n//图片采用cacheFirst策略,提升速度\nworkbox.routing.registerRoute(\n new RegExp('https://img\\.alicdn\\.com/'),\n workbox.strategies.cacheFirst({\n cacheName: 'tbh:img',\n plugins: [\n new workbox.cacheableResponse.Plugin({\n statuses: [0, 200]\n }),\n new workbox.expiration.Plugin({\n maxEntries: 20,\n maxAgeSeconds: 12 * 60 * 60\n })\n ]\n })\n);\n\nworkbox.routing.registerRoute(\n new RegExp('https://gtms01\\.alicdn\\.com/'),\n workbox.strategies.cacheFirst({\n cacheName: 'tbh:img',\n plugins: [\n new workbox.cacheableResponse.Plugin({\n statuses: [0, 200]\n }),\n new workbox.expiration.Plugin({\n maxEntries: 30,\n maxAgeSeconds: 12 * 60 * 60\n })\n ]\n })\n);\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"可以看出,使用Workbox比起直接手撸来,要快很多,也明确很多。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"原理","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"目前分析Service Worker和Workbox的文章不少,但是介绍Workbox原理的文章却不多。这里简单介绍下Workbox这个工具库的原理。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首先将几个我们产品用到的模块图奉上:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/78/78e899344897c901e71b76ecdfae65de.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"简单提几个Workbox源码的亮点。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"通过Proxy按需依赖","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"熟悉了Workbox后会得知,它是有很多个子模块的,各个子模块再通过用到的时候按需importScript到线程中。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/69/694f39ae0f7930568210d1031cb0ef21.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"做到按需依赖的原理就是通过Proxy对全局对象Workbox进行代理:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"new Proxy(this, {\n get(t, s) {\n //如果workbox对象上不存在指定对象,就依赖注入该对象对应的脚本\n if (t[s]) return t[s];\n const o = e[s];\n return o && t.loadModule(`workbox-${o}`), t[s];\n }\n})\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果找不到对应模块,则通过importScripts主动加载:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"/**\n * 加载前端模块\n * @param {Strnig} t \n */\nloadModule(t) {\n const e = this.o(t);\n try {\n importScripts(e), (this.s = !0);\n } catch (s) {\n throw (console.error(`Unable to import module '${t}' from '${e}'.`), s);\n }\n}\n","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"通过freeze冻结对外暴露api","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Workbox.core模块中提供了几个核心操作模块,如封装了indexedDB操作的DBWrapper、对Cache Storage进行读取的Cache Wrapper,以及发送请求的fetchWrapper和日志管理的logger等等。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"为了防止外部对内部模块暴露出去的api进行修改,导致出现不可预估的错误,内部模块可以通过Object.freeze将api进行冻结保护:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"var _private = /*#__PURE__*/Object.freeze({\n DBWrapper: DBWrapper,\n WorkboxError: WorkboxError,\n assert: finalAssertExports,\n cacheNames: cacheNames,\n cacheWrapper: cacheWrapper,\n fetchWrapper: fetchWrapper,\n getFriendlyURL: getFriendlyURL,\n logger: defaultExport\n });\n","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"总结","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通过对Service Worker的理解和Workbox的应用,可以进一步提升产品的性能和弱网情况下的体验。有兴趣的同学也可以对Workbox的源码细细评读,其中还有很多不错的设计模式和编程风格值得学习。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"-END-","attrs":{}}]},{"type":"horizontalrule","attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/e5/e50bded4db99e861b8c12cfa6f94557b.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/a1/a10c983bbfa32a8c90cb64f9d847c8ae.png","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章