今天在用create-react-app构建项目的时候发现多了一个serviceWorker做离线缓存的选项。
(1)什么是Service Worker
Service Worker
是一个注册在指定源和路径下的事件驱动 worker。它采用JavaScript控制关联的页面或者网站,拦截并修改访问和资源请求,细粒度地缓存资源。你可以完全控制应用在特定情形(最常见的情形是离线)下的表现。
Service Worker
运行在worker上下文,因此它不能访问DOM。相对于驱动应用的主JavaScript线程,它运行在其他线程中,所以不会造成阻塞。它设计为完全异步,同步API(如XHR和localStorage)不能在service worker中使用。
出于安全考量,Service workers只能由HTTPS承载,毕竟修改网络请求的能力暴露给中间人攻击会非常危险。
为什么不直接用web worker?
Web Worker 是临时的,每次做的事情的结果还不能被持久存下来,如果下次有同样的复杂操作,还得费时间的重新来一遍。那我们能不能有一个Worker 是一直持久存在的,并且随时准备接受主线程的命令。基于这样的需求推出了最初版本的 Service Worker
,Service Worker
在 Web Worker
的基础上加上了持久离线缓存能力。
在 Service Worker
之前也有一个在 HTML5 上做离线缓存的 API 叫 AppCache
, 但是 AppCache
就是一坨shit(别人说的)。W3C 决定 AppCache
仍然保留在 HTML 5.0 Recommendation 中,在 HTML 后续版本中移除。
(2)使用Service Worker
上面说到 Service Worker
必须使用https协议,但本地开发弄个https协议是很麻烦的。好在还算人性化,Service Worker在http://localhost或者http://127.0.0.1这种本地环境下的时候是可以跑起来的。
这里我推荐一个经常使用的npm工具,用来开启本地服务:live-server
。
1.注册worker
就像使用web worker那样,先创建两个文件serviceWorker.html、serviceWorker.js
。
serviceWorker.html:
<script type="text/javascript">
if ('serviceWorker' in navigator) {
window.addEventListener('load', function () {
navigator.serviceWorker.register('./serviceWorker.js', {scope: './'})
.then(function (registration) {
console.log('ServiceWorker registration successful with scope: ', registration.scope);
})
.catch(function (err) {
console.log('ServiceWorker registration failed: ', err);
});
});
}
</script>
- 首先在注册之前确保浏览器是支持 service worker 的。
- 接着,我们使用
ServiceWorkerContainer.register()
函数来注册serviceWorker.js
。 -
scope
参数是可选的,可以用来指定你想让service worker
控制的内容的子目录。 在这个例子里,我们指定了'/'
,表示 根网域下的所有内容。这也是默认值。 - 函数返回一个Promise,注册成功时执行
.then()
,失败时执行.catch()
。
2.安装worker
serviceWorker.js:
this.addEventListener('install', function (event) {
event.waitUntil(
caches.open('v1').then(function (cache) {
return cache.addAll([
'./index.html'
]);
})
);
});
install
事件会在worker安装完成后触发。install
事件一般是被用来设置你的浏览器的离线缓存能力。这里就用到了离线缓存Cache了。
这里我们 新增了一个
install
事件监听器,接着event对象调用了ExtendableEvent.waitUntil()
方法——这会确保Service Worker 不会在waitUntil()
里面的代码执行完毕之前就安装完成。(这个方法扩展了事件的生命周期。在service worker线程中,延长事件的寿命从而阻止浏览器在事件中的异步操作完成之前终止service worker线程。)在
waitUntil()
里,我们使用了caches.open()
方法来创建了一个叫做 v1 的新的缓存,这是我们的站点资源缓存的第一个版本。它返回了一个创建缓存的 promise。当它 resolved的时候,我们接着会调用创建的缓存上的addAll()
方法 ,这个方法的参数是一个数组,每个元素是相对于根域的 URL,这些 URL 就是你要缓存的资源的路径。(这里我就缓存了一个index.html)如果 promise 被 rejected,安装就会失败,这个 worker 不会做任何事情。
当启动服务运行后,可以看到:
打开缓存,也可以看到index.html被缓存了:
(3)fetch API
现在已经将index.html缓存了,你要告诉 service worker 怎么用这些缓存。这就要用到 fetch 事件。
给 service worker 添加一个 fetch 的事件监听器,接着调用 event 上的 respondWith()
方法来劫持我们的 HTTP 请求,然后你用可以用自己的方式来更新。
this.addEventListener('fetch', function(event) {
event.respondWith(
// magic goes here
);
});
这里最简单的做法就是,原原本本的返回与url匹配的缓存资源。
this.addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request)
);
});
caches.match(event.request)
允许我们将网络请求的资源和 cache 里可获取的资源进行匹配,查看是否缓存中有相应的资源。这个匹配通过 url 和各种header进行,就像正常的 http 请求一样。
原来的翻译多了个;号,难怪我复制后会报错,现在已经被我改过来了。
现在,就可以打开控制台断开你的网络,离线。再次访问index.html
。
可以发现还是ok的。
上面是最简单的例子,当缓存中没有匹配的资源时,或当缓存中没有匹配的资源,同时网络也不可用时。更多详情就直接看MDN吧。