在介绍ServiceWorker以前,咱们先来谈谈PWA。PWA (Progressive Web Apps) 是一种 Web App 新模型,并非具体指某一种前沿的技术或者某一个单一的知识点,,这是一个渐进式的 Web App,是经过一系列新的 Web 特性,配合优秀的 UI 交互设计,逐步的加强 Web App 的用户体验。css
在PWA要求的各类能力上,关于离线环境的支持咱们就须要仰赖ServiceWorker。Service workers 本质上充当Web应用程序与浏览器之间的代理服务器,也能够在网络可用时做为浏览器和网络间的代理。它们旨在(除其余以外)使得可以建立有效的离线体验,拦截网络请求并基于网络是否可用以及更新的资源是否驻留在服务器上来采起适当的动做。因为PWA是谷歌提出,那么对ServiceWorker,一样也提出一些能力要求:html
在目前阶段,ServiceWorker的主要能力集中在网络代理和离线缓存上。具体的实现上,能够理解为ServiceWorker是一个能在网页关闭时仍然运行的WebWorker。算法
刚才讲到ServiceWorker拥有离线能力的WebWorker,既然这么强的能力,那就须要好好管理起来。因此咱们要明白ServiceWorker的生命周期,也就是它从建立到销毁的过程。在全部介绍ServiceWorker生命周期的文章中最多见的就是下面这张图。json
整个过程当中一个ServiceWorker会经历:安装、激活、等待、销毁的阶段。但实际上这张图我感受并无清晰的解释ServiceWorker的声明周期,因此我制做了下面这张图。浏览器
这张图把ServiceWorker的声明周期分为了两部分,主线程中的状态和ServiceWorker子线程中的状态。子线程中的代码处在一个单独的模块中,当咱们须要使用ServiceWorker时,按照以下的方式来加载:缓存
if (navigator.serviceWorker != null) { // 使用浏览器特定方法注册一个新的service worker navigator.serviceWorker.register('sw.js') .then(function(registration) { window.registration = registration; console.log('Registered events at scope: ', registration.scope); }); }
这个时候ServiceWorker处于Parsed解析阶段。当解析完成后ServiceWorker处于Installing安装阶段,主线程的registration的installing属性表明正在安装的ServiceWorker实例,同时子线程中会触发install事件,并在install事件中指定缓存资源服务器
var cacheStorageKey = 'minimal-pwa-3'; var cacheList = [ '/', "index.html", "main.css", "e.png", "pwa-fonts.png" ] // 当浏览器解析完sw文件时,serviceworker内部触发install事件 self.addEventListener('install', function(e) { console.log('Cache event!') // 打开一个缓存空间,将相关须要缓存的资源添加到缓存里面 e.waitUntil( caches.open(cacheStorageKey).then(function(cache) { console.log('Adding to Cache:', cacheList) return cache.addAll(cacheList) }) ) })
这里使用了Cache API来将资源缓存起来,同时使用e.waitUntil接手一个Promise来等待资源缓存成功,等到这个Promise状态成功后,ServiceWorker进入installed状态,意味着安装完毕。这时候主线程中返回的registration.waiting属性表明进入installed状态的ServiceWorker。微信
/* In main.js */ navigator.serviceWorker.register('./sw.js').then(function(registration) { if (registration.waiting) { // Service Worker is Waiting } })
然而这个时候并不意味着这个ServiceWorker会立马进入下一个阶段,除非以前没有新的ServiceWorker实例,若是以前已有ServiceWorker,这个版本只是对ServiceWorker进行了更新,那么须要知足以下任意一个条件,新的ServiceWorker才会进入下一个阶段:网络
self.skipWaiting()
这个时候ServiceWorker的生命周期进入Activating阶段,ServiceWorker子线程接收到activate事件:框架
// 若是当前浏览器没有激活的service worker或者已经激活的worker被解雇, // 新的service worker进入active事件 self.addEventListener('activate', function(e) { console.log('Activate event'); console.log('Promise all', Promise, Promise.all); // active事件中一般作一些过时资源释放的工做 var cacheDeletePromises = caches.keys().then(cacheNames => { console.log('cacheNames', cacheNames, cacheNames.map); return Promise.all(cacheNames.map(name => { if (name !== cacheStorageKey) { // 若是资源的key与当前须要缓存的key不一样则释放资源 console.log('caches.delete', caches.delete); var deletePromise = caches.delete(name); console.log('cache delete result: ', deletePromise); return deletePromise; } else { return Promise.resolve(); } })); }); console.log('cacheDeletePromises: ', cacheDeletePromises); e.waitUntil( Promise.all([cacheDeletePromises] ) ) })
这个时候一般作一些缓存清理工做,当e.waitUntil接收的Promise进入成功状态后,ServiceWorker的生命周期则进入activated状态。这个时候主线程中的registration的active属性表明进入activated状态的ServiceWorker实例
/* In main.js */ navigator.serviceWorker.register('./sw.js').then(function(registration) { if (registration.active) { // Service Worker is Active } })
到此一个ServiceWorker正式进入激活状态,能够拦截网络请求了。若是主线程有fetch方式请求资源,那么就能够在ServiceWorker代码中触发fetch事件:
fetch('./data.json')
这时在子线程就会触发fetch事件:
self.addEventListener('fetch', function(e) { console.log('Fetch event ' + cacheStorageKey + ' :', e.request.url); e.respondWith( // 首先判断缓存当中是否已有相同资源 caches.match(e.request).then(function(response) { if (response != null) { // 若是缓存中已有资源则直接使用 // 不然使用fetch API请求新的资源 console.log('Using cache for:', e.request.url) return response } console.log('Fallback to fetch:', e.request.url) return fetch(e.request.url); }) ) })
那么若是在install或者active事件中失败,ServiceWorker则会直接进入Redundant状态,浏览器会释放资源销毁ServiceWorker。
如今若是没有网络进入离线状态,或者资源命中缓存那么就会优先读取缓存的资源:
那么若是咱们在新版本中更新了ServiceWorker子线程代码,当访问网站页面时浏览器获取了新的文件,逐字节比对 /sw.js 文件发现不一样时它会认为有更新启动 更新算法open_in_new,因而会安装新的文件并触发 install 事件。可是此时已经处于激活状态的旧的 Service Worker 还在运行,新的 Service Worker 完成安装后会进入 waiting 状态。直到全部已打开的页面都关闭,旧的 Service Worker 自动中止,新的 Service Worker 才会在接下来从新打开的页面里生效。若是想要当即更新须要在新的代码中作一些处理。首先在install事件中调用self.skipWaiting()方法,而后在active事件中调用self.clients.claim()方法通知各个客户端。
// 当浏览器解析完sw文件时,serviceworker内部触发install事件 self.addEventListener('install', function(e) { debugger; console.log('Cache event!') // 打开一个缓存空间,将相关须要缓存的资源添加到缓存里面 e.waitUntil( caches.open(cacheStorageKey).then(function(cache) { console.log('Adding to Cache:', cacheList) return cache.addAll(cacheList) }).then(function() { console.log('install event open cache ' + cacheStorageKey); console.log('Skip waiting!') return self.skipWaiting(); }) ) }) // 若是当前浏览器没有激活的service worker或者已经激活的worker被解雇, // 新的service worker进入active事件 self.addEventListener('activate', function(e) { debugger; console.log('Activate event'); console.log('Promise all', Promise, Promise.all); // active事件中一般作一些过时资源释放的工做 var cacheDeletePromises = caches.keys().then(cacheNames => { console.log('cacheNames', cacheNames, cacheNames.map); return Promise.all(cacheNames.map(name => { if (name !== cacheStorageKey) { // 若是资源的key与当前须要缓存的key不一样则释放资源 console.log('caches.delete', caches.delete); var deletePromise = caches.delete(name); console.log('cache delete result: ', deletePromise); return deletePromise; } else { return Promise.resolve(); } })); }); console.log('cacheDeletePromises: ', cacheDeletePromises); e.waitUntil( Promise.all([cacheDeletePromises] ).then(() => { console.log('activate event ' + cacheStorageKey); console.log('Clients claims.') return self.clients.claim(); }) ) })
注意这里说的是浏览器获取了新版本的ServiceWorker代码,若是浏览器自己对sw.js进行缓存的话,也不会获得最新代码,因此对sw文件最好配置成cache-control: no-cache或者添加md5。
实际过程当中像咱们刚才把index.html也放到了缓存中,而在咱们的fetch事件中,若是缓存命中那么直接从缓存中取,这就会致使即便咱们的index页面有更新,浏览器获取到的永远也是都是以前的ServiceWorker缓存的index页面,因此有些ServiceWorker框架支持咱们配置资源更新策略,好比咱们能够对主页这种作策略,首先使用网络请求获取资源,若是获取到资源就使用新资源,同时更新缓存,若是没有获取到则使用缓存中的资源。代码以下:
self.addEventListener('fetch', function(e) { console.log('Fetch event ' + cacheStorageKey + ' :', e.request.url); e.respondWith( // 该策略先从网络中获取资源,若是获取失败则再从缓存中读取资源 fetch(e.request.url) .then(function (httpRes) { // 请求失败了,直接返回失败的结果 if (!httpRes || httpRes.status !== 200) { // return httpRes; return caches.match(e.request) } // 请求成功的话,将请求缓存起来。 var responseClone = httpRes.clone(); caches.open(cacheStorageKey).then(function (cache) { return cache.delete(e.request) .then(function() { cache.put(e.request, responseClone); }); }); return httpRes; }) .catch(function(err) { // 无网络状况下从缓存中读取 console.error(err); return caches.match(e.request); }) ) })
ServiceWorker是一项新能力,目前IOS平台对他的支持性并不友好,可是在安卓侧已经没有大问题。而微信平台对它的支持也不错。
依赖项:
错误排查:
同时这里我也为你们录制视频,能够更清晰的看到这些细节。