推送通知可以很好的与用户进行互动,尤其是当他们关闭了标签页或访问其他网页的时候,体验类似于原生应用,要给用户发送通知,需要首先通过用户授权
Notification
h5 中用来给用户发送通知push API
获取服务器推送,然后通过 Service Workers
调用 Notification
发送通知发送通知步骤
注意:如果用户拒绝订阅,那么不但没法向其推送消息,而且下次用户访问网站的时候也不会被提示
// 注册 serviceWorker
function registerServiceWorker() {
return navigator.serviceWorker
.register('sw.js')
.then(registration => {
console.log('success registe serviceworker');
return registration;
})
.catch(err => console.error('Unable to register service worker.', err));
}
// 注册后显示通知
function execute() {
registerServiceWorker().then(registration => {
registration.showNotification('notification');
});
}
window.addEventListener('load', function() {
// 测试兼容性
// 支持 serviceWorker
if (!('serviceWorker' in navigator)) return;
// 支持 push API
if (!('PushManager' in window)) return;
// 获取通知权限
let promiseChain = new Promise((resolve, reject) => {
return Notification.requestPermission(result => resolve(result));
}).then(result => {
if (result === 'granted') {
// 有权限就直接执行,弹出通知
execute();
} else {
console.log('no permission');
}
});
});
一些配置项不是所有浏览器都支持
// 注册后显示通知
function execute() {
registerServiceWorker().then(registration => {
registration.showNotification('notification', {
body: 'this is body', // 内容
icon: '/center.png', // 图标
badge: '/min.png', // 小图标,手机上展现通知缩略信息时使用
image: '/max.jpg', // 给用户的预览图
vibrate: [100, 200, 200, 300], // 设置震动
requireInteraction: true, // 显示通知直到用户交互
actions: [
// 给通知增加一些按钮
{
action: 'f-action',
title: 'first-action',
icon: '/f.png'
},
{
action: 's-action',
title: 'second-action',
icon: '/s.png'
}
]
});
});
}
self.addEventListener('notificationclick', event => {
if (!event.action) {
// 没有点击在按钮上
console.log('Notification click.');
return;
}
// 对应 registration.showNotification(title, options) 中 options 中设置的按钮
switch (event.action) {
case 'f-action':
console.log('f-action click');
break;
case 's-action':
console.log('s-action click');
break;
default:
console.log('Unknow action click', event.action);
}
});
// 点击关闭通知后触发
self.addEventListener('notificationclose', event => {
console.log('notificationclose');
});
在主程序代码中发送通知
showNotification
并传递数据,而在service-worker
中监听事件处理,并根据数据做出响应
// index.js 主程序中发送通知
registration.showNotification('notification', {
body: 'this is body', // 内容
icon: '/center.png', // 图标
data: {
// 要发送的数据
time: Date.now(),
msg: 'this is index.html data'
}
});
// service worker 通过 event.notification.data 中接收数据然后根据数据做出响应
self.addEventListener('notificationclick', event => {
console.log(event.notification.data); // { time: 1520928976193, msg: "this is index.html data" }
});
self.addEventListener('notificationclick', event => {
const data = event.notification.data;
let promiseChain = Promise.resolve();
if (!event.action) {
// 没有点击在按钮上
console.log('Notification click.');
// data 中有跳转页面信息则安排跳转
if (data && data.page) {
promiseChain = clients.openWindow(data.page);
}
return;
}
event.waitUntil(promiseChain);
});
以 node 为例
const express = require('express');
const bodyParser = require('body-parser');
// 引用一个 node 上消息推送的模块
const webpush = require('web-push');
const app = express();
// 设置 VAPID
webpush.setVapidDetails(
'mailto:[email protected]',
'BAyb_WgaR0L0pODaR7wWkxJi__tWbM1MPBymyRDFEGjtDCWeRYS9EF7yGoCHLdHJi6hikYdg4MuYaK0XoD0qnoY',
'p6YVD7t8HkABoez1CvVJ5bl7BnEdKUu5bSyVjyxMBh0'
);
app.post('/register', (req, res) => {
let endpoint = req.body.endpoint;
// saveRegistrationDetails(endpoint, key, authSecret); 保存到服务器
/*
真实情况可以是从服务器批量获取 pushSubscription 订阅信息,然后推送,
下边这个对象时模拟获取的订阅信息
*/
const pushSubscription = {
endpoint: req.body.endpoint,
keys: {
auth: req.body.authSecret,
p256dh: req.body.key
}
};
const body = 'tttt for registering';
const iconUrl = 'http://localhost:3323/icon.png';
// 消息推送
webpush
.sendNotification(
pushSubscription,
JSON.stringify({
msg: body,
url: 'http://localhost:3323/',
icon: iconUrl
})
)
.then(result => res.sendStatus(201))
.catch(err => console.log(err));
});
客户端需要将自己与其他客户端区分的信息告诉服务器,这样服务器就知道要把信息推送给谁了,这个用于区分的信息就是订阅信息
>
// 订阅通知
let endpoint;
let key;
let authSecret;
let vapidPublicKey =
'BAyb_WgaR0L0pODaR7wWkxJi__tWbM1MPBymyRDFEGjtDCWeRYS9EF7yGoCHLdHJi6hikYdg4MuYaK0XoD0qnoY';
// 一个将 VAPID 钥从 base64 字符串转换成 Uint8 数组的函数
function urlBase64ToUint8Array(base64String) {
const padding = '='.repeat((4 - base64String.length % 4) % 4);
const base64 = (base64String + padding)
.replace(/\-/g, '+')
.replace(/_/g, '/');
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
}
// 注册 serviceWorker
if ('serviceWorker' in navigator) {
navigator.serviceWorker
.register('/sw.js')
.then(registration => {
console.log('successful', registration);
// 查看是否已经存在订阅权限
return registration.pushManager
.getSubscription()
.then(subscription => {
console.log(subscription);
if (subscription) {
return;
}
// 不存在就去申请一下
return registration.pushManager
.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(
vapidPublicKey
)
})
.then(subscription => {
// 申请成功后处理一下秘钥
const rawKey = subscription.getKey
? subscription.getKey('p256dh')
: '';
key = rawKey
? btoa(
String.fromCharCode.apply(
null,
new Uint8Array(rawKey)
)
)
: '';
const rawAuthSecret = subscription.getKey
? subscription.getKey('auth')
: '';
authSecret = rawAuthSecret
? btoa(
String.fromCharCode.apply(
null,
new Uint8Array(rawAuthSecret)
)
)
: '';
// 将订阅信息传给服务器
return fetch('./register', {
method: 'post',
headers: new Headers({
'content-type': 'application/json'
}),
body: JSON.stringify({
endpoint: subscription.endpoint,
key: key,
authSecret: authSecret
})
});
});
});
})
.catch(err => {
throw err;
});
}
Service Workers
的 push 事件,就能获取来自推送服务器的消息registration.showNotification()
方法就可以发出 Notification 了// 接收通知并与之互动
self.addEventListener('push', function(event) {
console.log('get push');
var payload = event.data ? JSON.parse(event.data.text()) : 'no payload';
var title = '测试通知!!';
event.waitUntil(
// 接收到通知后,显示
self.registration.showNotification(title, {
body: payload.msg,
url: payload.url,
icon: payload.icon
})
);
});
// 处理通知的点击事件
self.addEventListener('notificationclick', function(event) {
console.log('notificationclick');
event.waitUntil(
clients.matchAll({ type: 'window' }).then(clientList => {
for (var i = 0; i < clientList.length; i++) {
var client = clientList[i];
if (client.url == '/' && 'focus' in client)
return client.focus();
}
if (clients.openWindow) {
return clients.openWindow('http://localhost:3323');
}
})
);
event.notification.close();
});
注意:经过测试 在
http:localhost:3323
这类的地址下(非 https)firefox 上可以正常使用、Chrome 上pushManager.subscribe()
不能使用(但也不会报错),根据这个网站 了解到,有些功能国内不可用,这个 API 的支持度很低移动端目前不好使
简单了解 HTML5 中的 Web Notification 桌面通知
PWA 文档