上一章讲到了ServiceWorker
的基础使用,但是它的功能不仅仅只有这些,还有很多很多,比如消息推送,后台同步,甚至还有WebRTC
,这一章我们来进阶ServiceWorker
。
前期准备
在开始之前,我们先做一下前期的准备,还是使用我上一篇的例子,但是我们需要删除service-worker.js
里面的缓存代码,因为这一章用不上,新看到的小伙伴可以直接用我下面的代码也行:
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<script>
if ('serviceWorker' in navigator) {
// 在调试阶段,如果发现 service worker 没有更新,可以先用下面的代码强制更新
// navigator.serviceWorker.getRegistrations().then((registrations) => {
// for (let registration of registrations) {
// registration.unregister()
// }
// });
navigator.serviceWorker.register('/service-worker.js', {
scope: '/'
}).then((registration) => {
// 先空着
});
}
</script>
</body>
</html>
service-worker.js
里面暂时不写任何代码
消息推送
使用ServiceWorker
是一定要了解消息推送的,因为它是ServiceWorker
的一个重要功能,它可以让你的网站在用户不在的时候,也能够接收到消息,这对于一些即时通讯的网站来说,是非常重要的。
按照正常的消息推送流程,我们需要经历下面几个步骤:
- 注册
ServiceWorker
获得registration
对象 - 通过
registration
对象获得PushManager
对象 - 通过
PushManager
对象订阅消息推送,获得subscription
对象 - 将
subscription
对象发送给服务器,由服务器保存 - 服务器通过
subscription
对象推送消息 ServiceWorker
通过监听push
事件,获得推送的消息ServiceWorker
通过showNotification
方法,显示消息
在讲推送流程之前,我们先来体验一下消息推送的快感。
1. 体验消息推送
啥也不多说,直接上代码:
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/service-worker.js', {
scope: '/'
}).then((registration) => {
// 直接调用showNotification方法,显示消息
registration.showNotification('Hello World');
});
}
上面的方法直接让我们在window
上显示了一个消息,但是这个消息是没有任何交互的,我们可以通过Notification
对象来实现交互:
// 省略注册 ServiceWorker 的代码
self.showNotification('Hello World!', {
body: 'This is a notification!',
icon: 'https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/703a0cee8b0b494eadd27adc61883956~tplv-k3u1fbpfcp-watermark.image?',
actions: [{
action: 'yes',
title: 'Yes'
}, {
action: 'no',
title: 'No'
}]
});
这样我们就可以在消息上添加交互了,但是这个交互是没有任何效果的,我们需要通过监听notificationclick
事件来实现交互:
// service-worker.js
// 监听 notificationclick 事件
self.addEventListener('notificationclick', (event) => {
// 判断点击的是哪个按钮
if (event.action === 'yes') {
console.log('yes');
} else if (event.action === 'no') {
console.log('no');
}
});
这个时候我们就可以在控制台看到点击的是哪个按钮了,接下来我们就来认识一下showNotification
方法。
2. showNotification
showNotification
方法是ServiceWorkerRegistration
对象的一个方法,它可以用来显示消息,直接看函数签名:
interface ServiceWorkerRegistration extends EventTarget {
/**
* 显示消息
* @param title 消息标题
* @param options 消息配置
* options.body 消息内容
* options.icon 消息图标
* options.tag 消息标签
* options.data 消息数据
* options.actions 消息交互按钮
* @returns Promise
*/
showNotification(title: string, options?: NotificationOptions): Promise<Notification>;
}
上面的
options
参数并没有完全列出来,可以参考MDN。
通过上述的实验代码,不知道大家有没有发现一个问题,就是showNotification
方法调用出来的消息是直接显示在window
上的,而不是显示在浏览器的通知栏上。
这是因为消息推送是浏览器行为,作用在window
系统上,而不是作用在网页上。
通过这个案例,有没有产生一个想法,就是大家在写业务代码的时候,遇到消息通知都是自己手写一个弹窗消息,切换个网页就看不到了;
这个时候我们使用showNotification
方法,直接推送到window
上,虽然样式不能自定义,但是是不是逼格更高呢?
而我们的showNotification
方法,其实是通过Notification
对象来实现的,点到为止,继续学习ServiceWorker
。
消息推送流程
为什么要先体验消息推送的效果呢,再来讲消息推送的流程呢?
因为不想打击大家的热情,消息推送因为会被墙,所以在国内没有多少人使用,所以我们先体验一下,再来讲消息推送的流程。
现在我们直接上代码来查看整体流程是怎么样的:
- index.html
if ('serviceWorker' in navigator) {
// 1. 注册`ServiceWorker`获得`registration`对象
// 2. 通过`registration`对象获得`PushManager`对象
// 3. 通过`PushManager`对象订阅消息推送,获得`subscription`对象
// 4. 将`subscription`对象发送给服务器,由服务器保存
// 5. 服务器通过`subscription`对象推送消息
// 6. `ServiceWorker`通过监听`push`事件,获得推送的消息
// 7. `ServiceWorker`通过`showNotification`方法,显示消息
// 1. 注册`ServiceWorker`获得`registration`对象
navigator.serviceWorker.register('/service-worker.js', {
scope: '/'
}).then((registration) => {
// 2. 通过`registration`对象获得`PushManager`对象
const pushManager = registration.pushManager;
// 3. 通过`PushManager`对象订阅消息推送,获得`subscription`对象
pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: ''
}).then((subscription) => {
// 4. 将`subscription`对象发送给服务器,由服务器保存
console.log(subscription);
});
});
}
前面的四步都是在客户端完成的,这里的关键在于第三步,通过PushManager
对象订阅消息推送,获得subscription
对象。
我们先来看一下subscribe
方法的参数和简介:
interface PushSubscriptionOptions {
userVisibleOnly?: boolean;
applicationServerKey?: BufferSource;
}
interface PushManager {
/**
* 订阅消息推送
* @param options
* options.userVisibleOnly: 是否只有用户可见
* options.applicationServerKey: 服务器公钥
* @returns {Promise<PushSubscription>}
*/
subscribe(options?: PushSubscriptionOptions): Promise<PushSubscription>;
}
这里的
options
在文档上显示是可选参数,但是在部分浏览器上是必填参数,所以我们在调用subscribe
方法的时候,需要传入options
参数。
参数介绍:
userVisibleOnly
:布尔值,表示返回的推送订阅将只能被用于对用户可见的消息;在部分浏览器下,如果为false
,则会抛出错误。applicationServerKey
:服务器公钥,用于加密推送消息,这个参数在部分浏览器下是必填参数。
这里的重点就是applicationServerKey
,这个是由我们自己生成的,接下来我们来看一下如何生成。
生成服务器公钥
我们可以通过web-push
这个库来生成服务器公钥,这个库是Google
开发的,旨意是打通多端消息推送的通道,它提供了一些工具,可以帮助我们生成服务器公钥,以及发送推送消息。
web-push
的库在这里:web-push
安装web-push
会被墙,可以通过切换npm
源来解决:
npm config set registry https://registry.npm.taobao.org
安装web-push
:
npm install web-push
安装完成后,我们就可以使用web-push
来生成服务器公钥了:
const webpush = require('web-push');
// 生成服务器公钥
const vapidKeys = webpush.generateVAPIDKeys();
console.log(vapidKeys);
我们也可以全局安装web-push
,然后通过命令行来生成服务器公钥:
npm install web-push -g
web-push generate-vapid-keys --json
上面生成的密钥对会有一个公钥和一个私钥,私钥放在服务器保存,公钥用于注册推送订阅:
pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: 'BHXrxJPYpQSwGMwcN-HprCaU_Po9POIUvqWFLFq9UUNHP5SNJKxk_Io59y8_twMTOuB5SbpbcPBwHFo2kBUj7vQ'
}).then((subscription) => {
// 4. 将`subscription`对象发送给服务器,由服务器保存
console.log(subscription);
});
上面注册成功后,会返回一个subscription
对象,这个对象就是推送订阅,我们需要将这个对象发送给服务器,由服务器保存,用于后续推送消息。
有时候注册过了如果再重复注册会报错,可以通过pushManager.getSubscription()
来获取已经注册的subscription
对象。
pushManager.getSubscription().then((subscription) => {
if (subscription) {
// 这里 subscription 就是已经注册的 subscription 对象
return;
}
pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: 'BHXrxJPYpQSwGMwcN-HprCaU_Po9POIUvqWFLFq9UUNHP5SNJKxk_Io59y8_twMTOuB5SbpbcPBwHFo2kBUj7vQ'
}).then((subscription) => {
console.log(subscription);
});
});
发送推送消息
推送消息我们同样也是通过web-push
来发送,它提供了一个sendNotification()
方法,可以帮助我们发送推送消息。
这里我们通过node
服务来写一个简单的推送消息的接口:
import express from 'express';
import webpush from 'web-push';
const vapidKeys = {
"publicKey": "BHXrxJPYpQSwGMwcN-HprCaU_Po9POIUvqWFLFq9UUNHP5SNJKxk_Io59y8_twMTOuB5SbpbcPBwHFo2kBUj7vQ",
"privateKey": "Yhd4XF08Efh8HNF_8RDJ9VL6pF-Gos-3KOmgyMEUSf8"
}
// 这个是需要在 google cloud platform 中创建的项目id
webpush.setGCMAPIKey('<Your GCM API Key Here>');
// 联系邮箱填自己的邮箱
webpush.setVapidDetails(
'mailto:example@yourdomain.org',
vapidKeys.publicKey,
vapidKeys.privateKey
);
const app = express();
app.get('/push', (req, res) => {
// 这里的 pushSubscription 就是上面注册成功后返回的 subscription 对象
const pushSubscription = {
"endpoint": "https://fcm.googleapis.com/fcm/send/cSAH1Q7Fa6s:APA91bEgYeKNXMSO1rcGAOPzt3L9fMhyjL-zSPV5JfiKwgqtbx_Q4de_8plEY_QViLnhfe6-0fUgdo7Z3Gqpml3zIBSfO6IISDYdF9kzL2h_dbZ_FE_YKbKOG70gMG_A74xwK1vsocCv",
"keys": {
"p256dh": "BAqZaMLZn_rtYeR7WsBLqBWG7uMiOGRyCx2uhOqm0ZaJwDdQac-ubAyRRdLXJVZDOrNe-B3mCTy3g0vHCkeyYyo",
"auth": "fxDt8RtB92KHpQM7HetBUw"
}
};
webpush.sendNotification(pushSubscription, 'Hello world')
.then(result => {
res.send(result);
})
})
app.listen(1701, async () => {
console.log('服务启动成功:http://localhost:1701');
})
这里我们需要申请一个GCMApiKey
,这个是用于验证推送消息的身份,可以在FCM申请。
上面是
web-push
的使用,现在通过firebase
注册的推送消息已经更换了架构,不在使用web-push
,而是使用firebase
,详情可以参考这里;firebase
的使用在官网上有详细的文档,我这里只是演示订阅推送消息的过程,所以使用web-push
来生成密钥对,然后通过web-push
发送推送消息。
Service Worker
监听推送消息
Service Worker
监听推送消息,我们需要在Service Worker
中注册push
事件,然后在push
事件中处理推送消息,也就是最开始提到的showNotification()
方法。
// 注册 push 事件
self.addEventListener('push', function (event) {
const data = event.data.json();
self.registration.showNotification(data.title)
});
现在可以通过访问 http://localhost:1701/push
来发送推送消息了,然后在Service Worker
中通过showNotification()
方法来显示推送消息。
自此一整套的消息推送和接收的流程就走完了,但是为什么要订阅推送消息?为什么我自己的服务器还需要申请啥GCMApiKey
?
这就要说到消息推送的很多知识了,这里不宜过多的展开,有兴趣的可以自行了解。
消息推送的一些知识
我们这里需要了解的是订阅到服务器推送之间的过程倒是是什么样的,为什么我说推送消息会被墙,为什么我们推送消息会不成功?
这里我们直接看下面的流程图:
graph TD
A[订阅推送消息] --> B[FCM服务器]
B --> C1[验证不通过抛出异常]
B --> C[返回subscription对象]
C --> D[将subscription对象保存到服务器]
D --> E[服务器向FCM服务器发送推送消息]
E --> F[FCM服务器将消息直接推送给浏览器]
F --> G[浏览器接收到推送消息调用 push 回调]
F --> G1[浏览器不在线 推送消息会在浏览器上线时通过 notificationcallback 回调]
G --> H[执行 push 事件里面的回调函数]
可以看到服务和浏览器之间是没有直接的通信的,而是通过FCM
服务器来进行通信的;
而FCM
服务器是谷歌的服务器,所以我们不管是订阅还是推送消息都需要通过谷歌的服务器,这就是为什么我们推送消息会被墙的原因;
离线推送
PC
不像移动端,你的网页或者浏览器不会一直开着,所以你的网页或者浏览器不在线的时候,你的推送消息是不会被推送到你的浏览器的,这个时候你的推送消息就会被FCM
服务器保存起来,等你的网页或者浏览器上线的时候,FCM
服务器会通过notificationcallback
回调来推送消息到你的网页或者浏览器。
self.addEventListener('notificationcallback', function (event) {
const data = event.data.json();
self.registration.showNotification(data.title)
});
注意:这个回调是在离线的时候有推送消息,然后在你的网页上线的时候才会触发,如果你的网页一直在线,那么这个回调是不会触发的。
ok,到这里我们已经了解了推送消息的一些知识,我们继续下面的环节。
消息同步
上面讲到了push
事件的监听,现在就还剩下sync
事件的监听了,sync
事件是在Service Worker
中注册的,当我们调用navigator.serviceWorker.ready
方法后,就可以通过registration.sync.register()
方法来注册sync
事件了。
// 注册 sync 事件
navigator.serviceWorker.ready.then((registration) => {
return registration.sync.register('sync');
});
这里我们注册了一个sync
事件,这个事件的名字叫sync
,这个名字是自己定义的,可以随便定义,但是需要注意的是,这个名字是全局唯一的,如果你注册了一个sync
事件,那么下次再注册的时候,就不能再注册一个sync
事件了,否则会报错。
上面注册好了之后,就需要在Service Worker
中监听这个事件了:
// 监听 sync 事件
self.addEventListener('sync', (event) => {
if (event.tag === 'sync') {
event.waitUntil(
// 同步数据
syncData()
);
}
});
相对于push
事件,sync
事件的监听就简单多了,我们只需要判断一下事件的名字,然后执行对应的操作就可以了。
使用sync
事件首选需要注册一个sync
事件,通过registration.sync.register()
方法来注册;
上面使用到
navigator.serviceWorker.ready
是用来确保Service Worker
已经注册成功,它会返回一个不会失败的Promise
对象,如果没有注册成功,那么就会一直处于pending
状态。
在Service Worker
中监听sync
事件,通过self.addEventListener('sync', callback)
方法来监听,这个callback
函数会接收一个event
对象,通过event.tag
来判断事件的名字,然后执行对应的操作。
sync
的使用场景
看名字就知道,sync
事件是用来同步数据的,那么我们什么时候需要同步数据呢?
sync
的使用场景有很多,我下面列举几个:
- 离线数据同步
- 数据备份
- 数据恢复
- 数据同步
通常我们可以使用这个事件来通知Service Worker
同步数据,或者更新缓存,继续拿我之前的百万数据的例子来说;
百万数据很大,在上一篇中我们将数据缓存下来了,但是如果数据发送了变化,那么我们就需要更新缓存,这个时候就可以使用sync
事件来通知Service Worker
更新缓存。
// main.js
// 请求后台查询数据是否有变化
fetch('/api/check').then((res) => {
return res.json();
}).then((data) => {
if (data.hasChange) {
// 有变化,注册 sync 事件
navigator.serviceWorker.ready.then((registration) => {
return registration.sync.register('sync');
});
}
});
// service-worker.js
// 监听 sync 事件
self.addEventListener('sync', (event) => {
if (event.tag === 'sync') {
event.waitUntil(
// 同步数据
syncData()
);
}
});
// 同步数据
function syncData() {
// 请求后台获取数据
return fetch('/getData').then((res) => {
return res.json();
}).then((data) => {
// 更新缓存
caches.open('my-caches').then((cache) => {
cache.put('/getData', new Response(JSON.stringify(data)));
});
});
}
上面的代码中,我们在main.js
中请求后台查询数据是否有变化,如果有变化,那么就注册一个sync
事件,然后在Service Worker
中监听这个事件,当事件触发的时候,就会执行syncData
方法,这个方法中会请求后台获取数据,然后更新缓存。
Service Worker更新
本文的最开始给了一段注释的代码,这段代码是用来更新Service Worker
的,就是下面这段代码:
navigator.serviceWorker.getRegistrations().then((registrations) => {
for (let registration of registrations) {
registration.unregister()
}
});
这段代码并不是用来更新Service Worker
的,而是用来卸载Service Worker
的,所以最开始我注释说明是开发阶段使用的,因为我们在开发阶段,经常会修改Service Worker
的代码,如果不卸载Service Worker
重装很有可能缓存没有清除,导致我们看到的效果和预期的不一样。
但是在生产环境中,我们肯定是不能轻易卸载Service Worker
的,当然Service Worker
也提供了更新的方法,我们可以通过Service Worker
的update
方法来更新Service Worker
。
// 更新 Service Worker
navigator.serviceWorker.getRegistration().then((registration) => {
registration.update();
});
上面的代码中,我们通过getRegistration
方法获取Service Worker
的注册对象,然后调用update
方法来更新Service Worker
。
同样,是否需要更新Service Worker
,也是需要我们自己来判断的,比如我们可以通过Service Worker
的scriptURL
属性来判断Service Worker
的版本,如果版本不一致,那么就更新Service Worker
。
// 更新 Service Worker
navigator.serviceWorker.getRegistration().then((registration) => {
if (registration.scriptURL !== '/service-worker.js?v=1.0.0') {
registration.update();
}
});
通常我们会将Service Worker
的版本号放在scriptURL
中,这样就可以通过scriptURL
来判断Service Worker
的版本了。
Service Worker缓存更新
上面说到了Service Worker
的更新,我们的缓存也是需要更新的,比如我们的Service Worker
更新了,那么我们的缓存也需要更新,这样才能保证我们正常的版本迭代。
缓存在上一章中,是使用了自定义的cacheName
来进行缓存的,其实我们可以用版本号来进行缓存,和Service Worker
保持一致,然后在更新的时候删除对应的缓存:
// 更新 Service Worker
navigator.serviceWorker.getRegistration().then((registration) => {
if (registration.scriptURL !== '/service-worker.js?v=1.0.0') {
registration.update();
// 删除缓存
caches.delete('v1.0.0');
}
});
上面的代码中,我们在更新Service Worker
的时候,也删除了缓存,这样就可以保证我们的缓存和Service Worker
的版本一致了。
总结
本章几乎探索完了Service Worker
的高频常用知识,以目前了解的Service Worker
的知识来说,本章的内容已经足够满足大部分的需求了;
当然这只是一个起点,Service Worker
的知识还有很多,而且并不是只围绕着Service Worker
来讲,还有很多周边的知识;
例如本章讲到的showNotification
方法,其实是Notification
的API,push
方法,其实是Push
的API,还有很多;
本文讲的只是使用,并没有深入,光是使用这一篇文章就已经很长了,如果深入的话,那就更长了;
不多废话,记住这只是一个起点!!!
历史章节和预告
- 🎉🎉🎉 Web Workers 使用秘籍,祝您早日通关前端多线程!
- 🥳🥳🥳Worker中还可以创建多个Worker,打开多线程编程的大门
- ✨✨✨ ServiceWorker 让你的网页拥抱服务端的能力
- 当前章节
- ServiceWorker -> PWA的基石,在线离线都能玩!
- SharedWorker让你多个页面相互通信
- 详解 Web Worker,不再止步于会用!
- 构思中...