Web网页在离线情况下你是什么也做不了的,Service Worker是Web前端又一次离线情况下保证用户体验的尝试。
Service workers 本质上充当Web应用程序与浏览器之间的代理服务器,也可以在网络可用时作为浏览器和网络间的代理。
Service workers特点:
- 独立于页面业务逻辑的脚本,它运行在浏览器的后台拥有自己的线程,它无法直接访问DOM。
- 常用于缓存不经常变更的静态资源和数据。
- 拦截页面脚本发起的
fetch
。对于拦截的fetch
,你可以转发也可以自定义对其做出响应。
- 只能在HTTPS协议的页面中进行注册!
生命周期
Service workers生命周期分为四个部分:注册服务 ,安装服务 ,激活服务 ,更新服务 。
每个周期都有自己的用途和职责它们是紧密相连的。除了 注册服务 写在页面的业务脚本中,其它生命周期是写在Service workers脚本中。
注册服务
Service workers的注册是负责页面业务的脚本中进行的。它的目的是告诉浏览器Service workers脚本位置并加载它。
例子:
1 2 3 4 5 6 7 8 9 10 11 12
| window.addEventListener('load', ()=>{ if ('serviceWorker' in navigator ) { navigator.serviceWorker.register('/sw-test/sw.js',{scope:'/sw-test/'}).then( (reg)=> { console.log('Registration successed. Scope is '+ reg.scope) } ).catch ((error)=>{ console.log('Registration failed with'+ error) }) } })
|
实际应用场景中我们应该避免注册Service workers阻塞了界面和业务逻辑的加载。所以上面例子我们把注册Service workers放在 load event
window触发后。
安装服务
在安装服务中我们将不经常变更的静态资源缓存起来。
例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| const OFFILINE_PREFIX = 'offline_page_'; const CACHE_VERSION = 'v1.0'; const OFFILINE_CACHE_NAME = OFFILINE_PREFIX+CACHE_VERSION;
var vipUrlsToPrefetch = [ './index.html' ]; var urlsToPrefetch = [ './images/banner.png', './css/main.css', './js/main.js' ] this.addEventListener('install', event =>{ event.waitUntil( caches.open(OFFILINE_CACHE_NAME).then((cache)=>{ cache.addAll(urlsToPrefetch); return cache.addAll(vipUrlsToPrefetch) }) ) })
|
event
是ExtendableEvent
的实例,方法waitUntil()
的目的是告诉浏览器只有它所包含的方法执行完成后Service workers才安装完成。
caches
是CacheStorage的实例,CacheStorage
提供Cache
对象的存储机制。而cache
是Cache
的实例。Cache
提供缓存Request
和Response
对象的机制。
激活服务
在激活服务中我们清楚历史上无用的缓存数据。
1 2 3 4 5 6 7 8 9 10 11
| this.addEventListener('activate', event =>{ event.waitUntil(caches.keys().then(cacheNames => { return Promise.all(cacheNames.map((cacheName)=>{ if (cacheName !== OFFLINE_CACHE_NAME && cacheName.indexOf(OFFLINE_CACHE_PREFIX) != -1 ) { return caches.delete(cacheName); } })) })) })
|
caches.keys
会取得当前域名下所有的cache,可能同域名下其它的path也使用了Service Worker进行资源缓存,在删除时需要考虑清楚。
更新服务
当浏览器检测到新加载的Service workers脚本于已有的Service workers脚本字节不同,浏览器将考虑更换Service workers了。新的Service workers会进行安装,但不会激活因为还有客户端还是被旧Service workers控制着。只有当旧Service worker没有控制客户端后,新开的客户端才会是新Service workers控制。
安装成功但没有被激活的新Service workders处于等待期。可以通过skipWaiting()
方法提过等待期。
例子:
1 2 3 4 5 6 7
| self.addEventListener('install', event => { self.skipWaiting();
event.waitUntil( ); });
|
注意:skipWaiting() 意味着新服务工作线程可能会控制使用较旧工作线程加载的页面。 这意味着页面获取的部分数据将由旧服务工作线程处理,而新服务工作线程处理后来获取的数据。如果这会导致问题,则不要使用skipWaiting()
。
浏览器会自动检查更新,但你也可以手动触发更新:
1 2 3 4
| navigator.serviceWorker.register('/sw.js').then(reg => { reg.update(); });
|
缓存资源
Service workers中我们可以拦截业客户端发起的fetch
请求。对于拦截的请求我们转发继续请求,如果之前缓存了之前这个请求的返回值,可以直接从缓存中将返回值取出传给客户端。
缓存优先
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| self.addEventListener('fetch', event => { event.respondWith( caches.match(event.request) .then((response)) { if (response) { return response; } var fetchRequest = event.request.clone(); return fetch(fetchRequest).then( response=>{ if (!response || response.status !== 200 || response.type !== 'basic') { return response; } } var responseToCache = response.clone(); caches.open(CACHE_NAME) .then(cache=>{ cache.put(event.request, responseToCache); }); return response; ) } ) })
|
我们先检查客户端发起的请求在本地是否有缓存,如果没有就进行发起网络请求并将返回值缓存起来。
仅使用缓存
1 2 3 4 5 6 7
| this.addEventListener('fetch', event => { event.respondWith( caches.open(CACHE_NAME).then(cache=> { return cache.match(event.request.url); }) ) })
|
仅去匹配缓存中资源。如果没有资源则客户端显示资源加载失败。
仅使用网络缓存
1 2 3
| this.addEventListener('fetch', event=>{ event.respondWith(fetch(event.request)) })
|
仅转发客户端的请求且不做缓存。适用于动态资源、实时性要求高的场景。
网络优先
1 2 3 4 5 6 7 8 9 10
| this.addEventListener('fetch', event => { var fetchRequest = event.request.clone(); event.respondWith( fetch(event.request).catch(()=>{ return caches.open(CACHE_NAME).then(cache=>{ return cache.match(fetchRequest); }) }) ) })
|
先通过网络请求资源,如果网络加载失败再匹配本地资源。目的是为了展示最新的数据,对实时性要求比较高但又能够带来良好体验的应用,比如天气类型应用。
速度优先
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| function promiseAny (promises) { return new Primise ( (resolve, reject) => { promises = promises.map( p => Promise.resolve(p)); promises.forEach(p=>p.then(resolve)); promises.reduce( (a, b) => a.catch(()=>b)) .catch(() => reject(Error("All failed"))) }); } this.addEventListener('fetch', (event) => { event.respondWith( promiseAny( primiseAny[ caches.open(CACHE_NAME).then(cache=>{ return cache.match(event.request) }).then( response => { if (response) return response; return fetch(event.request); }), fetch(event.request); ] ) ) })
|
同时发起读取本地缓存匹配以及网络请求,谁先返回使用谁。该方案适用于性能要求比较高的站点,缩短了缓存优先策略中可能缓存中没有资源再折回网络的时间消耗。
跨域缓存
Service workers可以拦截它管辖范围内的基本上所有请求,跨域资源也不例外。为了保证Service workers可以缓存跨域请求需要做到一下几点:
- 首先保证跨域的资源来自安全的HTTPS地址;
- 保证跨域资源服务器的response中Access-Control-Allow-Origin中包含当前的页面所在域或为*;
- 对于前端页面中的跨域资源的url可以附带 “cors=1”参数,以便Service Worker在拦截之后可以判断跨域请求从而重新进行组装cors请求。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| var addToCache = function (resp) { return fetch (req.clone()).then( response => { var cacheResponse = response.clone(); if (response.status ! == 200 || (response.type !== 'basic' && response.type !== 'cors')) { return response; }; caches.open(CACHE_NAME).then( cache => { cache.put(req.clone(), cacheResponse) }); return response; }) }
this.addEventListener('fetch', event => { var req, url; url = event.request.url; if (url.indexOf('cors=1') !== -1) { req = new Request(url, {mode:'cors'}); } else { req = event.request.clone(); }
event.respondWith( caches.open(CACHE_NAME).then( cache => { return cache.match(event.request) }).then(response => { if (response) return response; return addToCache(req); }) ) })
|
参考
https://developers.google.com/web/fundamentals/primers/service-workers/?hl=zh-cn
https://developer.mozilla.org/zh-CN/docs/Web/API/Service_Worker_API
https://x5.tencent.com/tbs/guide/serviceworker.html
https://developer.mozilla.org/zh-CN/docs/Web/API/Service_Worker_API/Using_Service_Workers