本文首先會簡單介紹下前端的常見緩存方式,再引入Service Worker的概念,針對其原理和如何運用進行介紹。而後基於google推出的第三方庫Workbox,在產品中進行運用實踐,並對其原理進行簡要剖析。css
做者:劉放html
編輯:Ein前端
先簡單介紹一下現有的前端緩存技術方案,主要分爲http緩存和瀏覽器緩存。git
http緩存都是第二次請求時開始的,這也是個老生常談的話題了。無非也是那幾個http頭的問題:github
HTTP1.0的內容,服務器使用Expires頭來告訴Web客戶端它可使用當前副本,直到指定的時間爲止。web
HTTP1.1引入了Cathe-Control,它使用max-age指定資源被緩存多久,主要是解決了Expires一個重大的缺陷,就是它設置的是一個固定的時間點,客戶端時間和服務端時間可能有偏差。
因此通常會把兩個頭都帶上,這種緩存稱爲強緩存,表現形式爲:
算法
Last-Modified是服務器告訴瀏覽器該資源的最後修改時間,If-Modified-Since是請求頭帶上的,上次服務器給本身的該資源的最後修改時間。而後服務器拿去對比。chrome
若資源的最後修改時間大於If-Modified-Since,說明資源又被改動過,則響應整片資源內容,返回狀態碼200;數據庫
若資源的最後修改時間小於或等於If-Modified-Since,說明資源無新修改,則響應HTTP 304,告知瀏覽器繼續使用當前版本。編程
前面提到由文件的修改時間來判斷文件是否改動,仍是會帶來必定的偏差,好比註釋等可有可無的修改等。因此推出了新的方式。
Etag是由服務端特定算法生成的該文件的惟一標識,而請求頭把返回的Etag值經過If-None-Match再帶給服務端,服務端經過比對從而決定是否響應新內容。這也是304緩存。
簡單的緩存方式有cookie,localStorage和sessionStorage。這裏就不詳細介紹他們的區別了,這裏說下經過localStorage來緩存靜態資源的優化方案。
localStorage一般有5MB的存儲空間,咱們以微信文章頁爲例。
查看請求發現,基本沒有js和css的請求,由於它把所有的不須要改動的資源都放到了localStorage中:
因此微信的文章頁加載很是的快。
前端數據庫有WebSql和IndexDB,其中WebSql被規範廢棄,他們都有大約50MB的最大容量,能夠理解爲localStorage的增強版。
應用緩存主要是經過manifest文件來註冊被緩存的靜態資源,已經被廢棄,由於他的設計有些不合理的地方,他在緩存靜態文件的同時,也會默認緩存html文件。這致使頁面的更新只能經過manifest文件中的版本號來決定。因此,應用緩存只適合那種常年不變化的靜態網站。如此的不方便,也是被廢棄的重要緣由。
PWA也運用了該文件,不一樣於manifest簡單的將文件經過是否緩存進行分類,PWA用manifest構建了本身的APP骨架,並運用Servie Worker來控制緩存,這也是今天的主角。
Service Worker本質上也是瀏覽器緩存資源用的,只不過他不只僅是Cache,也是經過worker的方式來進一步優化。
他基於h5的web worker,因此絕對不會阻礙當前js線程的執行,sw最重要的工做原理就是:
一、後臺線程:獨立於當前網頁線程;
二、網絡代理:在網頁發起請求時代理,來緩存文件。
能夠看到,基本上新版瀏覽器仍是兼容滴。以前是隻有chrome和firefox支持,如今微軟和蘋果也相繼支持了。
判斷一個技術是否值得嘗試,確定要考慮下它的成熟程度,不然過一段時間又和應用緩存同樣被規範拋棄就尷尬了。
因此這裏我列舉了幾個使用Service Worker的頁面:
因此說仍是能夠嘗試下的。
一個網站是否啓用Service Worker,能夠經過開發者工具中的Application來查看:
被Service Worker緩存的文件,能夠在Network中看到Size項爲from Service Worker:
也能夠在Application的Cache Storage中查看緩存的具體內容:
若是是具體的斷點調試,須要使用對應的線程,再也不是main線程了,這也是webworker的通用調試方法:
sw 是基於 HTTPS 的,由於Service Worker中涉及到請求攔截,因此必須使用HTTPS協議來保障安全。若是是本地調試的話,localhost是能夠的。
而咱們恰好全站強制https化,因此正好可使用。
大概能夠用以下圖片來解釋:
要使用Service Worker,首先須要註冊一個sw,通知瀏覽器爲該頁面分配一塊內存,而後sw就會進入安裝階段。
一個簡單的註冊方式:
(function() { if('serviceWorker' in navigator) { navigator.serviceWorker.register('./sw.js'); } })()
固然也能夠考慮全面點,參考網易新聞的註冊方式:
"serviceWorker" in navigator && window.addEventListener("load", function() { var e = location.pathname.match(/\/news\/[a-z]{1,}\//)[0] + "article-sw.js?v=08494f887a520e6455fa"; navigator.serviceWorker.register(e).then(function(n) { n.onupdatefound = function() { var e = n.installing; e.onstatechange = function() { switch (e.state) { case "installed": navigator.serviceWorker.controller ? console.log("New or updated content is available.") : console.log("Content is now available offline!"); break; case "redundant": console.error("The installing service worker became redundant.") } } } }). catch(function(e) { console.error("Error during service worker registration:", e) }) })
前面提到過,因爲sw會監聽和代理全部的請求,因此sw的做用域就顯得額外的重要了,好比說咱們只想監聽咱們專題頁的全部請求,就在註冊時指定路徑:
navigator.serviceWorker.register('/topics/sw.js');
這樣就只會對topics/下面的路徑進行優化。
咱們註冊後,瀏覽器就會開始安裝sw,能夠經過事件監聽:
//service worker安裝成功後開始緩存所需的資源 var CACHE_PREFIX = 'cms-sw-cache'; var CACHE_VERSION = '0.0.20'; var CACHE_NAME = CACHE_PREFIX+'-'+CACHE_VERSION; var allAssets = [ './main.css' ]; self.addEventListener('install', function(event) { //調試時跳過等待過程 self.skipWaiting(); // Perform install steps //首先 event.waitUntil 你能夠理解爲 new Promise, //它接受的實際參數只能是一個 promise,由於,caches 和 cache.addAll 返回的都是 Promise, //這裏就是一個串行的異步加載,當全部加載都成功時,那麼 SW 就能夠下一步。 //另外,event.waitUntil 還有另一個重要好處,它能夠用來延長一個事件做用的時間, //這裏特別針對於咱們 SW 來講,好比咱們使用 caches.open 是用來打開指定的緩存,但開啓的時候, //並非一下就能調用成功,也有可能有必定延遲,因爲系統會隨時睡眠 SW,因此,爲了防止執行中斷, //就須要使用 event.waitUntil 進行捕獲。另外,event.waitUntil 會監聽全部的異步 promise //若是其中一個 promise 是 reject 狀態,那麼該次 event 是失敗的。這就致使,咱們的 SW 開啓失敗。 event.waitUntil( caches.open(CACHE_NAME) .then(function(cache) { console.log('[SW]: Opened cache'); return cache.addAll(allAssets); }) ); });
安裝時,sw就開始緩存文件了,會檢查全部文件的緩存狀態,若是都已經緩存了,則安裝成功,進入下一階段。
若是是第一次加載sw,在安裝後,會直接進入activated階段,而若是sw進行更新,狀況就會顯得複雜一些。流程以下:
首先老的sw爲A,新的sw版本爲B。
B進入install階段,而A還處於工做狀態,因此B進入waiting階段。只有等到A被terminated後,B才能正常替換A的工做。
這個terminated的時機有以下幾種方式:
一、關閉瀏覽器一段時間;
二、手動清除Service Worker;
三、在sw安裝時直接跳過waiting階段
//service worker安裝成功後開始緩存所需的資源 self.addEventListener('install', function(event) { //跳過等待過程 self.skipWaiting(); });
而後就進入了activated階段,激活sw工做。
activated階段能夠作不少有意義的事情,好比更新存儲在Cache中的key和value:
var CACHE_PREFIX = 'cms-sw-cache'; var CACHE_VERSION = '0.0.20'; /** * 找出對應的其餘key並進行刪除操做 * @returns {*} */ function deleteOldCaches() { return caches.keys().then(function (keys) { var all = keys.map(function (key) { if (key.indexOf(CACHE_PREFIX) !== -1 && key.indexOf(CACHE_VERSION) === -1){ console.log('[SW]: Delete cache:' + key); return caches.delete(key); } }); return Promise.all(all); }); } //sw激活階段,說明上一sw已失效 self.addEventListener('activate', function(event) { event.waitUntil( // 遍歷 caches 裏全部緩存的 keys 值 caches.keys().then(deleteOldCaches) ); });
這個空閒狀態通常是不可見的,這種通常說明sw的事情都處理完畢了,而後處於閒置狀態了。
瀏覽器會週期性的輪詢,去釋放處於idle的sw佔用的資源。
該階段是sw最爲關鍵的一個階段,用於攔截代理全部指定的請求,並進行對應的操做。
全部的緩存部分,都是在該階段,這裏舉一個簡單的例子:
//監聽瀏覽器的全部fetch請求,對已經緩存的資源使用本地緩存回覆 self.addEventListener('fetch', function(event) { event.respondWith( caches.match(event.request) .then(function(response) { //該fetch請求已經緩存 if (response) { return response; } return fetch(event.request); } ) ); });
生命週期大概講清楚了,咱們就以一個具體的例子來講明下原生的serviceworker是如何在生產環境中使用的吧。
咱們能夠以網易新聞的wap頁爲例,其針對不怎麼變化的靜態資源開啓了sw緩存,具體的sw.js邏輯和解讀以下:
'use strict'; //須要緩存的資源列表 var precacheConfig = [ ["https://static.ws.126.net/163/wap/f2e/milk_index/bg_img_sm_minfy.png", "c4f55f5a9784ed2093009dadf1e954f9"], ["https://static.ws.126.net/163/wap/f2e/milk_index/change.png", "9af1b102ef784b8ff08567ba25f31d95"], ["https://static.ws.126.net/163/wap/f2e/milk_index/icon-download.png", "1c02c724381d77a1a19ca18925e9b30c"], ["https://static.ws.126.net/163/wap/f2e/milk_index/icon-login-dark.png", "b59ba5abe97ff29855dfa4bd3a7a9f35"], ["https://static.ws.126.net/163/wap/f2e/milk_index/icon-refresh.png", "a5b1084e41939885969a13f8dbc88abd"], ["https://static.ws.126.net/163/wap/f2e/milk_index/icon-video-play.png", "065ff496d7d36345196d254aff027240"], ["https://static.ws.126.net/163/wap/f2e/milk_index/icon.ico", "a14e5365cc2b27ec57e1ab7866c6a228"], ["https://static.ws.126.net/163/wap/f2e/milk_index/iconfont_1.eot", "e4d2788fef09eb0630d66cc7e6b1ab79"], ["https://static.ws.126.net/163/wap/f2e/milk_index/iconfont_1.svg", "d9e57c341608fddd7c140570167bdabb"], ["https://static.ws.126.net/163/wap/f2e/milk_index/iconfont_1.ttf", "f422407038a3180bb3ce941a4a52bfa2"], ["https://static.ws.126.net/163/wap/f2e/milk_index/iconfont_1.woff", "ead2bef59378b00425779c4ca558d9bd"], ["https://static.ws.126.net/163/wap/f2e/milk_index/index.5cdf03e8.js", "6262ac947d12a7b0baf32be79e273083"], ["https://static.ws.126.net/163/wap/f2e/milk_index/index.bc729f8a.css", "58e54a2c735f72a24715af7dab757739"], ["https://static.ws.126.net/163/wap/f2e/milk_index/logo-app-bohe.png", "ac5116d8f5fcb3e7c49e962c54ff9766"], ["https://static.ws.126.net/163/wap/f2e/milk_index/logo-app-mail.png", "a12bbfaeee7fbf025d5ee85634fca1eb"], ["https://static.ws.126.net/163/wap/f2e/milk_index/logo-app-manhua.png", "b8905b119cf19a43caa2d8a0120bdd06"], ["https://static.ws.126.net/163/wap/f2e/milk_index/logo-app-open.png", "b7cc76ba7874b2132f407049d3e4e6e6"], ["https://static.ws.126.net/163/wap/f2e/milk_index/logo-app-read.png", "e6e9c8bc72f857960822df13141cbbfd"], ["https://static.ws.126.net/163/wap/f2e/milk_index/logo-site.png", "2b0d728b46518870a7e2fe424e9c0085"], ["https://static.ws.126.net/163/wap/f2e/milk_index/version_no_pic.png", "aef80885188e9d763282735e53b25c0e"], ["https://static.ws.126.net/163/wap/f2e/milk_index/version_pc.png", "42f3cc914eab7be4258fac3a4889d41d"], ["https://static.ws.126.net/163/wap/f2e/milk_index/version_standard.png", "573408fa002e58c347041e9f41a5cd0d"] ]; var cacheName = 'sw-precache-v3-new-wap-index-' + (self.registration ? self.registration.scope : ''); var ignoreUrlParametersMatching = [/^utm_/]; var addDirectoryIndex = function(originalUrl, index) { var url = new URL(originalUrl); if (url.pathname.slice(-1) === '/') { url.pathname += index; } return url.toString(); }; var cleanResponse = function(originalResponse) { // If this is not a redirected response, then we don't have to do anything. if (!originalResponse.redirected) { return Promise.resolve(originalResponse); } // Firefox 50 and below doesn't support the Response.body stream, so we may // need to read the entire body to memory as a Blob. var bodyPromise = 'body' in originalResponse ? Promise.resolve(originalResponse.body) : originalResponse.blob(); return bodyPromise.then(function(body) { // new Response() is happy when passed either a stream or a Blob. return new Response(body, { headers: originalResponse.headers, status: originalResponse.status, statusText: originalResponse.statusText }); }); }; var createCacheKey = function(originalUrl, paramName, paramValue, dontCacheBustUrlsMatching) { // Create a new URL object to avoid modifying originalUrl. var url = new URL(originalUrl); // If dontCacheBustUrlsMatching is not set, or if we don't have a match, // then add in the extra cache-busting URL parameter. if (!dontCacheBustUrlsMatching || !(url.pathname.match(dontCacheBustUrlsMatching))) { url.search += (url.search ? '&' : '') + encodeURIComponent(paramName) + '=' + encodeURIComponent(paramValue); } return url.toString(); }; var isPathWhitelisted = function(whitelist, absoluteUrlString) { // If the whitelist is empty, then consider all URLs to be whitelisted. if (whitelist.length === 0) { return true; } // Otherwise compare each path regex to the path of the URL passed in. var path = (new URL(absoluteUrlString)).pathname; return whitelist.some(function(whitelistedPathRegex) { return path.match(whitelistedPathRegex); }); }; var stripIgnoredUrlParameters = function(originalUrl, ignoreUrlParametersMatching) { var url = new URL(originalUrl); // Remove the hash; see https://github.com/GoogleChrome/sw-precache/issues/290 url.hash = ''; url.search = url.search.slice(1) // Exclude initial '?' .split('&') // Split into an array of 'key=value' strings .map(function(kv) { return kv.split('='); // Split each 'key=value' string into a [key, value] array }) .filter(function(kv) { return ignoreUrlParametersMatching.every(function(ignoredRegex) { return !ignoredRegex.test(kv[0]); // Return true iff the key doesn't match any of the regexes. }); }) .map(function(kv) { return kv.join('='); // Join each [key, value] array into a 'key=value' string }) .join('&'); // Join the array of 'key=value' strings into a string with '&' in between each return url.toString(); }; var hashParamName = '_sw-precache'; //定義須要緩存的url列表 var urlsToCacheKeys = new Map( precacheConfig.map(function(item) { var relativeUrl = item[0]; var hash = item[1]; var absoluteUrl = new URL(relativeUrl, self.location); var cacheKey = createCacheKey(absoluteUrl, hashParamName, hash, false); return [absoluteUrl.toString(), cacheKey]; }) ); //把cache中的url提取出來,進行去重操做 function setOfCachedUrls(cache) { return cache.keys().then(function(requests) { //提取url return requests.map(function(request) { return request.url; }); }).then(function(urls) { //去重 return new Set(urls); }); } //sw安裝階段 self.addEventListener('install', function(event) { event.waitUntil( //首先嚐試取出存在客戶端cache中的數據 caches.open(cacheName).then(function(cache) { return setOfCachedUrls(cache).then(function(cachedUrls) { return Promise.all( Array.from(urlsToCacheKeys.values()).map(function(cacheKey) { //若是須要緩存的url不在當前cache中,則添加到cache if (!cachedUrls.has(cacheKey)) { //設置same-origin是爲了兼容舊版本safari中其默認值不爲same-origin, //只有當URL與響應腳本同源才發送 cookies、 HTTP Basic authentication 等驗證信息 var request = new Request(cacheKey, {credentials: 'same-origin'}); return fetch(request).then(function(response) { //經過fetch api請求資源 if (!response.ok) { throw new Error('Request for ' + cacheKey + ' returned a ' + 'response with status ' + response.status); } return cleanResponse(response).then(function(responseToCache) { //並設置到當前cache中 return cache.put(cacheKey, responseToCache); }); }); } }) ); }); }).then(function() { //強制跳過等待階段,進入激活階段 return self.skipWaiting(); }) ); }); self.addEventListener('activate', function(event) { //清除cache中原來老的一批相同key的數據 var setOfExpectedUrls = new Set(urlsToCacheKeys.values()); event.waitUntil( caches.open(cacheName).then(function(cache) { return cache.keys().then(function(existingRequests) { return Promise.all( existingRequests.map(function(existingRequest) { if (!setOfExpectedUrls.has(existingRequest.url)) { //cache中刪除指定對象 return cache.delete(existingRequest); } }) ); }); }).then(function() { //self至關於webworker線程的當前做用域 //當一個 service worker 被初始註冊時,頁面在下次加載以前不會使用它。 claim() 方法會當即控制這些頁面 //從而更新客戶端上的serviceworker return self.clients.claim(); }) ); }); self.addEventListener('fetch', function(event) { if (event.request.method === 'GET') { // 標識位,用來判斷是否須要緩存 var shouldRespond; // 對url進行一些處理,移除一些沒必要要的參數 var url = stripIgnoredUrlParameters(event.request.url, ignoreUrlParametersMatching); // 若是該url不是咱們想要緩存的url,置爲false shouldRespond = urlsToCacheKeys.has(url); // 若是shouldRespond未false,再次驗證 var directoryIndex = 'index.html'; if (!shouldRespond && directoryIndex) { url = addDirectoryIndex(url, directoryIndex); shouldRespond = urlsToCacheKeys.has(url); } // 再次驗證,判斷其是不是一個navigation類型的請求 var navigateFallback = ''; if (!shouldRespond && navigateFallback && (event.request.mode === 'navigate') && isPathWhitelisted([], event.request.url)) { url = new URL(navigateFallback, self.location).toString(); shouldRespond = urlsToCacheKeys.has(url); } // 若是標識位爲true if (shouldRespond) { event.respondWith( caches.open(cacheName).then(function(cache) { //去緩存cache中找對應的url的值 return cache.match(urlsToCacheKeys.get(url)).then(function(response) { //若是找到了,就返回value if (response) { return response; } throw Error('The cached response that was expected is missing.'); }); }).catch(function(e) { // 若是沒找到則請求該資源 console.warn('Couldn\'t serve response for "%s" from cache: %O', event.request.url, e); return fetch(event.request); }) ); } } });
這裏的策略大概就是優先在Cache中尋找資源,若是找不到再請求資源。能夠看出,爲了實現一個較爲簡單的緩存,仍是比較複雜和繁瑣的,因此不少工具就應運而生了。
因爲直接寫原生的sw.js,比較繁瑣和複雜,因此一些工具就出現了,而Workbox是其中的佼佼者,由google團隊推出。
在 Workbox 以前,GoogleChrome 團隊較早時間推出過 sw-precache 和 sw-toolbox 庫,可是在 GoogleChrome 工程師們看來,workbox 纔是真正能方便統一的處理離線能力的更完美的方案,因此中止了對 sw-precache 和 sw-toolbox 的維護。
有不少團隊也是啓用該工具來實現serviceworker的緩存,好比說:
首先,須要在項目的sw.js文件中,引入Workbox的官方js,這裏用了咱們本身的靜態資源:
importScripts( "https://edu-cms.nosdn.127.net/topics/js/workbox_9cc4c3d662a4266fe6691d0d5d83f4dc.js" );
其中importScripts是webworker中加載js的方式。
引入Workbox後,全局會掛載一個Workbox對象
if (workbox) { console.log('workbox加載成功'); } else { console.log('workbox加載失敗'); }
而後須要在使用其餘的api前,提早使用配置
//關閉控制檯中的輸出 workbox.setConfig({ debug: false });
也能夠統一指定存儲時Cache的名稱:
//設置緩存cachestorage的名稱 workbox.core.setCacheNameDetails({ prefix:'edu-cms', suffix:'v1' });
Workbox的緩存分爲兩種,一種的precache,一種的runtimecache。
precache對應的是在installing階段進行讀取緩存的操做。它讓開發人員能夠肯定緩存文件的時間和長度,以及在不進入網絡的狀況下將其提供給瀏覽器,這意味着它能夠用於建立Web離線工做的應用。
首次加載Web應用程序時,Workbox會下載指定的資源,並存儲具體內容和相關修訂的信息在indexedDB中。
當資源內容和sw.js更新後,Workbox會去比對資源,而後將新的資源存入Cache,並修改indexedDB中的版本信息。
咱們舉一個例子:
workbox.precaching.precacheAndRoute([ './main.css' ]);
indexedDB中會保存其相關信息
這個時候咱們把main.css的內容改變後,再刷新頁面,會發現除非強制刷新,不然Workbox仍是會讀取Cache中存在的老的main.css內容。
即便咱們把main.css從服務器上刪除,也不會對頁面形成影響。
因此這種方式的緩存都須要配置一個版本號。在修改sw.js時,對應的版本也須要變動。
固然了,通常咱們的一些不常常變的資源,都會使用cdn,因此這裏天然就須要支持域外資源了,配置方式以下:
var fileList = [ { url:'https://edu-cms.nosdn.127.net/topics/js/cms_specialWebCommon_js_f26c710bd7cd055a64b67456192ed32a.js' }, { url:'https://static.ws.126.net/163/frontend/share/css/article.207ac19ad70fd0e54d4a.css' } ]; //precache 適用於支持跨域的cdn和域內靜態資源 workbox.precaching.suppressWarnings(); workbox.precaching.precacheAndRoute(fileList, { "ignoreUrlParametersMatching": [/./] });
這裏須要對應的資源配置跨域容許頭,不然是不能正常加載的。且文件都要以版本文件名的方式,來確保修改後Cache和indexDB會獲得更新。
理解了原理和實踐後,說明這種方式適合於上線後就不會常常變更的靜態資源。
運行時緩存是在install以後,activated和fetch階段作的事情。
既然在fetch階段發送,那麼runtimecache 每每應對着各類類型的資源,對於不一樣類型的資源每每也有不一樣的緩存策略。
Workbox提供的緩存策劃有如下幾種,經過不一樣的配置能夠針對本身的業務達到不一樣的效果:
這種策略的意思是當請求的路由有對應的Cache緩存結果就直接返回,
在返回Cache緩存結果的同時會在後臺發起網絡請求拿到請求結果並更新Cache緩存,若是原本就沒有Cache緩存的話,直接就發起網絡請求並返回結果,這對用戶來講是一種很是安全的策略,能保證用戶最快速的拿到請求的結果。
可是也有必定的缺點,就是仍是會有網絡請求佔用了用戶的網絡帶寬。能夠像以下的方式使用State While Revalidate策略:
workbox.routing.registerRoute( new RegExp('https://edu-cms\.nosdn\.127\.net/topics/'), workbox.strategies.staleWhileRevalidate({ //cache名稱 cacheName: 'lf-sw:static', plugins: [ new workbox.expiration.Plugin({ //cache最大數量 maxEntries: 30 }) ] }) );
這種策略就是當請求路由是被匹配的,就採用網絡優先的策略,也就是優先嚐試拿到網絡請求的返回結果,若是拿到網絡請求的結果,就將結果返回給客戶端而且寫入Cache緩存。
若是網絡請求失敗,那最後被緩存的Cache緩存結果就會被返回到客戶端,這種策略通常適用於返回結果不太固定或對實時性有要求的請求,爲網絡請求失敗進行兜底。能夠像以下方式使用Network First策略:
//自定義要緩存的html列表 var cacheList = [ '/Hexo/public/demo/PWADemo/workbox/index.html' ]; workbox.routing.registerRoute( //自定義過濾方法 function(event) { // 須要緩存的HTML路徑列表 if (event.url.host === 'localhost:63342') { if (~cacheList.indexOf(event.url.pathname)) return true; else return false; } else { return false; } }, workbox.strategies.networkFirst({ cacheName: 'lf-sw:html', plugins: [ new workbox.expiration.Plugin({ maxEntries: 10 }) ] }) );
這個策略的意思就是當匹配到請求以後直接從Cache緩存中取得結果,若是Cache緩存中沒有結果,那就會發起網絡請求,拿到網絡請求結果並將結果更新至Cache緩存,並將結果返回給客戶端。這種策略比較適合結果不怎麼變更且對實時性要求不高的請求。能夠像以下方式使用Cache First策略:
workbox.routing.registerRoute( new RegExp('https://edu-image\.nosdn\.127\.net/'), workbox.strategies.cacheFirst({ cacheName: 'lf-sw:img', plugins: [ //若是要拿到域外的資源,必須配置 //由於跨域使用fetch配置了 //mode: 'no-cors',因此status返回值爲0,故而須要兼容 new workbox.cacheableResponse.Plugin({ statuses: [0, 200] }), new workbox.expiration.Plugin({ maxEntries: 40, //緩存的時間 maxAgeSeconds: 12 * 60 * 60 }) ] }) );
比較直接的策略,直接強制使用正常的網絡請求,並將結果返回給客戶端,這種策略比較適合對實時性要求很是高的請求。
這個策略也比較直接,直接使用 Cache 緩存的結果,並將結果返回給客戶端,這種策略比較適合一上線就不會變的靜態資源請求。
又到了舉個栗子的階段了,此次咱們用淘寶好了,看看他們是如何經過Workbox來配置Service Worker的:
//首先是異常處理 self.addEventListener('error', function(e) { self.clients.matchAll() .then(function (clients) { if (clients && clients.length) { clients[0].postMessage({ type: 'ERROR', msg: e.message || null, stack: e.error ? e.error.stack : null }); } }); }); self.addEventListener('unhandledrejection', function(e) { self.clients.matchAll() .then(function (clients) { if (clients && clients.length) { clients[0].postMessage({ type: 'REJECTION', msg: e.reason ? e.reason.message : null, stack: e.reason ? e.reason.stack : null }); } }); }) //而後引入workbox importScripts('https://g.alicdn.com/kg/workbox/3.3.0/workbox-sw.js'); workbox.setConfig({ debug: false, modulePathPrefix: 'https://g.alicdn.com/kg/workbox/3.3.0/' }); //直接激活跳過等待階段 workbox.skipWaiting(); workbox.clientsClaim(); //定義要緩存的html var cacheList = [ '/', '/tbhome/home-2017', '/tbhome/page/market-list' ]; //html採用networkFirst策略,支持離線也能大致訪問 workbox.routing.registerRoute( function(event) { // 須要緩存的HTML路徑列表 if (event.url.host === 'www.taobao.com') { if (~cacheList.indexOf(event.url.pathname)) return true; else return false; } else { return false; } }, workbox.strategies.networkFirst({ cacheName: 'tbh:html', plugins: [ new workbox.expiration.Plugin({ maxEntries: 10 }) ] }) ); //靜態資源採用staleWhileRevalidate策略,安全可靠 workbox.routing.registerRoute( new RegExp('https://g\.alicdn\.com/'), workbox.strategies.staleWhileRevalidate({ cacheName: 'tbh:static', plugins: [ new workbox.expiration.Plugin({ maxEntries: 20 }) ] }) ); //圖片採用cacheFirst策略,提高速度 workbox.routing.registerRoute( new RegExp('https://img\.alicdn\.com/'), workbox.strategies.cacheFirst({ cacheName: 'tbh:img', plugins: [ new workbox.cacheableResponse.Plugin({ statuses: [0, 200] }), new workbox.expiration.Plugin({ maxEntries: 20, maxAgeSeconds: 12 * 60 * 60 }) ] }) ); workbox.routing.registerRoute( new RegExp('https://gtms01\.alicdn\.com/'), workbox.strategies.cacheFirst({ cacheName: 'tbh:img', plugins: [ new workbox.cacheableResponse.Plugin({ statuses: [0, 200] }), new workbox.expiration.Plugin({ maxEntries: 30, maxAgeSeconds: 12 * 60 * 60 }) ] }) );
能夠看出,使用Workbox比起直接手擼來,要快不少,也明確不少。
目前分析Service Worker和Workbox的文章很多,可是介紹Workbox原理的文章卻很少。這裏簡單介紹下Workbox這個工具庫的原理。
首先將幾個咱們產品用到的模塊圖奉上:
簡單提幾個Workbox源碼的亮點。
熟悉了Workbox後會得知,它是有不少個子模塊的,各個子模塊再經過用到的時候按需importScript到線程中。
作到按需依賴的原理就是經過Proxy對全局對象Workbox進行代理:
new Proxy(this, { get(t, s) { //若是workbox對象上不存在指定對象,就依賴注入該對象對應的腳本 if (t[s]) return t[s]; const o = e[s]; return o && t.loadModule(`workbox-${o}`), t[s]; } })
若是找不到對應模塊,則經過importScripts主動加載:
/** * 加載前端模塊 * @param {Strnig} t */ loadModule(t) { const e = this.o(t); try { importScripts(e), (this.s = !0); } catch (s) { throw (console.error(`Unable to import module '${t}' from '${e}'.`), s); } }
Workbox.core模塊中提供了幾個核心操做模塊,如封裝了indexedDB操做的DBWrapper、對Cache Storage進行讀取的Cache Wrapper,以及發送請求的fetchWrapper和日誌管理的logger等等。
爲了防止外部對內部模塊暴露出去的api進行修改,致使出現不可預估的錯誤,內部模塊能夠經過Object.freeze將api進行凍結保護:
var _private = /*#__PURE__*/Object.freeze({ DBWrapper: DBWrapper, WorkboxError: WorkboxError, assert: finalAssertExports, cacheNames: cacheNames, cacheWrapper: cacheWrapper, fetchWrapper: fetchWrapper, getFriendlyURL: getFriendlyURL, logger: defaultExport });
經過對Service Worker的理解和Workbox的應用,能夠進一步提高產品的性能和弱網狀況下的體驗。有興趣的同窗也能夠對Workbox的源碼細細評讀,其中還有不少不錯的設計模式和編程風格值得學習。
-END-