参考资料
MDN --- Service Worker API
Service Workers: an Introduction
服务工作线程生命周期
Service Worker Cookbook(收集了Service Worker的一些实践例子)
理解 Service Workers
温馨提示
- 使用限制
Service Worker由于权限很高,只支持https协议或者localhost。
个人认为Github Pages是一个很理想的练习场所。 - 储备知识
Service Worker大量使用Promise,不了解的请移步:Javascript:Promise对象基础
兼容性
一、 生命周期
个人觉得先理解一下它的生命周期很重要!之前查资料的时候,很多文章一上来就监听install
事件、waiting
事件、activate
事件……反正我是一脸懵逼。
1. Parsed
SW是一个JS文件,如果我们要使用一个SW(Service Worker),那么我们需要在我们的js代码中注册它,类似于:
navigator.serviceWorker.register('/sw-1.js')
现在并不需要知道这个方法各个部分的详细含义,只要知道我们现在在为我们的网页注册一个SW就可以了。
可以看到我们传入的参数是一个JS文件的路径,当浏览器执行到这里的时候,就会到相应的路径下载该文件,然后对该脚本进行解析,如果下载或者解析失败,那么这个SW就会被舍弃。
如果解析成功了,那就到了parsed状态。可以进行下面的工作了。
2. Installing
在installing状态中,SW 脚本中的 install 事件被执行。在能够控制客户端之前,install 事件让我们有机会缓存我们需要的所有内容。
比如,我们可以先缓存一张图片,那么当SW控制客户端之后,客户点击该链接的图片,我们就可以用SW捕获请求,直接返回该图片的缓存。
若事件中有 event.waitUntil() 方法,则 installing 事件会一直等到该方法中的 Promise 完成之后才会成功;若 Promise 被拒,则安装失败,Service Worker 直接进入废弃(redundant)状态。
3. Installed / Waiting
如果安装成功,Service Worker 进入installed(waiting)状态。在此状态中,它是一个有效的但尚未激活的 worker。它尚未纳入 document 的控制,确切来说是在等待着从当前 worker 接手。
处于 Waiting 状态的 SW,在以下之一的情况下,会被触发 Activating 状态。
- 当前已无激活状态的 worker
- SW 脚本中的 self.skipWaiting() 方法被调用
- 用户已关闭 SW作用域下的所有页面,从而释放了此前处于激活态的 worker
- 超出指定时间,从而释放此前处于激活态的 worker
4. Activating
处于 activating 状态期间,SW 脚本中的 activate 事件被执行。我们通常在 activate 事件中,清理 cache 中的文件(清除旧Worker的缓存文件)。
SW激活失败,则直接进入废弃(redundant)状态。
5. Activated
如果激活成功,SW 进入激活状态。在此状态中,SW开始接管控制客户端,并可以处理fetch
(捕捉请求)、 push
(消息推送)、 sync
(同步事件)等功能性事件:
// sw.js
self.addEventListener('fetch', function(event) {
// Do stuff with fetch events
});
self.addEventListener('message', function(event) {
// Do stuff with postMessages received from document
});
......
6. Redundant 废弃
Service Worker 可能以下之一的原因而被废弃(redundant)——
- installing 事件失败
- activating 事件失败
- 新的 Service Worker 替换其成为激活态 worker
我们已经理解了SW的生命周期了,那么现在就开始来做一个离线应用。
我们只实现最简单的功能:用户每发送一个http请求,我们就用SW捕获这个请求,然后在缓存里找是否缓存了这个请求对应的响应内容,如果找到了,就把缓存中的内容返回给主页面,否则再发送请求给服务器。
二、 register 注册
首先要注册一个SW,在index.js文件中:
// index.js
if ('serviceWorker' in navigator) {
window.addEventListener('load', function() {
// 注册一个service worker,这个例子中worker的路径是根目录中的,所以这个worker可以缓存这个项目中任意文件。如果目录是‘/js/sw.js‘,那么只能缓存目录'/js'下的文件
// 参数registration存储了本次注册的一些相关信息
navigator.serviceWorker.register('/sw.js').then(function(registration) {
// Registration was successful
// registration.scope 返回的是这个service worker的作用域
console.log('ServiceWorker registration successful with scope: ', registration.scope);
}).catch(function(err) {
// registration failed :(
console.log('ServiceWorker registration failed: ', err);
});
});
}
知识点:
1. window.navigator
返回一个Navigator对象,该对象简单来说就是允许我们获取我们用户代理(浏览器)的一些信息。比如,浏览器的官方名称,浏览器的版本,网络连接状况,设备位置信息等等。
2. navigator.serviceWorker
返回一个 ServiceWorkerContainer对象,该对象允许我们对SW进行注册、删除、更新和通信。
上面的代码中首先判断navigator
是否有serviceWorker
属性(存在的话表示浏览器支持SW),如果存在,那么通过navigator.serviceWorker.register()
(也就是ServiceWorkerContainer.register()
)来注册一个新的SW,.register()
接受一个 路径 作为第一个参数。
ServiceWorkerContainer.register()
返回一个Promise,所以可以用.then()
和.catch()
来进行后续处理。
3. SW的作用域
如果没有指定该SW的作用域,那么它的默认作用域就是其所在的目录。
比如,.register('/sw.js')
中,sw.js在根目录中,所以作用域是整个项目的文件。
如果是这样:.register('/controlled/sw.js')
,sw.js的作用域是/controlled。
我们可以手动为SW指定一个作用域:
.register('service-worker.js', { scope: './controlled' });
3. 为什么在load事件中进行注册
为什么需要在load事件启动呢?因为你要额外启动一个线程,启动之后你可能还会让它去加载资源,这些都是需要占用CPU和带宽的,我们应该保证页面能正常加载完,然后再启动我们的后台线程,不能与正常的页面加载产生竞争,这个在低端移动设备意义比较大。
三、install 安装
我们已经注册好了SW,如果 sw.js 下载并且解析成功,我们的SW就进入安装阶段了,这时候会触发install事件。我们一般在install事件中缓存我们想要缓存的静态资源,供SW控制主页面之后使用:
// sw.js
var CACHE_NAME = 'my-site-cache-v1'; // cache对象的名字
var urlsToCache = [ // 想要缓存的文件的数组
'/',
'/styles/main.css',
'/script/main.js'
];
// 如果所有文件都成功缓存,则将安装成功
self.addEventListener('install', function(event) {
// 执行安装步骤
// ExtendableEvent.waitUntil()方法延长了安装过程,直到其传回的Promise被resolve之后才会安装成功
event.waitUntil(
caches.open(CACHE_NAME)
.then(function(cache) {
// console.log('Opened cache');
return cache.addAll(urlsToCache);
})
);
});
知识点:
1. cache
Cache是允许我们管理缓存的 Request / Response 对象对的接口,可以通过这个接口增删查改 Request / Response 对。
上面代码中cache.addAll(urlsToCache)
表示把数组中的文件都缓存在内存中。
详细了解请戳 : Cache
2. caches
caches是一个CacheStorage对象,提供一个可被访问的命名Cache对象的目录,维护字符串名称到相应Cache对象的映射。
我们可以通过该对象打开某一个特定的Cache对象,或者查看该列表中是否有名字为“xxx”的Cache对象,也可以删除某一个Cache对象。
四、activate 激活
我们的SW已经安装成功了,它可以准备控制客户端并处理 push 和 sync 等功能事件了,这时,我们获得一个 activate 事件。
// sw.js
self.addEventListener("activate", function(event) {
console.log("service worker is active");
});
如果SW安装成功并被激活,那么控制台会打印出"service worker is active"。
如果我们是在更新SW的情况下,此时应该还有一个旧的SW在工作,这时我们的新SW就不会被激活,而是进入了 "Waiting" 状态。
我们需要关闭此网站的所有标签页来关闭旧SW,使新的SW激活。或者手动激活。
那么activate事件可以用来干什么呢?假设我们现在换了一个新的SW,新SW需要缓存的静态资源和旧的不同,那么我们就需要清除旧缓存。
为什么呢?因为一个域能用的缓存空间是有限的,如果没有正确管理缓存数据,导致数据过大,浏览器会帮我们删除数据,那么可能会误删我们想要留在缓存中的数据。
这个以后会详细讲,现在只需要知道activate事件能用来清除旧缓存旧可以了。
五、 fetch事件
现在我们的SW已经激活了,那么可以开始捕获网络请求,来提高网站的性能了。
当网页发出请求的时候,会触发fetch事件。
Service Workers可以监听该事件,'拦截' 请求,并决定返回内容 ———— 是返回缓存的数据,还是发送请求,返回服务器响应的数据。
下面的代码中,SW会检测缓存中是否有用户想要的内容,如果有,就返回缓存中的内容。否则再发送网络请求。
// sw.js
self.addEventListener('fetch', event => {
const { request } = event; // 获取request
const findResponsePromise = caches.open(CACHE_NAME)
// 在match的时候,需要请求的url和header都一致才是相同的资源
// caches.match(event.request, {ignoreVary: true}) 表示只要请求url相同就认为是同一个资源。
.then(cache => cache.match(request)) // 查看cache对象中是否有匹配的项
.then(response => {
if (response) { // 如果response不为空,则返回response,否则发送网络请求
return response;
}
return fetch(request);
});
// event.respondWith 是一个 FetchEvent 对象中的特殊方法,用于将请求的响应发送回浏览器。它接收一个对响应(或网络错误)resolve 后的 Promise 对象作为参数。
event.respondWith(findResponsePromise);
});
箭头函数真的很适合用于Promise对象,省略了一堆的function
、return
关键字,看着舒服多了……
关于缓存策略
不同的应用场景需要使用不同的缓存策略。
比如,小红希望她的网站在在线的时候总是返回缓存中的内容,然后在后台更新缓存;在离线的时候,返回缓存的内容。
比如,小明希望他的网站可以在在线的时候返回最新的响应内容,离线的时候再返回缓存中的内容。
……
如果想要研究一下各种缓存策略,可以参考下面的资料,这里就不详述了,不然文章就成裹脚布了……
The Service Worker Cookbook
离线指南
Service Worker最佳实践
不过,既然标题是“做一个离线网页应用”,那我们就做一个最简单的缓存策略:如果缓存中保存着请求的内容,则返回缓存中的内容,否则,请求新内容,并缓存新内容。
self.addEventListener('fetch', function (event) {
event.respondWith(
caches.match(event.request)
.then(response => {
// Cache hit - return response
if (response) {
return response;
}
// 克隆请求。因为请求是一个“stream”,只能用一次。但我们需要用两次,一次用来缓存,一次给浏览器抓取内容,所以需要克隆
var fetchRequest = event.request.clone();
// 返回请求的内容
return fetch(fetchRequest).then(
response => {
// 检查是否为有效的响应。basic表示同源响应,也就是说,这意味着,对第三方资产的请求不会添加到缓存。
if (!response || response.status !== 200 || response.type !== 'basic') {
return response;
}
// 同request,response是一个“stream”,只能用一次,但我们需要用两次,一次用来缓存一个返回给浏览器,所以需要克隆。
var responseToCache = response.clone();
// 缓存新请求
caches.open(CACHE_NAME)
.then(cache => cache.put(event.request, responseToCache));
return response;
}
);
})
);
});
完成啦!我们简陋的离线应用!
打开页面,看一下缓存中有什么内容:
然后点击“Vue”的链接:
可以看到缓存中多了一张后缀为.png的图片。
SW缓存了我们的新请求!
打开chrome的开发者工具,点击offline,使标签页处于离线状态:
然后,刷新页面。
依然可以访问页面。