Web网页在离线情况下你是什么也做不了的,Service Worker是Web前端又一次离线情况下保证用户体验的尝试。

Service workers 本质上充当Web应用程序与浏览器之间的代理服务器,也可以在网络可用时作为浏览器和网络间的代理。

Service workers特点:

  • 独立于页面业务逻辑的脚本,它运行在浏览器的后台拥有自己的线程,它无法直接访问DOM。
  • 常用于缓存不经常变更的静态资源和数据。
  • 拦截页面脚本发起的fetch。对于拦截的fetch,你可以转发也可以自定义对其做出响应。
  • 只能在HTTPS协议的页面中进行注册!

生命周期

Service workers生命周期分为四个部分:注册服务安装服务激活服务更新服务
每个周期都有自己的用途和职责它们是紧密相连的。除了 注册服务 写在页面的业务脚本中,其它生命周期是写在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 eventwindow触发后。

安装服务

在安装服务中我们将不经常变更的静态资源缓存起来。
例子:

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(
// 打开指定版本Service workers 的 cache
caches.open(OFFILINE_CACHE_NAME).then((cache)=>{
// 缓存不重要的资源,即使缓存失败也不影响Service workers 安装
cache.addAll(urlsToPrefetch);
// 缓存重要的资源,只有全部缓存成功,Service Worker安装才顺利完成才能进行激活事件。
return cache.addAll(vipUrlsToPrefetch)
})
)
})

eventExtendableEvent的实例,方法waitUntil()的目的是告诉浏览器只有它所包含的方法执行完成后Service workers才安装完成。
cachesCacheStorage的实例,CacheStorage提供Cache对象的存储机制。而cacheCache的实例。Cache提供缓存RequestResponse对象的机制。

激活服务

在激活服务中我们清楚历史上无用的缓存数据。

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(
// caching etc
);
});

注意:skipWaiting() 意味着新服务工作线程可能会控制使用较旧工作线程加载的页面。 这意味着页面获取的部分数据将由旧服务工作线程处理,而新服务工作线程处理后来获取的数据。如果这会导致问题,则不要使用skipWaiting()

浏览器会自动检查更新,但你也可以手动触发更新:

1
2
3
4
navigator.serviceWorker.register('/sw.js').then(reg => {
// sometime later…
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;
}
//request是stream类型,只能被使用一次。所以我们需要clone一份用于缓存时使用。
var fetchRequest = event.request.clone();

return fetch(fetchRequest).then(
response=>{
if (!response || response.status !== 200 || response.type !== 'basic') {
return response;
}
}
//response 和 request一样也是stream类型,只能被使用一次。所以我们需要clone一份用于缓存时使用。
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可以缓存跨域请求需要做到一下几点:

  1. 首先保证跨域的资源来自安全的HTTPS地址;
  2. 保证跨域资源服务器的response中Access-Control-Allow-Origin中包含当前的页面所在域或为*;
  3. 对于前端页面中的跨域资源的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