service worker一般用的可能不多,但在很多时候却有着不可替代的作用,现在很多文章都只是对其简要介绍,很多细节部分都没有说明,所以就打算写这篇文章较为全面地聊一聊service worker能做什么,怎么做。
兼容性
就目前来说,service worker有着不错的兼容性,除了IE以外的大部分新浏览器都可以较好支持。
如何使用service worker
一个基本的service worker脚本
service worker脚本与普通js脚本的区别主要是因为他们的运行容器不同,在普通页面脚本中,有许多宿主对象可以使用,如与dom相关的window
, document
等,这些是service worker无法使用的,由于没有window
对象,worker中的全局对象变成了self
。其次,service worker被设计成完全异步的,所以需要尽量避免在其中使用需要长时间计算的同步逻辑。
service worker具有自己的生命周期状态,在不同的生命周期中可以做不同事情,主要有以下几个:
- install:准备好缓存等内容
- waiting:如果有一个旧版本的service worker在运行,那么会进入waiting状态,等待页面关闭后再打开才会更新service worker版本
- activate:表示service worker取得了当前页面的控制权,可以监听fetch和返回缓存等了
(在官方的表述中,有installing,installed,activating,activated,redundant几种,但由于它们没有相应的监听事件,所以不使用这种表述)
注:service worker 在不用时会被中止,并在下次有需要时重启
下面来写一个基本的service worker:
self.addEventListener('install', function(event) {
// Perform install steps
event.waitUntil(
new Promise((resolve, reject) => {
setTimeout(resolve, 1000);
})
);
});
self.addEventListener('activate', function(event) {
console.log('activate');
});
install和activate是service worker中的两个生命周期钩子,这两个生命周期里面要做些什么主要取决于需要通过service worker实现的功能,这些会在下面的【service worker能做什么】部分介绍。
在两个事件中都可以使用event.waitUntil
接受一个promise,来告诉浏览器目前还在install
或者activate
过程中,不要关闭worker,否则worker是有可能在任何时候被关闭的,如上面所示,就表示install
过程会持续至少1s。
注册service worker
service worker一般通过navigator.serviceWorker.register()
来注册,它的用例如下:
注:如果你不是在localhost上开发,那么需要https才能使用service worker
(async function regist() {
try {
let registration = await navigator.serviceWorker.register('service-worker.js', {scope: './'});
} catch (e) {
console.error(e);
}
})()
register的第一个参数是脚本文件的路径,第二个参数中的scope(暂时只有这个属性)是这个脚本影响的作用域范围,如果像上面这样写,那么在当前路径/子路径下的页面都会受到影响。如果需要判断一个页面是否受service worker控制,可以检测navigator.serviceWorker.controller
这个属性是否为null或者一个service worker实例。
用过web worker的话可能知道对web worker来说,脚本文件不一定是一个真实的文件路径地址,也可以是通过blob url来创建,就像下面这样:
let content = "console.log('worker is runningggg!')";
let url = URL.createObjectURL(new Blob([content], {type: "text/javascript"}));
let worker = new Worker(url);
但是service worker不能用这种方式来创建,否则会报错:
TypeError: Failed to register a ServiceWorker: The URL protocol of the script ('blob:xxxx') is not supported.
更新service worker
是否更新service worker是首先是由浏览器来决定的,只有当前后两个service worker文件在内容上不同的时候浏览器才会启动新的service worker的install事件。
有几种情况下,页面不会立即通过注册的service worker进行请求:
- 第一次注册service worker时,即使service worker已经activate,页面的请求也不会通过worker,只有在刷新页面后才会通过service worker代理请求
- 当页面存在旧版本的service worker,那么install过后会进入waiting状态,新的service worker会在重新打开页面后(注意不是刷新页面)生效
- 同一个scope中已打开的其它页面,在刷新之前不会受控于service worker
在第二种情况下,可以通过self.skipWaiting()
来跳过waiting,这样的话每次更新service worker文件浏览器都会第一时间激活新的worker,无论有没有重新打开页面,self.skipWaiting()
可以放在任何地方,不过一般会把它放在install事件里面:
self.addEventListener('install', function(event) {
self.skipWaiting();
event.waitUntil(
// do something
);
});
需要注意的是,以上只是激活了service worker,激活不等于能够使用,如果不刷新页面还是不能使用的,如果不想刷新页面(第一、三种情况),可以在service worker中使用clients.claim()替换这种默认行为,如果像下面这样写,那么service worker在activate之后立即生效:
self.addEventListener('activate', event => {
event.waitUntil(clients.claim());
});
在这些页面中没有显式调用register,如果需要在这些页面中知道新的service worker被激活,那么可以监听controllerchange
事件
navigator.serviceWorker.addEventListener('controllerchange', e => console.log(e))
service worker工作流程
将注册和更新过程整合就可以得到完整的service worker工作流程,因为一直没有找到比较详细的service worker整体流程图,所以自己画了一个,有错误还请指出。
有一点需要注意,对于service worker脚本文件,它的http缓存规则与普通文件有所不同,如果设置了强缓存,并且max-age设置小于24小时,那么与普通http缓存无异,但是如果max-age大于24小时,那么service worker文件会在24小时之后强制更新。
与页面间的通信
由于service worker挂载在navigator.serviceWorker.controller
上,所以可以通过它与service worker进行通讯,方法是与web worker相同的postMessage,而在service worker中则是使用self.clients获取受控的页面:
navigator.serviceWorker.controller.postMessage('hello');
navigator.serviceWorker.addEventListener('message', function(e) {
console.log(e.data);
})
// sw.js
self.addEventListener('message', e => {
console.log(e);
// 向特定窗口返回消息
e.source.postMessage('response from service worker')
});
// 向全部窗口发送消息
(async function() {
let cls = await self.clients.matchAll();
cls.forEach(cl => cl.postMessage('message from service worker'));
})();
与其它地方的postMessage类似,也可以通过MessageChannel进行通讯:
const channel = new MessageChannel();
navigator.serviceWorker.controller.postMessage('hello', [channel.port1]);
channel.port2.onmessge = function(e) { console.log(e.data); }
// sw.js
self.addEventListener('message', e => {
e.ports[0].postMessage('message from service worker');
});
service worker能做什么:缓存
service worker强大的缓存功能主要来自于它能够拦截全局的fetch事件,以及能在后台运行的能力等。
既然能够拦截fetch事件,那很容易想到的就是可以把关于浏览器请求的处理都放在一处,也就很大程度上方便了缓存管理,并且由于service worker可以离线使用,使得它成为了PWA的基础。
下面介绍一下如何使用service worker管理缓存
管理缓存
service worker的缓存能力主要与self.caches
对象有关,这是一个CacheStorage对象,在普通页面中也可以使用,但是一般用在service worker中,同样的,CacheStorage只能用在https环境中。
如果预先知道需要缓存什么内容,可以使用caches.addAll()
来预先缓存内容,如果像下面这样写,那么会在service worker安装的时候缓存test.jpg
这个文件:
self.addEventListener('install', function(event) {
event.waitUntil(
caches.open('v1').then(function(cache) {
return cache.addAll([
'/test.jpg',
]);
})
);
});
然后便可以通过拦截fetch
事件来使用缓存了:
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request).then(function(response) {
return response || fetch(event.request);
})
);
});
在上面这种情况下,test.jpg
会从缓存中返回,而其它文件则会从网络下载,但在一些情况下我们不能预知需要缓存的内容,第一次请求之后进行缓存,与caches.addAll()
不同,caches.put()
可以手动将内容放入缓存中,需要注意的是,由于fetch所获得的response内容的读取是一次性的,而实际上我们把response返回给了页面,还把它放入了缓存中,所以要使用response.clone()
创建两个相同的response:
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request).then(function(resp) {
return resp || fetch(event.request).then(function(response) {
return caches.open('v1').then(function(cache) {
cache.put(event.request, response.clone());
return response;
});
});
})
);
});
缓存的更新
由于caches是可以分版本的,所以在更新了新的缓存列表后可以将旧的删除,这一是为了避免缓存混乱,二是为了减少缓存空间占用,每个浏览器都有自己的磁盘空间限制,在容量超出的时候,为了内容的一致性,浏览器一般会删除域下面的所有数据。
const currentCacheVersion = 'v2';
self.addEventListener('activate', function (event) {
event.waitUntil(
caches.keys().then(function (keyList) {
return Promise.all(keyList.map(function (key) {
if (key !== currentCacheVersion) {
return caches.delete(key);
}
}));
})
);
});
手动构建Response
上面所示的response都是通过fetch获的,实际上也可以手动构建Response,这给开发者带来了很大的灵活性,例如在返回缓存时修改header信息等,这里不详细介绍,可以参考MDN。
service worker能做什么:后台同步
这个功能移动端可能应用场景会更多,并且对大部分应用场景来说这或许是一个锦上添花的功能,并且兼容性不是很好:
可以看到只有chromium系的浏览器支持后台同步
后台同步(Background Sync)能做什么
后台同步主要为了将网络请求与页面分离,在用户关闭页面后也可以继续请求,比方说一个请求了很长列表的页面,当列表还未加载完的时候用户就关闭了页面,那么下次打开的时候又需要重新加载,这时就可以使用后台同步。
后台同步分为几个部分
- service worker中监听
sync
事件 - 页面向service worker发送
sync
请求 - 在service worker中触发
sync
事件回调函数,然后进行请求
下面是一个例子:
navigator.serviceWorker.register('/sw.js');
navigator.serviceWorker.ready.then(function(swRegistration) {
return swRegistration.sync.register('sync-user-list');
});
// sw.js
self.addEventListener('sync', function(event) {
if (event.tag == 'sync-user-list') {
event.waitUntil(fetch('./sync?type=userlist'));
}
});
注意要将返回Promise放入event.waitUntil
中,好让浏览器知道什么时候请求完成。
同步的数据如何使用
同步后的数据可以通过多种途径来使用,例如:
- 通过postMessage发送回页面
- 通过Notification给用户发送通知
- 通过一些变量/indexDB存储同步回来的结果(service worker无法使用localStorage等同步存储方式)
以第一种为例:
navigator.serviceWorker.register('/sw.js');
navigator.serviceWorker.ready.then(function(swRegistration) {
return swRegistration.sync.register('sync-user-list');
});
navigator.serviceWorker.addEventListener('message', e => {
const content = e.data;
})
// sw.js
self.addEventListener('sync', function(event) {
if (event.tag == 'sync-user-list') {
event.waitUntil(fetch('./sync?type=userlist').then(async res => {
const content = await res.text();
return clients.matchAll().then(cls => cls[0].postMessage(content));
}));
}
});
以第二种为例:
navigator.serviceWorker.register('/sw.js');
navigator.serviceWorker.ready.then(function(swRegistration) {
return swRegistration.sync.register('sync-user-list');
});
// sw.js
self.addEventListener('sync', function(event) {
if (event.tag == 'sync-user-list') {
event.waitUntil(fetch('./sync?type=userlist').then(async res => {
const content = await res.text();
self.registration.showNotification(content);
}));
}
});
service worker能做什么:Web Push
web push是一种浏览器后台推送通知的方法,它可以实现在关闭页面后继续推送通知的功能原理是通过浏览器的推送服务器向用户推送消息,看看它的兼容性怎么样:
这里特意把一些国内的手机浏览器也放了进来,可以看到,safari是不支持的,但是几个国内的移动端浏览器都支持,需要注意的是,如果使用的是Chrome,那么推送服务使用的是Google的FCM,这个服务国内应该是无法使用的。
这一部分不详细介绍了,在Google Developers上有一篇比较详细的介绍,如果有空的话我可能也会写一篇相关的文章。
DevTools
由于service worker的特性,相比于页面脚本它不是那么好开发调试,下面介绍两个开发的小方法。
调试
如果使用chrome,可以在chrome://inspect/#service-workers 中点击相应service worker的inspect,即可调试
service worker自动更新
上面说到,在service worker文件更新了之后要关闭页面重新打开才能生效,为了避免这个麻烦,可以在开发代码中加入skipWaitinig()
和clients.claim()
,或者在DevTools的Applcation标签页选上Update on reload
,这样每次刷新页面就会自动激活新的service worker。
工程化
由于目前的前端工程几乎都用打包工具进行构建,而service worker一般都要作为一个单独的文件来引入,所以可以在构建工具中做一些配置:
例如使用webpack构建:
...
entry: {
'app': "./src/index.js",
'service-worker': "./src/service-worker.ts",
}
...
另外也有一些工具与service worker相关:
offline-plugin
serviceworker-webpack-plugin 已Archived
由于这些工具我没怎么用过,所以就不介绍了。
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!