https://developers.google.com/web/fundamentals/primers/service-workers/?hl=encss
它提供了丰富的离线体验,按期的后台同步以及消息推送等技术,这些技术通常依赖于原生应用,但如今能够在web应用上使用了。html
是浏览器后台运行的一个脚本,它为咱们提供了许多新特性。这些特性已经包括了消息推送和后台同步,将来将会支持按期同步或者地理位置的获取。除了sw,AppCache也支持离线功能。但sw更完善,能从设计上避免AppCache的一些问题。c++
sw的生命周期是最复杂的一部分,只有把它搞清楚了,才能作到无缝地、无冲突地发布更新sw。它与页面的生命周期(onload等)彻底分离。git
首先须要页面脚本中注册sw,浏览器就会后台安装sw了。安装完了而且激活完成,页面刷新后,sw就能够控制整个页面了。有以下两种状况:github
如下sw第一次安装的简单的生命周期图示:web
最后一点尤其重要,没有sw,用户能够这时打开一个标签,过一会又打开一个新的标签来访问咱们的站点,这时两个标签中的站点的版本可能已经不一致了。有时候这是没问题的,但到了须要同步两个页面内的设置时,能够用共享的存储来解决,但这可能出错或者数据丢失。chrome
对于如下页面: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被篡改,会很麻烦
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脚本内部能够监听install事件,能够在里面缓存咱们须要的文件,通常分为以下几步
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只能被读取一次,为了下次缓存使用,须要把流克隆一次。
何时才会触发更新?
navigator.serviceWorker.register('/sw.js').then(reg => { // sometime later…
reg.update(); });
更新过程:
在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缓存等。
1.优先访问网络,访问不了再取缓存:http://www.cnblogs.com/hellohello/p/9063241.html#e
2.使用构建工具插件 sw-precache,其中demo里生成的sw.js逻辑大体以下:
附上一段生成好的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"}});
使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阶段来保证同一时间只运行一个版本的sw,若是你不须要这个特色。能够执行self.skipWaiting来跳过这个waiting阶段。
self.addEventListener('install', event => { self.skipWaiting(); event.waitUntil( // caching etc
); });
这会使新的sw只要一进入waiting阶段,就会开始激活(至关于跳过了waiting阶段)。当前页面会受到新的sw控制,而其余页面依旧处于旧的sw控制,至关于有两个版本的sw同时运行了。因此通常不要使用这个函数。
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-v1.js改成./sw-v2.js。这样作v2的sw永远不会生效。
默认的fetch
默认的fetch不会包含用户凭证,如cookie,若是想要包含用户凭证,须要在调用时多加一个配置:
fetch(url, { credentials: 'include' })
处理响应式图片
srcset属性或者picture元素会在运行时选择合适的图片而且发出网络请求。
对于sw,若是想要在install阶段缓存一个图片,能够有如下选择:
事实上,应该选择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
什么是导航请求(navigation requests)?请求的目标是一个文档,如iframe的src请求就是一个导航请求,这种请求比较消耗性能。SPA依赖于dom替换以及H5中的History API来避免导航请求,但初始打开SPA,也仍是一个导航请求
对于移动应用来讲,数据从客户端到服务器的往返时间基本大于整个页面的渲染时间。对于导航请求的优化方式以下:
https://developers.google.com/web/updates/2016/06/sw-readablestreams
https://jakearchibald.com/2016/streams-ftw/
处理导航请求时,把html分红多个部分(一个静态的header与footer,中间是html正文内容,依赖于url),而后再传输这多个部分,这能保证,第一部分能最快显示出来
用于处理不依赖于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; }()); } });
对于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