service worker

https://developers.google.com/web/fundamentals/primers/service-workers/?hl=encss

  它提供了豐富的離線體驗,按期的後臺同步以及消息推送等技術,這些技術通常依賴於原生應用,但如今能夠在web應用上使用了。html

什麼是service worker(後續簡稱SW)

  是瀏覽器後臺運行的一個腳本,它爲咱們提供了許多新特性。這些特性已經包括了消息推送後臺同步,將來將會支持按期同步或者地理位置的獲取。除了sw,AppCache也支持離線功能。但sw更完善,能從設計上避免AppCache的一些問題。c++

  • 它是一個web worker,因此不能直接操做dom。而是經過postMessage把消息傳遞給父頁面,而後由父頁面來操做dom
  • 它是一個可編程的網絡代理,容許咱們去操做頁面的網絡請求
  • 當不須要的時候能夠終止,須要的時候能夠從新啓動。因此最好不要在onmessage中依賴全局數據,若是重啓前須要保存數據,重啓後獲取這些數據,能夠在worker內訪問indexedDB。
  • 它會大量使用promise

生命週期

  sw的生命週期是最複雜的一部分,只有把它搞清楚了,才能作到無縫地、無衝突地發佈更新sw。它與頁面的生命週期(onload等)徹底分離。git

  首先須要頁面腳本中註冊sw,瀏覽器就會後檯安裝sw了。安裝完了而且激活完成,頁面刷新後,sw就能夠控制整個頁面了。有以下兩種狀況:github

  1. sw可能會由於內存不足而被終止
  2. 正常運行

如下sw第一次安裝的簡單的生命週期圖示:web

生命週期的做用

  1. 在生命週期函數中,下載好須要離線訪問的資源
  2. 容許一個新的sw作好準備,同時運行着舊的sw,頁面刷新後,新的sw會替換舊的sw
  3. 保證同域下的多個頁面能夠由對應域的sw來管理,如/sw.js 能夠管理/a.html和/b.html(域都是根目錄)。
  4. 保證只有一個版本的sw在運行

  最後一點尤其重要,沒有sw,用戶能夠這時打開一個標籤,過一會又打開一個新的標籤來訪問咱們的站點,這時兩個標籤中的站點的版本可能已經不一致了。有時候這是沒問題的,但到了須要同步兩個頁面內的設置時,能夠用共享的存儲來解決,但這可能出錯或者數據丟失。chrome

第一個sw

  1. install事件是sw的第一個事件,並且只觸發一次。
  2. 傳遞給installEvent.waitUntil函數的promise標誌着install階段的持續時間以及install是成功仍是失敗。也就是說這個promise表明了install階段
  3. 僅當install結束並且active以後,sw纔會接收fetch和push事件。
  4. 默認的,sw不會接收fetch事件直到頁面經過sw來請求本身。也就是必需要頁面從新打開(而不是刷新)後,sw纔會起做用
  5. clients.claim函數能夠改變以上的默認行爲(使頁面第一次打開,sw第一次啓動,不須要重啓頁面就能夠控制當前頁面。這使sw儘快啓動,惟一的應用場景就是能夠在fetch中動態進行緩存,sw啓動越早,能緩存到的資源就越多)
  6. 對於第一個sw(當前沒有舊的sw在運行),當第一個sw安裝好了以後,只要用戶刷新頁面或者訪問了其餘頁面再回來,sw就能夠接收fetch事件了

  對於如下頁面:shell

<!DOCTYPE html> An image will appear here in 3 seconds: <script> navigator.serviceWorker.register('/sw.js') .then(reg => console.log('SW registered!', reg)) .catch(err => console.log('Boo!', err)); setTimeout(() => { const img = new Image(); img.src = '/dog.svg'; document.body.appendChild(img); }, 3000); </script>

  sw.js:數據庫

self.addEventListener('install', event => { console.log('V1 installing…'); // cache a cat SVG
 event.waitUntil( caches.open('static-v1').then(cache => cache.add('/cat.svg')) ); }); self.addEventListener('activate', event => { console.log('V1 now ready to handle fetches!'); }); self.addEventListener('fetch', event => { const url = new URL(event.request.url); // serve the cat SVG from the cache if the request is
  // same-origin and the path is '/dog.svg'
  if (url.origin == location.origin && url.pathname == '/dog.svg') { event.respondWith(caches.match('/cat.svg')); } });

  以上的運行效果就是:頁面打開以後,看到dog.svg,頁面刷新以後,就只看到cat.svg了。  express

預備知識

瀏覽器支持:https://jakearchibald.github.io/isserviceworkerready/

https

  開發過程容許咱們使用localhost,但發佈以後就必須使用https了。

  使用service worker能夠劫持鏈接,僞造以及過濾響應,但這些可能會影響咱們站點的安全性,爲了不這個,必須使用https,這樣一來所接收到的sw就不會被篡改替換了。sw權限太大,要是sw被篡改,會很麻煩

註冊一個SW

window.navigator 對象包含有關訪問者瀏覽器的信息。如userAgent也在這個對象中。

if ('serviceWorker' in navigator) { window.addEventListener('load', function() { navigator.serviceWorker.register('/sw.js').then(function(registration) { // Registration was successful
      console.log('ServiceWorker registration successful with scope: ', registration.scope); }, function(err) { // registration failed :(
      console.log('ServiceWorker registration failed: ', err); }); }); }

  能夠在任意時候註冊sw,瀏覽器會自動判斷這個sw是否已經註冊過了,是的話則直接拿來用。

  以上使用的是根目錄下的sw腳本,這意味着這個sw的做用域就是整個域。sw能夠從本身的域中接收fetch事件,假如sw位於/example/sw.js,則sw就收到的fetch事件僅僅是url前綴爲/example/了(如/example/page1)。一旦頁面處於sw的控制,這個頁面的請求都會轉發給對應的sw。navigator.serviceWorker.controller指向對應的sw實例,若是頁面沒有被控制,這個值爲null。

  能夠訪問 chrome://inspect/#servie-workers 來檢查站點中的sw是否已經啓用。測試開啓和關閉sw的最好方式是使用瀏覽器隱身模式,由於對其餘頁面無影響,關閉後對應的sw以及緩存等所有都會被清除。

  在最開始沒有sw時,執行以上的register,瀏覽器會開始下載腳本(協商緩存)而後初始化執行。假以下載失敗或者執行失敗,register返回的promise就會被reject,這個sw也無效了。

  通常在onload以後才進行sw的註冊,對網絡較慢的移動設備比較友好。由於瀏覽器新建立一個線程來下載和運行sw資源,是佔用帶寬以及CPU的,這會影響頁面的首次加載。因此通常按照以上方式來register(先判斷、後寫onload回調)

  sw active後,瀏覽器會在頁面任何請求產生前啓動好sw,因此以後調不調用register是無所謂的。

安裝sw

  在sw腳本內部能夠監聽install事件,能夠在裏面緩存咱們須要的文件,通常分爲以下幾步

  1. 打開緩存
  2. 緩存咱們的文件
  3. 確認全部的資源是否已經被緩存了
var CACHE_NAME = 'my-site-cache-v1'; var urlsToCache = [ '/', '/styles/main.css', '/script/main.js' ]; self.addEventListener('install', function(event) { // Perform install steps
 event.waitUntil( caches.open(CACHE_NAME) .then(function(cache) { console.log('Opened cache'); return cache.addAll(urlsToCache); }) ); });

  open和addAll都會返回一個promise,waitUntil須要接受一個promise,經過這個promise才能知道安裝須要花多長時間,以及是成功了仍是失敗了。

  當全部文件都緩存成功了,則sw就安裝成功了。若是其中任意一個文件下載失敗,則sw就會安裝失敗,因此咱們必須謹慎決定哪些文件須要在安裝步驟被緩存。要緩存的文件越多,則sw安裝失敗的可能性就越大。

  以上open一個cache,若是這個cache已經存在,則使用,不存在則建立。能夠根據緩存分類等需求,open多個cache。

  頁面每次刷新瀏覽器都會檢查sw腳本有沒有發生變化(協商緩存),一旦發生變化,則認爲這是一個新的sw,則又從新執行生命週期(install也會再次執行)

緩存和返回請求

  當一個sw已經安裝,用戶訪問了一個其餘頁面或者刷新,sw會接收到一個fetch事件:

self.addEventListener('fetch', function(event) { event.respondWith( caches.match(event.request) .then(function(response) { // Cache hit - return response
        if (response) { return response; } return fetch(event.request); } ) ); });

  以上match返回一個promise,這個函數內部會尋找請求,而且將已經被sw緩存好的響應信息(指安裝階段被緩存的url的響應信息)返回。若是沒有緩存,則(在zhen中)調用fetch來從網絡獲取並返回請求結果。

  也能夠按照下面的代碼來慢慢地增長緩存(執行fetch以後添加到緩存中):

self.addEventListener('fetch', function(event) { event.respondWith( caches.match(event.request) .then(function(response) { // Cache hit - return response
        if (response) { return response; } // IMPORTANT: Clone the request. A request is a stream and
        // can only be consumed once. Since we are consuming this
        // once by cache and once by the browser for fetch, we need
        // to clone the response.
        var fetchRequest = event.request.clone(); return fetch(fetchRequest).then( function(response) { // Check if we received a valid response
            if(!response || response.status !== 200 || response.type !== 'basic') { return response; } // IMPORTANT: Clone the response. A response is a stream
            // and because we want the browser to consume the response
            // as well as the cache consuming the response, we need
            // to clone it so we have two streams.
            var responseToCache = response.clone(); caches.open(CACHE_NAME) .then(function(cache) { cache.put(event.request, responseToCache); }); return response; } ); }) ); });

  以上根據名字打開一個cache,而後把數據put進去便可。type=basic意味着這個請求來自於當前域,而不是第三方域。response之因此須要clone一次,是由於fetch的response是一個流,body只能被讀取一次,爲了下次緩存使用,須要把流克隆一次。

更新sw

何時纔會觸發更新?

  1. 訪問做用域內的頁面時
  2. register指向的sw腳本地址發生改變時
  3. 調用update來手動觸發更新(當用戶可能長時間使用頁面而不刷新時,咱們能夠setInterval按期執行手動觸發更新sw)
    navigator.serviceWorker.register('/sw.js').then(reg => { // sometime later…
     reg.update(); });

更新過程:

  1. 當用於訪問咱們的頁面時,瀏覽器會首先啓動舊的緩存中的sw(register返回的promise會立刻執行),接着會後臺嘗試從新下載新的sw腳本文件(協商緩存)
  2. 舊版本的sw依然控制着當前頁面。下載後的sw會進入install階段,完成後進入waiting狀態,等待active(使用self.skipWaiting能夠在install以後立刻active,跳過waiting階段,觸發active事件)。能夠經過F12 -> application -> Service Workers 中查看到新的sw處於waiting狀態
  3. 當舊的sw控制的頁面全都被關閉後(再也不控制任何頁面),舊的sw就會被移除。瀏覽器就是經過這點來保證同時只運行一個版本的sw
  4. 頁面從新打開,新的sw控制當前頁面,纔會觸發active事件。
  5. 新的sw更新與舊的sw運行是相互獨立的,互不影響

  在activate的回調事件中,通常進行緩存管理。緣由是須要清除掉舊的sw在install階段的緩存。如下代碼將不在白名單中的cache所有刪除:

self.addEventListener('activate', function(event) { var cacheWhitelist = ['pages-cache-v1', 'blog-posts-cache-v1']; event.waitUntil( caches.keys().then(function(cacheNames) { return Promise.all( cacheNames.map(function(cacheName) { if (cacheWhitelist.indexOf(cacheName) === -1) { return caches.delete(cacheName); } }) ); }) ); });

刷新不能active sw的緣由是:

  刷新時,當前頁面等到響應頭到來時纔可能會銷燬。若是響應頭中包含了Content-Disposition頭,則頁面不會被銷燬。因此爲了active一個sw,應該關閉全部這個域下的標籤或者都訪問其餘頁面。這點相似於chrome的更新,新的chrome在後臺下載,僅當chrome徹底重啓後,纔會應用上去。 

  這個特性會致使開發比較困難,每次都要從新關閉打開頁面。其實chrome提供了一個功能,來改變這一點,使開發變得簡單。只須要勾選 Application->Service Worker -> Update on Reload。這樣一來,只要刷新頁面,就會從新下載sw,而且走生命週期,即便這個sw沒有被修改。並且跳過waiting階段直接active生效。

僅當舊的sw被移除並且新的sw控制當前頁面時,active事件纔會執行。因此通常在裏面遷移數據庫、清除舊的sw緩存等。

使用sw來更新cache

1.優先訪問網絡,訪問不了再取緩存:http://www.cnblogs.com/hellohello/p/9063241.html#e

2.使用構建工具插件 sw-precache,其中demo裏生成的sw.js邏輯大體以下:

  • sw-precache-config.js配置文件制定了要監視哪些資源文件
  • 將被監視的資源文件路徑以及文件的hash值寫入到生成的service-worker.js文件中,保存到這個變量中「precacheConfig」。也就是說每次發佈,只要資源變化了,service-worker.js也會發生變化,會從新進入install階段、activae階段。
  • 根據變量precacheConfig中的值,生成多個地址,如a.css?[hash]
  • 在install階段對以上地址進行資源緩存。在activate階段對以上地址之外的資源進行清除
  • 攔截fetch的時候,緩存優先,沒緩存再去訪問網絡

 附上一段生成好的service-worker.js:

  1 /**
  2  * Copyright 2016 Google Inc. All rights reserved.
  3  *
  4  * Licensed under the Apache License, Version 2.0 (the "License");
  5  * you may not use this file except in compliance with the License.
  6  * You may obtain a copy of the License at
  7  *
  8  *     http://www.apache.org/licenses/LICENSE-2.0
  9  *
 10  * Unless required by applicable law or agreed to in writing, software
 11  * distributed under the License is distributed on an "AS IS" BASIS,
 12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 13  * See the License for the specific language governing permissions and
 14  * limitations under the License.
 15 */
 16 
 17 // DO NOT EDIT THIS GENERATED OUTPUT DIRECTLY!
 18 // This file should be overwritten as part of your build process.
 19 // If you need to extend the behavior of the generated service worker, the best approach is to write
 20 // additional code and include it using the importScripts option:
 21 //   https://github.com/GoogleChrome/sw-precache#importscripts-arraystring
 22 //
 23 // Alternatively, it's possible to make changes to the underlying template file and then use that as the
 24 // new base for generating output, via the templateFilePath option:
 25 //   https://github.com/GoogleChrome/sw-precache#templatefilepath-string
 26 //
 27 // If you go that route, make sure that whenever you update your sw-precache dependency, you reconcile any
 28 // changes made to this original template file with your modified copy.
 29 
 30 // This generated service worker JavaScript will precache your site's resources.
 31 // The code needs to be saved in a .js file at the top-level of your site, and registered
 32 // from your pages in order to be used. See
 33 // https://github.com/googlechrome/sw-precache/blob/master/demo/app/js/service-worker-registration.js
 34 // for an example of how you can register this script and handle various service worker events.
 35 
 36 /* eslint-env worker, serviceworker */
 37 /* eslint-disable indent, no-unused-vars, no-multiple-empty-lines, max-nested-callbacks, space-before-function-paren, quotes, comma-spacing */
 38 'use strict';
 39 
 40 var precacheConfig = [["css/main.css","3cb4f06fd9e705bea97eb1bece31fd6d"],["images/one.png","c5a951f965e6810d7b65615ee0d15053"],["images/two.png","29d2cd301ed1e5497e12cafee35a0188"],["index.html","d378b5b669cd3e69fcf8397eba85b67d"],["js/a.js","18ecf599c02b50bf02b849d823ce81f0"],["js/b.js","c7a9d7171499d530709140778f1241cb"],["js/service-worker-registration.js","d60f01dc1393cbaaf4f7435339074d5e"]];
 41 var cacheName = 'sw-precache-v3-sw-precache-' + (self.registration ? self.registration.scope : '');
 42 
 43 
 44 var ignoreUrlParametersMatching = [/^utm_/];
 45 
 46 
 47 
 48 var addDirectoryIndex = function (originalUrl, index) {
 49     var url = new URL(originalUrl);
 50     if (url.pathname.slice(-1) === '/') {
 51       url.pathname += index;
 52     }
 53     return url.toString();
 54   };
 55 
 56 var cleanResponse = function (originalResponse) {
 57     // If this is not a redirected response, then we don't have to do anything.
 58     if (!originalResponse.redirected) {
 59       return Promise.resolve(originalResponse);
 60     }
 61 
 62     // Firefox 50 and below doesn't support the Response.body stream, so we may
 63     // need to read the entire body to memory as a Blob.
 64     var bodyPromise = 'body' in originalResponse ?
 65       Promise.resolve(originalResponse.body) :
 66       originalResponse.blob();
 67 
 68     return bodyPromise.then(function(body) {
 69       // new Response() is happy when passed either a stream or a Blob.
 70       return new Response(body, {
 71         headers: originalResponse.headers,
 72         status: originalResponse.status,
 73         statusText: originalResponse.statusText
 74       });
 75     });
 76   };
 77 
 78 var createCacheKey = function (originalUrl, paramName, paramValue,
 79                            dontCacheBustUrlsMatching) {
 80     // Create a new URL object to avoid modifying originalUrl.
 81     var url = new URL(originalUrl);
 82 
 83     // If dontCacheBustUrlsMatching is not set, or if we don't have a match,
 84     // then add in the extra cache-busting URL parameter.
 85     if (!dontCacheBustUrlsMatching ||
 86         !(url.pathname.match(dontCacheBustUrlsMatching))) {
 87       url.search += (url.search ? '&' : '') +
 88         encodeURIComponent(paramName) + '=' + encodeURIComponent(paramValue);
 89     }
 90 
 91     return url.toString();
 92   };
 93 
 94 var isPathWhitelisted = function (whitelist, absoluteUrlString) {
 95     // If the whitelist is empty, then consider all URLs to be whitelisted.
 96     if (whitelist.length === 0) {
 97       return true;
 98     }
 99 
100     // Otherwise compare each path regex to the path of the URL passed in.
101     var path = (new URL(absoluteUrlString)).pathname;
102     return whitelist.some(function(whitelistedPathRegex) {
103       return path.match(whitelistedPathRegex);
104     });
105   };
106 
107 var stripIgnoredUrlParameters = function (originalUrl,
108     ignoreUrlParametersMatching) {
109     var url = new URL(originalUrl);
110     // Remove the hash; see https://github.com/GoogleChrome/sw-precache/issues/290
111     url.hash = '';
112 
113     url.search = url.search.slice(1) // Exclude initial '?'
114       .split('&') // Split into an array of 'key=value' strings
115       .map(function(kv) {
116         return kv.split('='); // Split each 'key=value' string into a [key, value] array
117       })
118       .filter(function(kv) {
119         return ignoreUrlParametersMatching.every(function(ignoredRegex) {
120           return !ignoredRegex.test(kv[0]); // Return true iff the key doesn't match any of the regexes.
121         });
122       })
123       .map(function(kv) {
124         return kv.join('='); // Join each [key, value] array into a 'key=value' string
125       })
126       .join('&'); // Join the array of 'key=value' strings into a string with '&' in between each
127 
128     return url.toString();
129   };
130 
131 
132 var hashParamName = '_sw-precache';
133 var urlsToCacheKeys = new Map(
134   precacheConfig.map(function(item) {
135     var relativeUrl = item[0];
136     var hash = item[1];
137     var absoluteUrl = new URL(relativeUrl, self.location);
138     var cacheKey = createCacheKey(absoluteUrl, hashParamName, hash, false);
139     return [absoluteUrl.toString(), cacheKey];
140   })
141 );
142 
143 function setOfCachedUrls(cache) {
144   return cache.keys().then(function(requests) {
145     return requests.map(function(request) {
146       return request.url;
147     });
148   }).then(function(urls) {
149     return new Set(urls);
150   });
151 }
152 
153 self.addEventListener('install', function(event) {
154   event.waitUntil(
155     caches.open(cacheName).then(function(cache) {
156       return setOfCachedUrls(cache).then(function(cachedUrls) {
157         return Promise.all(
158           Array.from(urlsToCacheKeys.values()).map(function(cacheKey) {
159             // If we don't have a key matching url in the cache already, add it.
160             if (!cachedUrls.has(cacheKey)) {
161               var request = new Request(cacheKey, {credentials: 'same-origin'});
162               return fetch(request).then(function(response) {
163                 // Bail out of installation unless we get back a 200 OK for
164                 // every request.
165                 if (!response.ok) {
166                   throw new Error('Request for ' + cacheKey + ' returned a ' +
167                     'response with status ' + response.status);
168                 }
169 
170                 return cleanResponse(response).then(function(responseToCache) {
171                   return cache.put(cacheKey, responseToCache);
172                 });
173               });
174             }
175           })
176         );
177       });
178     }).then(function() {
179       
180       // Force the SW to transition from installing -> active state
181       return self.skipWaiting();
182       
183     })
184   );
185 });
186 
187 self.addEventListener('activate', function(event) {
188   var setOfExpectedUrls = new Set(urlsToCacheKeys.values());
189 
190   event.waitUntil(
191     caches.open(cacheName).then(function(cache) {
192       return cache.keys().then(function(existingRequests) {
193         return Promise.all(
194           existingRequests.map(function(existingRequest) {
195             if (!setOfExpectedUrls.has(existingRequest.url)) {
196               return cache.delete(existingRequest);
197             }
198           })
199         );
200       });
201     }).then(function() {
202       
203       return self.clients.claim();
204       
205     })
206   );
207 });
208 
209 
210 self.addEventListener('fetch', function(event) {
211   if (event.request.method === 'GET') {
212     // Should we call event.respondWith() inside this fetch event handler?
213     // This needs to be determined synchronously, which will give other fetch
214     // handlers a chance to handle the request if need be.
215     var shouldRespond;
216 
217     // First, remove all the ignored parameters and hash fragment, and see if we
218     // have that URL in our cache. If so, great! shouldRespond will be true.
219     var url = stripIgnoredUrlParameters(event.request.url, ignoreUrlParametersMatching);
220     shouldRespond = urlsToCacheKeys.has(url);
221 
222     // If shouldRespond is false, check again, this time with 'index.html'
223     // (or whatever the directoryIndex option is set to) at the end.
224     var directoryIndex = 'index.html';
225     if (!shouldRespond && directoryIndex) {
226       url = addDirectoryIndex(url, directoryIndex);
227       shouldRespond = urlsToCacheKeys.has(url);
228     }
229 
230     // If shouldRespond is still false, check to see if this is a navigation
231     // request, and if so, whether the URL matches navigateFallbackWhitelist.
232     var navigateFallback = '';
233     if (!shouldRespond &&
234         navigateFallback &&
235         (event.request.mode === 'navigate') &&
236         isPathWhitelisted([], event.request.url)) {
237       url = new URL(navigateFallback, self.location).toString();
238       shouldRespond = urlsToCacheKeys.has(url);
239     }
240 
241     // If shouldRespond was set to true at any point, then call
242     // event.respondWith(), using the appropriate cache key.
243     if (shouldRespond) {
244       event.respondWith(
245         caches.open(cacheName).then(function(cache) {
246           return cache.match(urlsToCacheKeys.get(url)).then(function(response) {
247             if (response) {
248               return response;
249             }
250             throw Error('The cached response that was expected is missing.');
251           });
252         }).catch(function(e) {
253           // Fall back to just fetch()ing the request if some unexpected error
254           // prevented the cached response from being valid.
255           console.warn('Couldn\'t serve response for "%s" from cache: %O', event.request.url, e);
256           return fetch(event.request);
257         })
258       );
259     }
260   }
261 });
262 
263 
264 // *** Start of auto-included sw-toolbox code. ***
265 /* 
266  Copyright 2016 Google Inc. All Rights Reserved.
267 
268  Licensed under the Apache License, Version 2.0 (the "License");
269  you may not use this file except in compliance with the License.
270  You may obtain a copy of the License at
271 
272      http://www.apache.org/licenses/LICENSE-2.0
273 
274  Unless required by applicable law or agreed to in writing, software
275  distributed under the License is distributed on an "AS IS" BASIS,
276  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
277  See the License for the specific language governing permissions and
278  limitations under the License.
279 */!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var t;t="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,t.toolbox=e()}}(function(){return function e(t,n,r){function o(c,s){if(!n[c]){if(!t[c]){var a="function"==typeof require&&require;if(!s&&a)return a(c,!0);if(i)return i(c,!0);var u=new Error("Cannot find module '"+c+"'");throw u.code="MODULE_NOT_FOUND",u}var f=n[c]={exports:{}};t[c][0].call(f.exports,function(e){var n=t[c][1][e];return o(n?n:e)},f,f.exports,e,t,n,r)}return n[c].exports}for(var i="function"==typeof require&&require,c=0;c<r.length;c++)o(r[c]);return o}({1:[function(e,t,n){"use strict";function r(e,t){t=t||{};var n=t.debug||m.debug;n&&console.log("[sw-toolbox] "+e)}function o(e){var t;return e&&e.cache&&(t=e.cache.name),t=t||m.cache.name,caches.open(t)}function i(e,t){t=t||{};var n=t.successResponses||m.successResponses;return fetch(e.clone()).then(function(r){return"GET"===e.method&&n.test(r.status)&&o(t).then(function(n){n.put(e,r).then(function(){var r=t.cache||m.cache;(r.maxEntries||r.maxAgeSeconds)&&r.name&&c(e,n,r)})}),r.clone()})}function c(e,t,n){var r=s.bind(null,e,t,n);d=d?d.then(r):r()}function s(e,t,n){var o=e.url,i=n.maxAgeSeconds,c=n.maxEntries,s=n.name,a=Date.now();return r("Updating LRU order for "+o+". Max entries is "+c+", max age is "+i),g.getDb(s).then(function(e){return g.setTimestampForUrl(e,o,a)}).then(function(e){return g.expireEntries(e,c,i,a)}).then(function(e){r("Successfully updated IDB.");var n=e.map(function(e){return t.delete(e)});return Promise.all(n).then(function(){r("Done with cache cleanup.")})}).catch(function(e){r(e)})}function a(e,t,n){return r("Renaming cache: ["+e+"] to ["+t+"]",n),caches.delete(t).then(function(){return Promise.all([caches.open(e),caches.open(t)]).then(function(t){var n=t[0],r=t[1];return n.keys().then(function(e){return Promise.all(e.map(function(e){return n.match(e).then(function(t){return r.put(e,t)})}))}).then(function(){return caches.delete(e)})})})}function u(e,t){return o(t).then(function(t){return t.add(e)})}function f(e,t){return o(t).then(function(t){return t.delete(e)})}function h(e){e instanceof Promise||p(e),m.preCacheItems=m.preCacheItems.concat(e)}function p(e){var t=Array.isArray(e);if(t&&e.forEach(function(e){"string"==typeof e||e instanceof Request||(t=!1)}),!t)throw new TypeError("The precache method expects either an array of strings and/or Requests or a Promise that resolves to an array of strings and/or Requests.");return e}function l(e,t,n){if(!e)return!1;if(t){var r=e.headers.get("date");if(r){var o=new Date(r);if(o.getTime()+1e3*t<n)return!1}}return!0}var d,m=e("./options"),g=e("./idb-cache-expiration");t.exports={debug:r,fetchAndCache:i,openCache:o,renameCache:a,cache:u,uncache:f,precache:h,validatePrecacheInput:p,isResponseFresh:l}},{"./idb-cache-expiration":2,"./options":4}],2:[function(e,t,n){"use strict";function r(e){return new Promise(function(t,n){var r=indexedDB.open(u+e,f);r.onupgradeneeded=function(){var e=r.result.createObjectStore(h,{keyPath:p});e.createIndex(l,l,{unique:!1})},r.onsuccess=function(){t(r.result)},r.onerror=function(){n(r.error)}})}function o(e){return e in d||(d[e]=r(e)),d[e]}function i(e,t,n){return new Promise(function(r,o){var i=e.transaction(h,"readwrite"),c=i.objectStore(h);c.put({url:t,timestamp:n}),i.oncomplete=function(){r(e)},i.onabort=function(){o(i.error)}})}function c(e,t,n){return t?new Promise(function(r,o){var i=1e3*t,c=[],s=e.transaction(h,"readwrite"),a=s.objectStore(h),u=a.index(l);u.openCursor().onsuccess=function(e){var t=e.target.result;if(t&&n-i>t.value[l]){var r=t.value[p];c.push(r),a.delete(r),t.continue()}},s.oncomplete=function(){r(c)},s.onabort=o}):Promise.resolve([])}function s(e,t){return t?new Promise(function(n,r){var o=[],i=e.transaction(h,"readwrite"),c=i.objectStore(h),s=c.index(l),a=s.count();s.count().onsuccess=function(){var e=a.result;e>t&&(s.openCursor().onsuccess=function(n){var r=n.target.result;if(r){var i=r.value[p];o.push(i),c.delete(i),e-o.length>t&&r.continue()}})},i.oncomplete=function(){n(o)},i.onabort=r}):Promise.resolve([])}function a(e,t,n,r){return c(e,n,r).then(function(n){return s(e,t).then(function(e){return n.concat(e)})})}var u="sw-toolbox-",f=1,h="store",p="url",l="timestamp",d={};t.exports={getDb:o,setTimestampForUrl:i,expireEntries:a}},{}],3:[function(e,t,n){"use strict";function r(e){var t=a.match(e.request);t?e.respondWith(t(e.request)):a.default&&"GET"===e.request.method&&0===e.request.url.indexOf("http")&&e.respondWith(a.default(e.request))}function o(e){s.debug("activate event fired");var t=u.cache.name+"$$$inactive$$$";e.waitUntil(s.renameCache(t,u.cache.name))}function i(e){return e.reduce(function(e,t){return e.concat(t)},[])}function c(e){var t=u.cache.name+"$$$inactive$$$";s.debug("install event fired"),s.debug("creating cache ["+t+"]"),e.waitUntil(s.openCache({cache:{name:t}}).then(function(e){return Promise.all(u.preCacheItems).then(i).then(s.validatePrecacheInput).then(function(t){return s.debug("preCache list: "+(t.join(", ")||"(none)")),e.addAll(t)})}))}e("serviceworker-cache-polyfill");var s=e("./helpers"),a=e("./router"),u=e("./options");t.exports={fetchListener:r,activateListener:o,installListener:c}},{"./helpers":1,"./options":4,"./router":6,"serviceworker-cache-polyfill":16}],4:[function(e,t,n){"use strict";var r;r=self.registration?self.registration.scope:self.scope||new URL("./",self.location).href,t.exports={cache:{name:"$$$toolbox-cache$$$"+r+"$$$",maxAgeSeconds:null,maxEntries:null},debug:!1,networkTimeoutSeconds:null,preCacheItems:[],successResponses:/^0|([123]\d\d)|(40[14567])|410$/}},{}],5:[function(e,t,n){"use strict";var r=new URL("./",self.location),o=r.pathname,i=e("path-to-regexp"),c=function(e,t,n,r){t instanceof RegExp?this.fullUrlRegExp=t:(0!==t.indexOf("/")&&(t=o+t),this.keys=[],this.regexp=i(t,this.keys)),this.method=e,this.options=r,this.handler=n};c.prototype.makeHandler=function(e){var t;if(this.regexp){var n=this.regexp.exec(e);t={},this.keys.forEach(function(e,r){t[e.name]=n[r+1]})}return function(e){return this.handler(e,t,this.options)}.bind(this)},t.exports=c},{"path-to-regexp":15}],6:[function(e,t,n){"use strict";function r(e){return e.replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&")}var o=e("./route"),i=e("./helpers"),c=function(e,t){for(var n=e.entries(),r=n.next(),o=[];!r.done;){var i=new RegExp(r.value[0]);i.test(t)&&o.push(r.value[1]),r=n.next()}return o},s=function(){this.routes=new Map,this.routes.set(RegExp,new Map),this.default=null};["get","post","put","delete","head","any"].forEach(function(e){s.prototype[e]=function(t,n,r){return this.add(e,t,n,r)}}),s.prototype.add=function(e,t,n,c){c=c||{};var s;t instanceof RegExp?s=RegExp:(s=c.origin||self.location.origin,s=s instanceof RegExp?s.source:r(s)),e=e.toLowerCase();var a=new o(e,t,n,c);this.routes.has(s)||this.routes.set(s,new Map);var u=this.routes.get(s);u.has(e)||u.set(e,new Map);var f=u.get(e),h=a.regexp||a.fullUrlRegExp;f.has(h.source)&&i.debug('"'+t+'" resolves to same regex as existing route.'),f.set(h.source,a)},s.prototype.matchMethod=function(e,t){var n=new URL(t),r=n.origin,o=n.pathname;return this._match(e,c(this.routes,r),o)||this._match(e,[this.routes.get(RegExp)],t)},s.prototype._match=function(e,t,n){if(0===t.length)return null;for(var r=0;r<t.length;r++){var o=t[r],i=o&&o.get(e.toLowerCase());if(i){var s=c(i,n);if(s.length>0)return s[0].makeHandler(n)}}return null},s.prototype.match=function(e){return this.matchMethod(e.method,e.url)||this.matchMethod("any",e.url)},t.exports=new s},{"./helpers":1,"./route":5}],7:[function(e,t,n){"use strict";function r(e,t,n){return n=n||{},i.debug("Strategy: cache first ["+e.url+"]",n),i.openCache(n).then(function(t){return t.match(e).then(function(t){var r=n.cache||o.cache,c=Date.now();return i.isResponseFresh(t,r.maxAgeSeconds,c)?t:i.fetchAndCache(e,n)})})}var o=e("../options"),i=e("../helpers");t.exports=r},{"../helpers":1,"../options":4}],8:[function(e,t,n){"use strict";function r(e,t,n){return n=n||{},i.debug("Strategy: cache only ["+e.url+"]",n),i.openCache(n).then(function(t){return t.match(e).then(function(e){var t=n.cache||o.cache,r=Date.now();if(i.isResponseFresh(e,t.maxAgeSeconds,r))return e})})}var o=e("../options"),i=e("../helpers");t.exports=r},{"../helpers":1,"../options":4}],9:[function(e,t,n){"use strict";function r(e,t,n){return o.debug("Strategy: fastest ["+e.url+"]",n),new Promise(function(r,c){var s=!1,a=[],u=function(e){a.push(e.toString()),s?c(new Error('Both cache and network failed: "'+a.join('", "')+'"')):s=!0},f=function(e){e instanceof Response?r(e):u("No result returned")};o.fetchAndCache(e.clone(),n).then(f,u),i(e,t,n).then(f,u)})}var o=e("../helpers"),i=e("./cacheOnly");t.exports=r},{"../helpers":1,"./cacheOnly":8}],10:[function(e,t,n){t.exports={networkOnly:e("./networkOnly"),networkFirst:e("./networkFirst"),cacheOnly:e("./cacheOnly"),cacheFirst:e("./cacheFirst"),fastest:e("./fastest")}},{"./cacheFirst":7,"./cacheOnly":8,"./fastest":9,"./networkFirst":11,"./networkOnly":12}],11:[function(e,t,n){"use strict";function r(e,t,n){n=n||{};var r=n.successResponses||o.successResponses,c=n.networkTimeoutSeconds||o.networkTimeoutSeconds;return i.debug("Strategy: network first ["+e.url+"]",n),i.openCache(n).then(function(t){var s,a,u=[];if(c){var f=new Promise(function(r){s=setTimeout(function(){t.match(e).then(function(e){var t=n.cache||o.cache,c=Date.now(),s=t.maxAgeSeconds;i.isResponseFresh(e,s,c)&&r(e)})},1e3*c)});u.push(f)}var h=i.fetchAndCache(e,n).then(function(e){if(s&&clearTimeout(s),r.test(e.status))return e;throw i.debug("Response was an HTTP error: "+e.statusText,n),a=e,new Error("Bad response")}).catch(function(r){return i.debug("Network or response error, fallback to cache ["+e.url+"]",n),t.match(e).then(function(e){if(e)return e;if(a)return a;throw r})});return u.push(h),Promise.race(u)})}var o=e("../options"),i=e("../helpers");t.exports=r},{"../helpers":1,"../options":4}],12:[function(e,t,n){"use strict";function r(e,t,n){return o.debug("Strategy: network only ["+e.url+"]",n),fetch(e)}var o=e("../helpers");t.exports=r},{"../helpers":1}],13:[function(e,t,n){"use strict";var r=e("./options"),o=e("./router"),i=e("./helpers"),c=e("./strategies"),s=e("./listeners");i.debug("Service Worker Toolbox is loading"),self.addEventListener("install",s.installListener),self.addEventListener("activate",s.activateListener),self.addEventListener("fetch",s.fetchListener),t.exports={networkOnly:c.networkOnly,networkFirst:c.networkFirst,cacheOnly:c.cacheOnly,cacheFirst:c.cacheFirst,fastest:c.fastest,router:o,options:r,cache:i.cache,uncache:i.uncache,precache:i.precache}},{"./helpers":1,"./listeners":3,"./options":4,"./router":6,"./strategies":10}],14:[function(e,t,n){t.exports=Array.isArray||function(e){return"[object Array]"==Object.prototype.toString.call(e)}},{}],15:[function(e,t,n){function r(e,t){for(var n,r=[],o=0,i=0,c="",s=t&&t.delimiter||"/";null!=(n=x.exec(e));){var f=n[0],h=n[1],p=n.index;if(c+=e.slice(i,p),i=p+f.length,h)c+=h[1];else{var l=e[i],d=n[2],m=n[3],g=n[4],v=n[5],w=n[6],y=n[7];c&&(r.push(c),c="");var b=null!=d&&null!=l&&l!==d,E="+"===w||"*"===w,R="?"===w||"*"===w,k=n[2]||s,$=g||v;r.push({name:m||o++,prefix:d||"",delimiter:k,optional:R,repeat:E,partial:b,asterisk:!!y,pattern:$?u($):y?".*":"[^"+a(k)+"]+?"})}}return i<e.length&&(c+=e.substr(i)),c&&r.push(c),r}function o(e,t){return s(r(e,t))}function i(e){return encodeURI(e).replace(/[\/?#]/g,function(e){return"%"+e.charCodeAt(0).toString(16).toUpperCase()})}function c(e){return encodeURI(e).replace(/[?#]/g,function(e){return"%"+e.charCodeAt(0).toString(16).toUpperCase()})}function s(e){for(var t=new Array(e.length),n=0;n<e.length;n++)"object"==typeof e[n]&&(t[n]=new RegExp("^(?:"+e[n].pattern+")$"));return function(n,r){for(var o="",s=n||{},a=r||{},u=a.pretty?i:encodeURIComponent,f=0;f<e.length;f++){var h=e[f];if("string"!=typeof h){var p,l=s[h.name];if(null==l){if(h.optional){h.partial&&(o+=h.prefix);continue}throw new TypeError('Expected "'+h.name+'" to be defined')}if(v(l)){if(!h.repeat)throw new TypeError('Expected "'+h.name+'" to not repeat, but received `'+JSON.stringify(l)+"`");if(0===l.length){if(h.optional)continue;throw new TypeError('Expected "'+h.name+'" to not be empty')}for(var d=0;d<l.length;d++){if(p=u(l[d]),!t[f].test(p))throw new TypeError('Expected all "'+h.name+'" to match "'+h.pattern+'", but received `'+JSON.stringify(p)+"`");o+=(0===d?h.prefix:h.delimiter)+p}}else{if(p=h.asterisk?c(l):u(l),!t[f].test(p))throw new TypeError('Expected "'+h.name+'" to match "'+h.pattern+'", but received "'+p+'"');o+=h.prefix+p}}else o+=h}return o}}function a(e){return e.replace(/([.+*?=^!:${}()[\]|\/\\])/g,"\\$1")}function u(e){return e.replace(/([=!:$\/()])/g,"\\$1")}function f(e,t){return e.keys=t,e}function h(e){return e.sensitive?"":"i"}function p(e,t){var n=e.source.match(/\((?!\?)/g);if(n)for(var r=0;r<n.length;r++)t.push({name:r,prefix:null,delimiter:null,optional:!1,repeat:!1,partial:!1,asterisk:!1,pattern:null});return f(e,t)}function l(e,t,n){for(var r=[],o=0;o<e.length;o++)r.push(g(e[o],t,n).source);var i=new RegExp("(?:"+r.join("|")+")",h(n));return f(i,t)}function d(e,t,n){return m(r(e,n),t,n)}function m(e,t,n){v(t)||(n=t||n,t=[]),n=n||{};for(var r=n.strict,o=n.end!==!1,i="",c=0;c<e.length;c++){var s=e[c];if("string"==typeof s)i+=a(s);else{var u=a(s.prefix),p="(?:"+s.pattern+")";t.push(s),s.repeat&&(p+="(?:"+u+p+")*"),p=s.optional?s.partial?u+"("+p+")?":"(?:"+u+"("+p+"))?":u+"("+p+")",i+=p}}var l=a(n.delimiter||"/"),d=i.slice(-l.length)===l;return r||(i=(d?i.slice(0,-l.length):i)+"(?:"+l+"(?=$))?"),i+=o?"$":r&&d?"":"(?="+l+"|$)",f(new RegExp("^"+i,h(n)),t)}function g(e,t,n){return v(t)||(n=t||n,t=[]),n=n||{},e instanceof RegExp?p(e,t):v(e)?l(e,t,n):d(e,t,n)}var v=e("isarray");t.exports=g,t.exports.parse=r,t.exports.compile=o,t.exports.tokensToFunction=s,t.exports.tokensToRegExp=m;var x=new RegExp(["(\\\\.)","([\\/.])?(?:(?:\\:(\\w+)(?:\\(((?:\\\\.|[^\\\\()])+)\\))?|\\(((?:\\\\.|[^\\\\()])+)\\))([+*?])?|(\\*))"].join("|"),"g")},{isarray:14}],16:[function(e,t,n){!function(){var e=Cache.prototype.addAll,t=navigator.userAgent.match(/(Firefox|Chrome)\/(\d+\.)/);if(t)var n=t[1],r=parseInt(t[2]);e&&(!t||"Firefox"===n&&r>=46||"Chrome"===n&&r>=50)||(Cache.prototype.addAll=function(e){function t(e){this.name="NetworkError",this.code=19,this.message=e}var n=this;return t.prototype=Object.create(Error.prototype),Promise.resolve().then(function(){if(arguments.length<1)throw new TypeError;return e=e.map(function(e){return e instanceof Request?e:String(e)}),Promise.all(e.map(function(e){"string"==typeof e&&(e=new Request(e));var n=new URL(e.url).protocol;if("http:"!==n&&"https:"!==n)throw new t("Invalid scheme");return fetch(e.clone())}))}).then(function(r){if(r.some(function(e){return!e.ok}))throw new t("Incorrect response status");return Promise.all(r.map(function(t,r){return n.put(e[r],t)}))}).then(function(){})},Cache.prototype.add=function(e){return this.addAll([e])})}()},{}]},{},[13])(13)});
280 
281 
282 // *** End of auto-included sw-toolbox code. ***
283 
284 
285 
286 // Runtime cache configuration, using the sw-toolbox library.
287 
288 toolbox.router.get(/runtime-caching/, toolbox.cacheFirst, {"cache":{"maxEntries":1,"name":"runtime-cache"}});
View Code

使用skipWaiting和claim實現「Immediate claim」

  使sw install以後當即active控制當前頁面,而不須要關閉再打開(這個過程,一些文檔稱爲 waiting for a navigation event)。

// Install event - cache files (...or not) // Be sure to call skipWaiting()!
self.addEventListener('install', function(event) { event.waitUntil( caches.open('my-cache').then(function(cache) { // Important to `return` the promise here to have `skipWaiting()`
        // fire after the cache has been updated.
        return cache.addAll([/* file1.jpg, file2.png, ... */]); }).then(function() { // `skipWaiting()` forces the waiting ServiceWorker to become the
      // active ServiceWorker, triggering the `onactivate` event.
      // Together with `Clients.claim()` this allows a worker to take effect
      // immediately in the client(s).
      return self.skipWaiting(); }) ); }); // Activate event // Be sure to call self.clients.claim()
self.addEventListener('activate', function(event) { // `claim()` sets this worker as the active worker for all clients that
    // match the workers scope and triggers an `oncontrollerchange` event for
    // the clients.
    return self.clients.claim(); });

跳過waiting

  瀏覽器經過waiting階段來保證同一時間只運行一個版本的sw,若是你不須要這個特色。能夠執行self.skipWaiting來跳過這個waiting階段。

self.addEventListener('install', event => { self.skipWaiting(); event.waitUntil( // caching etc
 ); });

  這會使新的sw只要一進入waiting階段,就會開始激活(至關於跳過了waiting階段)。當前頁面會受到新的sw控制,而其餘頁面依舊處於舊的sw控制,至關於有兩個版本的sw同時運行了。因此通常不要使用這個函數。

sw資源緩存與http緩存

  sw請求緩存的資源,會轉到http的緩存上來處理,也就是說在http緩存上多加了一層sw緩存。

  sw緩存的資源也會存在於http緩存中,這是由於sw緩存來源於http的響應。因此能夠在http請求時指定不緩存:

self.addEventListener('install', event => { event.waitUntil( caches.open(`static-${version}`) .then(cache => cache.addAll([ new Request('/styles.css', { cache: 'no-cache' }), new Request('/script.js', { cache: 'no-cache' }) ])) ); });

  若是不支持這種寫法,能夠換另外一種方式:

self.addEventListener('install', event => { event.waitUntil( caches.open(`static-${version}`) .then(cache => Promise.all( [ '/styles.css', '/script.js' ].map(url => { // cache-bust using a random query string
          return fetch(`${url}?${Math.random()}`).then(response => { // fail on 404, 500 etc
            if (!response.ok) throw Error('Not ok'); return cache.put(url, response); }) }) )) ); });

  指定緩存資源時能夠指定"/",表明緩存當前頁面。在Application的Cache store中能夠看到sw緩存的內容,即便緩存了當前頁面,離線訪問當前頁面也仍是不行的,沒明白這個緩存斜槓是用來幹嗎的

不要改變頁面中sw腳本的地址

  如./sw-v1.js改成./sw-v2.js。這樣作v2的sw永遠不會生效。

很差的地方

默認的fetch

  默認的fetch不會包含用戶憑證,如cookie,若是想要包含用戶憑證,須要在調用時多加一個配置:

fetch(url, { credentials: 'include' })

處理響應式圖片

  srcset屬性或者picture元素會在運行時選擇合適的圖片而且發出網絡請求。

  對於sw,若是想要在install階段緩存一個圖片,能夠有如下選擇:

  1. 緩存全部picture元素或者srcset屬性可能會請求的圖片
  2. 緩存一套低分辨率的圖片
  3. 緩存一套高分辨率的圖片

  事實上,應該選擇2或者3。由於1太浪費空間了。假設選擇方案2,在sw安裝的時候就緩存好全部的低分辨率圖片,而後在頁面loaded加載完成(sw不須要再次安裝),再去請求高分辨率的圖片。假如請求失敗,就選用低分辨率的圖片。這是好的,但會有一個問題:

  假若有如下兩套圖片

分辨率 寬度 高度
1x 400 400
2x 800 800

   對於img標籤的srcset屬性(ps:對於這個標籤沒有指定寬和高,則顯示時的寬高就是size/dpr了。在這個例子中,當顯示在1dpr的設備上時,一個位圖像素對應一個屏幕物理像素,則圖片顯示出來的效果就是400px*400px,而對於2dpr的設備,須要兩個位圖像素對應一個物理像素,則顯示的效果就是400px*400px了):

<img src="image-src.png" srcset="image-src.png 1x, image-2x.png 2x" />

  對於dpr爲2的設備,瀏覽器會下載image-2x.png。若是咱們離線了,則在catch(由於網絡訪問失敗)中返回image-src.png(低分辨率的圖片已經在install階段下載好了)。這樣一來2dpr的設備上就顯示了400*400的圖片,顯示的效果就是200px*200px了而不是400px*400px,這樣的尺寸變化會影響界面佈局。因此解決辦法就是固定住這個圖片的寬高:

<img src="image-src.png" srcset="image-src.png 1x, image-2x.png 2x" style="width:400px; height: 400px;" />

ps:關於srcset瞭解:http://www.cnblogs.com/flicat/p/4381089.html

高性能加載sw

  什麼是導航請求(navigation requests)?請求的目標是一個文檔,如iframe的src請求就是一個導航請求,這種請求比較消耗性能。SPA依賴於dom替換以及H5中的History API來避免導航請求,但初始打開SPA,也仍是一個導航請求

  對於移動應用來講,數據從客戶端到服務器的往返時間基本大於整個頁面的渲染時間。對於導航請求的優化方式以下:

web stream

https://developers.google.com/web/updates/2016/06/sw-readablestreams

https://jakearchibald.com/2016/streams-ftw/

  處理導航請求時,把html分紅多個部分(一個靜態的header與footer,中間是html正文內容,依賴於url),而後再傳輸這多個部分,這能保證,第一部分能最快顯示出來

緩存靜態的html頁面

  用於處理不依賴於url參數的,不變的靜態html頁面

self.addEventListener('fetch', event => { if (event.request.mode === 'navigate') { // See /web/fundamentals/getting-started/primers/async-functions
    // for an async/await primer.
    event.respondWith(async function() { // Optional: Normalize the incoming URL by removing query parameters.
      // Instead of https://example.com/page?key=value,
      // use https://example.com/page when reading and writing to the cache.
      // For static HTML documents, it's unlikely your query parameters will
      // affect the HTML returned. But if you do use query parameters that
      // uniquely determine your HTML, modify this code to retain them.
      const normalizedUrl = new URL(event.request.url); normalizedUrl.search = ''; // Create promises for both the network response,
      // and a copy of the response that can be used in the cache.
      const fetchResponseP = fetch(normalizedUrl); const fetchResponseCloneP = fetchResponseP.then(r => r.clone()); // event.waitUntil() ensures that the service worker is kept alive
      // long enough to complete the cache update.
      event.waitUntil(async function() { const cache = await caches.open('my-cache-name'); await cache.put(normalizedUrl, await fetchResponseCloneP); }()); // Prefer the cached response, falling back to the fetch response.
      return (await caches.match(normalizedUrl)) || fetchResponseP; }()); } });

使用appShell

  對於SPA,每次的導航請求都去請求這個緩存shell,shell中有完整的代碼能夠根據url參數來動態修改內容

// Not shown: install and activate handlers to keep app-shell.html // cached and up to date.
self.addEventListener('fetch', event => { if (event.request.mode === 'navigate') { // Always respond to navigations with the cached app-shell.html,
    // regardless of the underlying event.request.url value.
    event.respondWith(caches.match('app-shell.html')); } });

其餘待閱讀

https://jakearchibald.github.io/isserviceworkerready/resources.html

https://developers.google.com/web/updates/2017/02/navigation-preload

相關文章
相關標籤/搜索