面试中经常问service worker
的内容,但是网上没有一篇文章完整的
简介
W3C
组织早在2014
年5
月就提出过Service Worker
这样的一个HTML5 API
,主要用来做持久的离线缓存。
浏览器中js
运行中单一主线程中,同一时间只能做一件事情。 如果一段代码运算太过耗时,就会一直占用浏览器主线程,造成性能下降。基于这个问题,W3C
提出了web Worker
,将耗时太长的任务交给web worker
,然后通过post Message
告诉主线程,主线程通过onMessage
得到结果。
但是web Worker
是临时的,每次运行的结果不能持久的保持下来,下次有复杂的运算,还需要重新计算一次。为了解决这个问题,推出了Service Worker
,相对于web worker
增加了离线缓存能力。
Service Worker
主要有以下特点和功能:
- 离线缓存
- 消息推送
- 请求拦截
如何使用Service Worker
注册
创建一个html
,底部加入注册service Worker
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js', {scope: '/'})
.then(() => {
console.log('Service Worker registration successful')
})
.catch((err) => {
console.log('Service worker registration failed')
})
})
}
这段代码首先是判断service Worker
是否支持,然后支持就调用他的register
方法。
这里的scope
官方文旦上写的是想让Service Worker
控制的内容的子目录,感觉挺迷惑的,例如我的目录是这样的
然后
scope
写成
./config
,那么
service-worker
只会拦截
config
目录下的
fetch
事件,但是在下面提到的
cache.addAll
仍然可以缓存
/
下面的
index.html
的内容。 也可以看到
service worker
不是服务单一页面的,所以需要注意在
service worker
中定义的全局变量。
service worker
基本都是基于
promise
操作,当注册完成以后产生成功和失败回调,最后可以看到结果
安装
注册完service Worker
以后,service worker
就会进行安装,触发install
事件,在install
事件里边可以缓存一些资源,如下:
// 监听service worker的install事件
this.addEventListener('install', (event) => {
// 如果监听到了service worker已经安装成功的话
// 就会调用event.waitUtil回调函数
event
.waitUntil(
// 安装成功后调用CacheStorage缓存,使用之前先通过caches.open()
// 打开对应的缓存空间
caches.open('my-test-cache-v1')
.then((cache) => {
// 通过cache缓存对象的addAll方法添加
return cache.addAll([
'/',
'/index.html'
])
})
)
})
首先是监听install
事件, 调用了waitUntil
,这个方法主要是用于延长事件的寿命,内部需要传入一个Promise
,需要等到内部传入的Promise
变为resolve
。这里主要是为了延长service worker
的installing
周期,等资源缓存完成以后达到installed
生命周期。
具体的缓存内容可以通过Application
的Cache Storage
进行查看
上述代码已经能达到一个资源缓存的效果了,但是没有对缓存资源进行使用,下面编写
Service Worker
使用缓存的代码
请求拦截
Service Worker
具有请求拦截的功能,在页面发送HTTP
请求时,service worker
可以通过fetch
事件进行请求拦截,并且给出自己的响应,所以为了安全,需要使用https
,下面来编写具体的内容:
this.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request)
.then((response) => {
// 如果 service worker有自己的放回,就直接返回,减少一次http请求
if (response) {
return response;
}
// 如果service worker没有返回,那就直接请求真实远程服务
var request = event.request.clone();
return fetch(request)
.then((res) => {
// 请求失败,直接返回失败的结果
if (!res || res.status !== 200) {
return res;
}
// 请求成功的话,将请求缓存起来
var responseClone = res.clone;
caches
.open('my-test-cache-v1')
.then((cache) => {
cache.put(event.request, respondClone)
})
return res;
})
})
)
})
首先监听fetch
事件,然后调用event.respondWith
,这个函数的使用和waitUntil
类似,当传入的Promise resolved
之后,才会把对应的response
返回给浏览器。和cache
中的数据对比,看是否有缓存内容,如果有就使用缓存内容,没有则请求远程服务。请求缓存这个部分需要注意因为Service Worker
会拦截所有的请求,所以需要注意判断哪些内容需要缓存,哪些不需要缓存,例如ajax
就没有必要进行缓存。当我们重新访问index.html
的时候,可以看到index.html
直接从service worker
中获取
service worker
更新
当我们更改缓存策略的时候,service worker
是如何进行更新的呢,主要有下面几种策略
service worker
文件URL
的更新service worker
文件内容发生更改- 用户在无操作
24
小时可以自动更新
更改service worker
的URL
先看一下这种方式是否是可行的,假如第一次访问的时候,在sw1.js
将index.html
进行缓存,这时候我们更改index.html
的内容,发现展现的页面内容并没有发生更改,需要这个时候就需要更改service worker
,我们可以选择把sw1.js
改为sw2.js
,意思是说重新注册一个service worker
,来进行新文件的缓存如下:
我们更改了
service worker
文件的
url
但是在浏览器中发现,
index.html
并没有发生改变,是因为用户访问站点的时候由于
sw1.js
的作用,从缓存中取出的
index.html
引用的仍然是
v1
,并不是我们升级后引用的
v2
,那有人说直接不缓存
html
的内容不就行了吗,那这个应用就失去了离线使用的功能了
更改service worker
文件内容
如果sw.js
内容有更新,当访问网站页面时浏览器获取了新的文件,逐字节比对/sw.js
文件不同时他会认为有更新,于是会安装新的文件并触发install
文件,但是此时已经处于激活状态的旧的service worker
还在运行,新的service worker
完成安装后会进入waiting
状态。直到所有已打开的页面都关闭,旧的service worker
自动更新,新的service worker
才会在接下来重新打开的页面里生效。
例如在sw.js
中加入一个版本号,用于service worker
的更新
var version = '0.0.1';
// 跳过等待,直接进入active
this.addEventListener('install', funciton (event) {
event.waitUntil(self.skipWaiting())
})
this.addEventListener('activate', function (event) {
event.waitUntil(
Promise.all([
// 更新客户端
self.clients.claim(),
// 清理旧版本
caches.keys().then((cacheList) => {
return Promise.all(
cacheList.map((cacheName) => {
if (cacheName !== 'my-test-cache-v1') {
return caches.delete(cacheName)
}
})
)
})
])
)
})
首先我们调用self.skipWaiting
直接跳过installing
阶段,接管老的service worker
,如果不执行这一步你会发现
页面原来有一个
service worker
,需要把当前页面关闭以后新的
service worker
才能生效,所以这里调用
skipWaiting
跳过
installing
,直接接管老的
service worker
,监听
activate
,处理老的缓存。
但是这样做存在一个问题,假如有如下场景:
- 一个页面
index.html
已经安装了old_sw
- 用户打开这个页面,所有网络请求都通过了
old_sw
进行处理,页面加载完成 - 因为
service worker
具有异步安装的特性,一般在浏览器空闲时,他会去执行那句navigator.serviceWorker.register
。这时候浏览器发现了有个new_sw
,于是安装让他等待 - 但是由于
new_sw
在install
阶段有self.skipWaiting()
,所以浏览器强制退出了old_sw
,让new_sw
马上激活并控制页面 - 用户如果在
index.html
后续操作有网络请求,就由new_sw
处理 很明显,同一个页面,前半部分是由old_sw
控制,而后半部分由new_sw
控制。就可能导致两者行为不一致从而出现未知错误
手动更新service worker
和上面同理,都使用一个版本号,进行更新
var version = '1.0.1'
navigator.serviceWorker.register('/sw.js')
.then((reg) => {
if(localStorage.getItem('sw_version') !== version) {
reg.update()
.then(() => {
localStorage.setItem('sw_version', version)
})
}
})
自动更新
Service Worker
的特殊之处除了由浏览器触发更新之外,还应用了特殊的缓存策略: 如果该文件已 24
小时没有更新,当Update
触发时会强制更新。这意味着最坏情况下Service Worker
会每天更新一次。
如何对html
进行缓存
在上述例子中提到对service worker
的更新,资源可以缓存到缓存数据库中。但是在上面的例子中存在一个问题就是我们虽然说对资源进行了缓存,但是需要第二次访问的时候页面展示才是更改后的内容。因为html
不像其他静态资源有一个文件摘要,所以需要对html
文件进行特殊处理。
因为第一次从缓存中中取到的仍然是老的资源,针对这个问题我提了一些想法: 就是对html
格式文件进行特殊处理,如果是有网的情况就从服务端获取新的资源,html
文件的缓存策略一般使用协商缓存,r如果没网就使用缓存的html
内容,这样就能达到每次每次访问到的页面都是最新的,然后也达到了离线使用的效果。具体思路,就是在fetch
中判断是不是有网并且是不是html
格式的:
this.addEventListener('fetch', function (event) {
event.respondWith(
caches.match(event.request).then(function (response) {
var isHtml = /\.html/.test(reponse ? response.url : '');
var onLine = navigator.onLine;
// 如果没网,就全部使用缓存内容
if (!onLine) {
return response;
}
// 如果有网并且不是html,而且response存在,就返回response
if (!isHtml && response) {
return response;
}
// ...
})
这就能保证在有网的时候每次的html
都是最新的。
消息推送
消息推送有着十分广阔的应用场景:
- 新品上架,推送消息给用户,点击即进入商品详情页面
- 用户很久没有进入站点了,推送消息告知这段时间站点的更新 使用推送消息通知,能够让我们的应用像
Native App
一样,提升用户体验
获取授权
在订阅消息前,需要获取用户的授权才能使用消息推送,具体代码如下:
navigator.serviceWorker.register('./sw.js')
.then((reg) => {
res.pushManager.getSubscription().then((subscription) => {
// 如果用户没有订阅
if (!subscription) {
subscribeUser(reg)
} else {
console.log('you have subscribed our notification')
}
})
})
如果已经订阅了,就不会再次弹出下面的弹窗,如果没有订阅就会调用subscribeUser
订阅推送服务
服务端作为消息来源,委托推送服务发送消息给订阅消息的浏览器,所以需要服务器需要保存浏览器的唯一标识。
这里使用web-push
生成一个公钥和私钥,公钥给浏览器通过service worker
生成唯一标识,交给服务端。服务端通过这个唯一标识,对浏览器进行推送消息。
function subscribeUser(registration) {
const applicationServerPublicKey = 'BKzIIoV8RgBqSlOZ5GMle3OY6rZoB-aaoRxldWN8jn5MZOXbtH5tFTchxDRW1jTSLTCOdNPfyk4Yszx0Lk1Clts';
const applicationServerKey = urlB64ToUint8Array(applicationServerPublicKey);
// 订阅
registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey,
})
.then((subscription) => {
$.post({
type: 'post',
url: 'http://localhost:3000/add',
data: {
subscription: JSON.stringify(subscription)
},
success: (res) => {
console.log(res)
}
})
})
.catch((err) => {
console.log('Failed to subscribe the user: ', err)
})
}
applicationServerPublicKey
是刚才用web-push
生成的公钥,然后调用pushManager.subscribe
生成该浏览器的唯一标识,传给后端。这个subscription
的内容长下面这种格式
{"endpoint":"https://fcm.googleapis.com/fcm/send/eAWELgsiTME:APA91bGZ4UwYtr26b0JE8K4sTNNFN8Z8GJ07QgDZHJP9aAqeMsjqiJJaaXd4Ype62vm5v4EjRnD0MuSD5ouBLYy6aT6nU5tWFpp5DjSjPmt_bh-h2Nm5pLo9-xY8H83Q8MHTynY7onKk","expirationTime":null,"keys":{"p256dh":"BLpOkRk1lLRXG8kMP3Yc4D6SUmz3aagln-ysP0lslwJsPA7SQhkmeytSFRCLZKBToBwMe3qRaUAMcJ0R3B1ZND4","auth":"pWaweBbyQqi5lNDR0Rqqew"}}
得到这个信息以后,以后服务器就可以通过这个标识向指定的浏览器进行消息推送
服务端实现推送
这里也需要借助web-push
进行消息推送,由于消息推送需要借助谷歌的FCM
服务,由于我们自身网络的网络就导致无法使用FCM
服务。所以这里没法用谷歌浏览器,查阅了大量的资料后,发现firefox
也能达到同样的效果,所以建议使用firefox
进行实现,也不会存在墙的问题。
const webpush = require('web-push');
// push的数据
const payload = {
title: '一篇新的文章',
body: '点开看看吧',
icon: 'xx', // 图片链接
data: {
url: 'www.baidu.com'
}
}
const vapidKeys = {
publicKey: 'BKzIIoV8RgBqSlOZ5GMle3OY6rZoB-aaoRxldWN8jn5MZOXbtH5tFTchxDRW1jTSLTCOdNPfyk4Yszx0Lk1Clts',
privateKey: 'm5rk4Cann9l5pp7TiLPuNmL2Ho_zmIvgM3wz07EZSSs'
}
const pushSubScription = {
"endpoint": "https://updates.push.services.mozilla.com/wpush/v2/gAAAAABeaki7zwcdJ8r-2PZhwjyeCkHN3GaFAI4NQP8awz3e5svu0xDP6Peanq7iNTRd6S8weseu8JGpJDmLF1V2CcSZRExeWfLt0p5ksuNvCQmYnC4Bwy6wBzUGt-yQRAQMdq9_RKsEnadYfWAQt6LHENfaUr0gKcJJcj1Jb6vGfel-eqjEmjE",
"keys": {
"auth": "QyYLx2m29E-3a5kXzqdIDg",
"p256dh": "BEX1qgwC7MIRw-Vck7wsQPw5M8CIhkQ6thqs5ZwmPkXYy1zF-7sXvKE9hxeZtlm1rHd5lpvpjJf3q26rJje8zUc"
}
}
webpush
.sendNotification(pushSubScription, JSON.stringify(payload), {
vapidDetails: {
subject: 'mailto:18223306087@163.com',
...vapidKeys,
},
})
.then((res) => {
console.log(res);
})
.catch((err) => {
console.log(err)
})
首先先要拿到先前客户端传给我们的subscription
还有先前生成的publickKey
和privateKey
,然后调用webpush.sendNotification
进行主动的消息推送
service Worker
监听push
服务端将信息推送到客户端后,我们需要对push
事件进行监听,然后展示效果
self.addEventListener('push', function (event) {
console.log('push');
// var notificationData = event.data.json();
// var title = notificationData.title;
const title = 'push works';
const options = {
body: 'push is working',
icon: 'resource/logo.png',
badge: 'resource/logo.png'
}
event.waitUntil(self.registration.showNotification(title, options));
})
this.addEventListener('notificationclick', function(event) {
event.notification.close();
event.waitUntil(
clients.openWindow('https://baidu.com')
)
})
这里我们对push
进行监听,然后拿到服务端主动推送的消息内容,这里为了简便就随便做一些内容
然后可以监听
notificationclick
事件,这个主要是点击上面的内容而产生的回调,例如上面例子中写道,如果点击这个通知,就会跳转到百度页面。
这样就看到效果,其实在很多国外的网站都使用了这项技术,但是国内本身因为墙的原因,导致这门技术无法被推广。
页面通信
service worker
不能直接操作DOM
,但是可以通过postMessage
方法和Web
页面进行通信,让页面操作DOM
。
使用postMessage
发送请求
service worker
发送数据: 在sw.js
中向页面发消息,使用client.postMessage
,实例代码如下:
this.clients.matchAll()
.then(function (clients){
if (clients && clients.length) {
clients.forEach((client) => {
// 发送数据
client.postMessage('sw update')
})
}
})
页面发送数据:在主页面中使用navigator.serviceWorker.controller.postMessage()
进行数据发送
if (navigator.serviceWorker.controller) {
navigator.serviceWorker.controller.postMessage('home update')
}
接收数据
在service worker
中接受主页面发来的信息,示例如下:
self.addEventListener('message', function (event) {
console.log(event.data); // home update
});
在主页面中接受service worker
发来的信息,示例如下:
navigator.serviceWorker.addEventListener('message', function (event) {
console.log(event.data)
});
参考资料
PWA文档
understanding-service-worker-scope
谨慎处理 Service Worker 的更新
向网络应用添加推送通知
今天的文章Service Worker简易教程分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/20070.html