完善订阅推送流程
前面已经基本完成了Web推送的基础流程,还有一些细节需要处理。
获取订阅状态
首先用户订阅之后,再次在同一个浏览器打开页面时,需要默认将订阅状态设为勾选状态。通过serviceWorkerRegistration.pushManager.getSubscription()
获取到当前设备的订阅对象之后,还需要向服务端查询该订阅对象是否还有效,修改frontend/index.js
1 | async function checkSubscription(serviceWorkerRegistration) { |
在backend/index.js中添加路由
1 | app.post('/api/get-subscription/', async (req, res) => { |
取消订阅
我们还需要添加取消订阅的功能,当取消勾选后,需要调用subscription.unsubscribe()
取消订阅,并删除订阅对象:
1 | // frontend/index.js |
订阅成功上报
接下来完善前面遗留的订阅上报的流程,修改frontend/service-worker.js:
1 | async function reportNotify(notification) { |
在backend/index.js中添加/api/report-push
路由:
1 | app.post('/api/report-push', async (req, res) => { |
通知框选项
前面的service worker代码中,调用registration.showNotification()
时,我们只是传入了通知的title和body。还有其他选项可以定制通知框的外观和行为。该方法所支持的所有选项可以在这里查看。
icon和badge
通知框可以自定义icon和badge(只有Android的Chrome支持badge选项),修改frontend/service-worker.js.
1 | self.addEventListener('push', (event) => { |
显示效果如下:
actions
通知还支持actions选项(该选项处于试验阶段,大多数平台还不支持):
1 | const actions = [ |
浏览器所支持的最大actions数量可以可以通过window.Notification?.maxActions
查看。
action还支持快捷回复选项,通过将action的type属性设置为text
来实现:
1 | const actions = [ |
以上代码placeholder会修改文本输入框的问题提示。
点击通知框
在上面的例子中,点击通知框什么也没有发生,比较常见的做法是点击通知框时打开某个页面并关闭通知。通知框的点击事件可以添加'notificationclick'
事件监听器。用户点击通知框时,可以点击上文的action按钮或者点击整个通知框,先看如何处理action的点击事件:
1 | function handleActionClick(event) { |
可以看到通过event.action可以获取到点击的action按钮,他的值就是我们定义actions选项时的action字段的值。
如果用户点击了整个通知框,我们还可以帮用户打开某个页面,如果页面已经打开,则将当前焦点聚焦到目标页面上,通过以下代码即可实现:
1 | self.addEventListener('push', (event) => { |
以上代码,我们先通过在option上的data属性来添加自定义的数据,在click事件回调中通过clickedNotification.data获取到需要打开的页面的相对URL,通过new URL(msgUrl, self.location.origin).href;
将其转换为绝对URL,然后调用clients.matchAll()
获取到当前打开的页面。
需要注意的是传给matchAll方法的两个属性type: 'window'
表示匹配的是w indow类型的客户端,includeUncontrolled: true
表示查找所有同域的页面,即使该页面没有被当前的service worker接管,该API的详细信息参考这里,然后通过matchingClient.focus()来聚焦已经打开的页面或者调用clients.openWindow(urlToOpen)打开新的页面。
以上代码同时展示了option的其他选项:
vibrate
、sound
、timestamp
。这些选项大多数还处于试验阶段,其中vibrate用于指定通知框的抖动行为,它的值是一个数字数组,数组的0,2,4…等偶数索引指的是抖动的毫秒数。sound指得是消息提示音
关闭通知框事件
如果我们想在用户点击关闭按钮关闭通知框时,上报用户的点击行为,我们可以监听notificationclose
事件:
1 | self.addEventListener('notificationclose', function (event) { |
合并通知
如果短时间内有多条通知,例如聊天类应用,可以合并多条通知,以避免显示多条通知打扰用户。通过registration.getNotifications()
获取所有未关闭(调用clickedNotification.close()
关闭)的通知。
1 | function mergeNotification(notification) { |
以上代码模拟多个用户发送消息,将相同用户名发送的消息进行合并,代码先查找相同用户发送的未关闭的通知,如果找到修改通知的title和body,并调用currentNotification.close()
,如果未找到,则直接显示当前推送消息。
处理特殊场景
某些时候,当用户正在浏览网站时,可以不显示通知框,或者将该推送消息发送至页面并更新页面,例如聊天类应用。当用户正在聊天时,新消息抵达时直接更新聊天界面即可,无需显示推送消息。
1 | function isClientFocused() { |
代码通过windowClient.focused
判断当前是否有同域窗口正在打开。
然后在push
回调函数中调用该函数来决定是否显示通知。
1 | const promiseChain = isClientFocused().then((clientIsFocused) => { |
或者通过在service worker中调用windowClient.postMessage
将消息通过postMessage发送给主线程。
1 | const promiseChain = isClientFocused().then((clientIsFocused) => { |
在index.js中可以接受service worker线程向主线程发送的消息:
1 | navigator.serviceWorker.addEventListener('message', function (event) { |
以上demo的完整代码点击这里。