Service Worker 简介
Service Workers 本质上是一种能在浏览器后台运行的独立线程,它能够在网页关闭后持续运行,能够拦截网络请求并根据网络是否可用来采取适当的动作、更新来自服务器的的资源,从而实现拦截和加工网络请求、消息推送、静默更新、事件同步等一系列功能,是 PWA 应用的核心技术之一。
与普通 JS 运行环境相比,Service Workers 有如下特点:
- 基于web worker(JavaScript主线程的独立线程,如果执行消耗大量资源的操作也不会堵塞主线程)
- 在web worker的基础上增加了离线缓存的能力
- 本质上充当Web应用程序(服务器)与浏览器之间的代理服务器
- 创建有效的离线体验(将一些不常更新的内容缓存在浏览器,提高访问体验)
- 由事件驱动的,具有生命周期
- 可以访问cache和indexDB
- 支持消息推送
- 并且可以让开发者自己控制管理缓存的内容以及版本
- 无法直接访问 DOM,可以通过 postMessage 接口把数据传递给其他 JS 文件
- 必须在
HTTPS
协议或本地localhost
下运行。 - 更多无限可能
使用场景
Server Worker在PWA之外也有诸多应用,基于它对HTTP请求和响应的强大管理能力,它可以作为多种依赖网络的应用的核心流程管理器。
cache.addAll(urlsToPrefetch.map(function(urlToPrefetch) {
return new Request(urlToPrefetch, { mode: 'no-cors' });
})).then(function() {
console.log('All resources have been fetched and cached.');
});
30X的HTTP状态码尚不支持离线请求重定向, 这是一个已知的issue。建议在官方支持离线重定向前,根据你的使用场景寻找其他方案,
在使用Service Worker代理HTTP的响应体时,务必记住clone response,而不要直接消费掉响应体。 原因是HTTP response是一个 流, 它的内容只能被消费一次。 只要我们仍然希望既能让浏览器正确的获得响应体中的内容,又能是它被缓存或者在Service Worker作内容检查,请不要忘记复制一个响应体。
- 全静态站点
如果一个网站只包含静态数据而无需服务, 我们可以缓存所有的html页面,css样式,脚本和图片等资源,来使得这个页面在初次打开后可以被完全地离线访问。 - 预加载
为了优化首屏渲染,页面上非必要的资源通常被延迟加载直到它们被需要。这类资源使用Server Worker来加载既可以使得在需要被加载时有良好的体验,有不会影响到首屏性能。 - 应变响应
有时候HTTP请求可能会因为不确定因素失败(如服务器离线,网络中断等),此时为用户提供一个应变的响应比如展示上一次成功加载的资源/数据。(例如:实时数据监测)
Service worker可以帮助验证请求是否成功,如果失败就提供应变策略的回复。 - 仿造响应
仿造响应是非常有用。它可以帮助我们隔离部分特定的请求来使用给定的回复,或者我们可以用它来测试一些尚不可用,或者不能稳定重现问题的资源或者REST API. - 窗口缓存
Service Worker来承担缓存数据的责任,页面可以直接使用window.cache来访问缓存。
通过窗口缓存作为媒介可以间接实现service worker向页面的数据传递,也可以将Service Worker用作缓存的生产者而页面作为消费者。 - 在Service Worker中使用fetch API来转发请求,请求中默认不会包含cookie等中的用户认证信息。
如果需要为转发请求附带认证信息, 在fetch请求中添加'credentials'的参数:
fetch(url, {
credentials: 'include'
})
跨域资源默认是不支持缓存的,需要额外参数。
- 如果目标资源支持CORS,在构建请求需要附带参数 {mode: 'cors'} 。
- 如果目标资源不支持CORS或者不确定, 我们可以使用 non-cors模式,
但这会导致"不透明"的响应, 意味着Service Worker不能判断响应中的状态,不透明的结果被缓存后仍被页面消费成non-cors的响应。
生命周期
其生命周期分为首次加载
和更新加载
首次访问页面时候Service Worker
会立即被下载下来并进行尝试安装,安装成功后就会尝试去激活等操作
更新在默认情况下Service Worker
一定会每24小时被下载一次,如果下载的文件是最新文件,那么它就会被重新注册和安装但不会被激活,当不再有页面使用旧的 Service Worker
的时候,它就会被激活。
用户首次访问
service worker
控制的网站或页面时,service worker
会立刻被下载。
之后,在以下情况将会触发更新:
一个前往作用域内页面的导航
在service worker
上的一个事件被触发并且过去 24 小时没有被下载
无论它与现有service worker
不同(字节对比),还是第一次在页面或网站遇到service worker
,如果下载的文件是新的,安装就会尝试进行。
如果这是首次启用service worker
,页面会首先尝试安装,安装成功后它会被激活。
如果现有service worker
已启用,新版本会在后台安装,但不会被激活,这个时序称为worker in waiting
。直到所有已加载的页面不再使用旧的service worker
才会激活新的service worker
。只要页面不再依赖旧的service worker
,新的service worker
会被激活(成为active worker)。
首次加载
- 注册(register)
- 安装(installing)
- 活动(activated)或者异常(error)
- 空闲(idle)
- 拦截(fetch)或终止(terminated)
更新加载
- 更新(update)
- 安装(installing)
- 等待活动(waiting)或者异常(error)
页面加载完成注册
if('serviceWorker' in navigator) {
window.addEventListener('load', function() {
navigator.serviceWorker.register('sw.js', { scope: './' })
.then(function (reg) {
console.log('success', reg);
})
.catch(function (err) {
console.log('fail', err);
});
});
}
安装
self.addEventListener('install', function (event) {
event.waitUntil(
caches.open(CACHE_NAME)
.then(function(cache) {
return cache.addAll(urlsToCache);
})
);
});
- 因为缓存文件需要时间所以可以通过waitUntil来防止缓存未完成就关闭serviceWorker一旦所有文件缓存成功那么serviceWorker就安装成功了,只要一个缓存失败就会导致安装失败waitUntil也会通过内部promise来获取安装事件和是否成功
激活
一旦首次安装成功后或者sw进行更新就会触发activated相对首次安装会直接进入激活状态更新触发会显得比较复杂比如
A为老的sw
B为新的sw
B进入安装更新阶段时候A还在工作状态那么B就会进waiting阶段,只有等到A被terminated后,B才能完全替换A的工作
activate阶段可以做很多有意义的事情,比如更新存储在cache中的key和value,可以清理旧缓存和旧的service worker关联的东西
self.addEventListener('activate', function(event) {
console.log('Service Worker activate');
event.waitUntil(
// 遍历 caches 里所有缓存的 keys 值
caches.keys().then(function() {
return caches.keys().then(function (keys) {
var all = keys.map(function (key) {
if (key.indexOf(CACHE_NAME) !== -1){
console.log('[SW]: Delete cache:' + key);
return caches.delete(key);
}
});
return Promise.all(all);
});
})
);
});
终止
terminated终止状态一般触发条件由下面几种方式:
- 关闭浏览器一段时间
- 手动清除serviceworker
- 在sw安装时直接跳过waiting阶段
拦截
fetch拦截阶段是sw最终要和关键阶段,主要用于拦截代理所有指定的请求,然后进行二次相应的处理操作通过这个阶段我们可以实现很多有意思的操作
输出缓存
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request)
.then(function(response) {
//该fetch请求已经缓存
if (response) {
return response;
}
return fetch(event.request);
}
)
);
});
输出JSON
self.addEventListener("fetch", event => {
const data = {
hello: "world"
}
const json = JSON.stringify(data, null, 2)
return event.respondWith(
new Response(json, {
headers: {
"content-type": "application/json;charset=UTF-8"
}
})
)
})
输出HTML
const html = `<!DOCTYPE html>
<body>
<h1>Hello World</h1>
<p>This markup was generated by a Cloudflare Worker.</p>
</body>`
async function handleRequest(request) {
return new Response(html, {
headers: {
"content-type": "text/html;charset=UTF-8",
},
})
}
addEventListener("fetch", event => {
return event.respondWith(handleRequest(event.request))
})
重定向URL
const destinationURL = "https://www.baidu.com"
const statusCode = 301
async function handleRequest(request) {
return Response.redirect(destinationURL, statusCode)
}
addEventListener("fetch", async event => {
event.respondWith(handleRequest(event.request))
})
缓存一些静态文件
cons CACHE_NAME="my-site-v1"
self.addEventListener('install', function (event) {
let url_list=[
'/',
'/static/xxx.css',
'/static/xxx.js',
'https://www.baidu.com/img/pc_629ee8886a9c20e7f3cb1d2889c3e45d.gif',
'/static/xxx`.txt',
];
event.waitUntil(
caches.open(CACHE_NAME)
.then(function(cache) {
consloe.log("缓存打开成功");
cache.addAll(url_list).then(function(){
consloe.log("所有资源都已获取并缓存");
});
}).catch(function(error) {
console.log('缓存打开失败:', error);
})
);
});
网友评论