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