service worker

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

  它提供了丰富的离线体验,定期的后台同步以及消息推送等技术,这些技术一般依赖于原生应用,但现在可以在web应用上使用了。

什么是service worker(后续简称SW)

  是浏览器后台运行的一个脚本,它为我们提供了许多新特性。这些特性已经包括了消息推送后台同步,未来将会支持定期同步或者地理位置的获取。除了sw,AppCache也支持离线功能。但sw更完善,能从设计上避免AppCache的一些问题。

  • 它是一个web worker,所以不能直接操作dom。而是通过postMessage把消息传递给父页面,然后由父页面来操作dom
  • 它是一个可编程的网络代理,允许我们去操作页面的网络请求
  • 当不需要的时候可以终止,需要的时候可以重新启动。所以最好不要在onmessage中依赖全局数据,如果重启前需要保存数据,重启后获取这些数据,可以在worker内访问indexedDB。
  • 它会大量使用promise

生命周期

  sw的生命周期是最复杂的一部分,只有把它搞清楚了,才能做到无缝地、无冲突地发布更新sw。它与页面的生命周期(onload等)完全分离。

  首先需要页面脚本中注册sw,浏览器就会后台安装sw了。安装完了并且激活完成,页面刷新后,sw就可以控制整个页面了。有如下两种情况:

  1. sw可能会因为内存不足而被终止
  2. 正常运行

以下sw第一次安装的简单的生命周期图示:

生命周期的作用

  1. 在生命周期函数中,下载好需要离线访问的资源
  2. 允许一个新的sw做好准备,同时运行着旧的sw,页面刷新后,新的sw会替换旧的sw
  3. 保证同域下的多个页面可以由对应域的sw来管理,如/sw.js 可以管理/a.html和/b.html(域都是根目录)。
  4. 保证只有一个版本的sw在运行

  最后一点尤为重要,没有sw,用户可以这时打开一个标签,过一会又打开一个新的标签来访问我们的站点,这时两个标签中的站点的版本可能已经不一致了。有时候这是没问题的,但到了需要同步两个页面内的设置时,可以用共享的存储来解决,但这可能出错或者数据丢失。

第一个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事件了

  对于以下页面:

<!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了。  

预备知识

浏览器支持: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

 

posted @ 2018-01-23 11:12  HelloHello233  阅读(504)  评论(0编辑  收藏  举报