網易雲課堂 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}}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章