为网站添加订阅推送!
ApliNi opened this issue · 0 comments
参考资料
知乎 :: 浏览器通知推送服务选型
Mozilla :: Push API
Mozilla :: Service Worker API
Mozilla :: IndexedDB
Mozilla :: ServiceWorkerRegistration.showNotification()
web.dev :: The Web Push Protocol
网络推送 Web Push
网络推送也称订阅推送/Web Push, 与服务器推送和APP消息推送类似, 只要用户订阅了一个站点的 Web Push 服务, 即使用户关闭浏览器, 依然可以收到来自站点推送的消息.
相对于邮件推送和APP消息推送, 这种方式更加简单且成本低. 邮箱推送通常需要用户主动填写邮箱地址, 如果不是重要的消息, 大部分用户是不愿意提供的. 但是网络推送依赖用户浏览器中的本地存储和 Service Worker, 非常容易被清理而失效, 但用于推送文章这些不重要的消息则非常合适.
尝试实现
第一次实现最后失败了, 因为当时参考的资料不够多, 忽略了Push API
, 而是使用轮询查询数据(因为没有足够的资源, 没有使用 WebSocket). 没有实现预期的效果, 这里就当成笔记本了, 万一哪天要看看还能找到().
完整实现部分请看下一个二级标题.
展开收起
Web Push 需要在网站的 Service Worker (它与 web worker 类似, 运行在主线程外) 中进行. 所以先创建一个 Service Worker.
// sw.js
console.log('sw - 开始运行');
// main.js
// 注册 Service Worker
if('serviceWorker' in window.navigator){
let $sw = navigator.serviceWorker.register('sw.js');
$sw.then(function(reg){
console.log('sw', reg);
// 注册完成
});
$sw.catch(function(err){
console.log('sw', err);
});
console.log('成功注册 Service Worker');
}else{
console.log('浏览器不支持 Service Worker');
}
接下来试试主线程与 Service Worker 通讯.
// sw.js
// 接收来自主线程的消息
this.addEventListener('message', function(event){
let $tp = event.data;
console.log('sw - 收到来自主程序的指令', $tp);
});
// main.js
// 连接sw
function connectSW(back = null){
navigator.serviceWorker.getRegistrations().then((e) => {
e.map((w) => {
if(w?.active !== null){
if(back !== null) back(w);
}else{
// 连接失败, 等待5毫秒后重新连接
setTimeout(function(){connectSW(back);}, 5);
}
});
});
);
// 向sw发送消息
connectSW((w) => {
w.active.postMessage('Hello');
});
尝试通过 Service Worker 发送通知.
// sw.js
// 消息通知点击事件
self.addEventListener('notificationclick', function(event){
// 关闭当前的弹窗
event.notification.close();
// 在新窗口打开页面
event.waitUntil(
clients.openWindow('https://ipacel.cc/ApliNi/?')
);
});
// 创建消息通知
function _Notification($title, $body){
// 触发一条通知
self.registration.showNotification($title, {
body: $body,
icon: 'plugins/icon_512_big_b.png',
});
};
// 接收来自主线程的消息
this.addEventListener('message', function(event){
let $tp = event.data;
console.log('sw - 收到来自主程序的指令', $tp);
if($tp.mode === 'testNotification'){
_Notification('这是一条测试通知', '在查询到新数据时, 您将会收到这样的通知, 点击后跳转到网页');
}
});
// main.js
connectSW((w) => {
w.active.postMessage({mode: "testNotification"});
});
尝试调用本地存储, Service Worker 无法调用同步运行的 localStorage
, 所以使用 indexedDB
.
indexedDB 封装
// sw.js
// 初始化 indexedDB
// 封装的代码来源于网络(找不到了), 我对其稍作修改
(function () {
dbObj = {};
/**
* 打开数据库
*/
dbObj.init = function (param, back) {
this.dbName = param.dbName;
this.dbVersion = param.dbVersion;
this.dbStoreName = param.dbStoreName;
// if (!window.indexedDB) {
// alert('浏览器不支持indexedDB')
// }
var request = indexedDB.open(this.dbName, this.dbVersion);
// 打开数据库失败
request.onerror = function (event) {
console.log('数据库连接失败: ', event)
}
// 打开数据库成功
request.onsuccess = function (event) {
// 获取数据对象
dbObj.db = event.target.result;
// console.log('连接数据库成功');
back(event);
}
// if (this.db.objectStoreNames.contains(dbObj.dbStoreName)) {
// console.log('数据仓库已存在');
// }
// 创建数据库
request.onupgradeneeded = function (event) {
dbObj.db = event.target.result;
dbObj.db.createObjectStore(dbObj.dbStoreName, {
// keyPath: "id", //设置主键 设置了内联主键就不可以使用put的第二个参数(这里是个坑)
autoIncrement: true // 自增
});
}
}
dbObj.getStore = function (dbStoreName, mode) {
// 获取事务对象
var ts = dbObj.db.transaction(dbStoreName, mode);
// 通过事务对象去获取对象仓库
return ts.objectStore(dbStoreName);
}
/**
* 添加和修改数据
*/
dbObj.put = function (msg, key, back) {
var store = this.getStore(dbObj.dbStoreName, 'readwrite')
var request = store.put(msg, key);
request.onsuccess = function () {
// if (key)
// console.log('sw - 同步数据库成功');
// else
// console.log('添加成功');
};
request.onerror = function (event) {
//console.log(event);
back(event);
}
}
/**
* 删除数据
*/
dbObj.delete = function (id) {
var store = this.getStore(dbObj.dbStoreName, 'readwrite')
var request = store.delete(id);
request.onsuccess = function () {
//alert('删除成功');
}
}
/**
* 查询数据
*/
dbObj.select = function (key, back) {
var store = this.getStore(dbObj.dbStoreName, 'readwrite')
if (key)
var request = store.get(key);
else
var request = store.getAll();
request.onsuccess = function () {
//console.log(request.result);
back(request.result);
}
}
/**
* 删除表
*/
dbObj.clear = function () {
var store = this.getStore(dbObj.dbStoreName, 'readwrite')
var request = store.clear();
request.onsuccess = function () {
//alert('清除成功');
}
};
//window.dbObj = dbObj;
})();
// 数据库二次封装, 仅用于读写主配置
function db($mode, back = null){
// 向数据库同步数据
if($mode === 'SET'){
dbObj.put({name: 'Config', age: $c}, 1,
(e) => {
if(back !== null) back(e);
}
);
}else
// 从数据库查询数据
if($mode === 'GET'){
dbObj.select(1,
($data) => {
if(back !== null) back($data);
}
);
}else
// 初始化数据库
if($mode === 'INIT'){
dbObj.init({
dbName: 'sw_subscribe',
dbVersion: '1.0',
dbStoreName: 'Config',
}, (e) => {
if(back !== null) back(e);
}
);
}
};
通过轮询查询数据(这里不应该用轮询, 原因上方有提到).
// sw.js
// 测试使用
let $c = {
"UUID": '000000',
"time": 0,
};
main();
function main(){
// 循环
setTimeout(function(){
main();
}, 600000 * 10); // 10分钟 // 600000 * 10
// 任务队列, 循环
console.log('sw - 循环');
tpData();
};
// 请求新的文章
function tpData(){
// 创建配置
let $nowTime = Date.parse(new Date()) / 1000;
let $io = JSON.stringify({
"uuid": $c['UUID'],
"load": {
"mode": "SW",
"data": $c['time'],
},
"time": Date.parse(new Date()) / 1000,
});
let $data = new FormData()
$data.append('io', $io)
;
fetch('cake/FastLoad.php?from=blog_sw', {
method: 'POST', // or 'PUT'
headers: {
//"Content-Type": "multipart/form-data"
},
body: $data,
})
.then(response => response.text())
.catch((error) => {
//console.error('sw-Err: ', error);
})
.then(data => {
console.log('sw - 来自后端的数据: ', data);
if(data == '') return;
data = JSON.parse(data);
// 显示通知
_Notification(data.iM.Title, data.iM.Info);
// 更新客户端时间
$c.time = $nowTime;
// 更新数据库
db('SET');
});
};
正片 使用第三方服务 OneSignal 实现订阅推送
OneSignal/OneSignal-Website-SDK#367
OneSignal 可能会在国内的 Chrome 浏览器上失效, 除非用户使用代理.
OneSignal 是一个实现网络推送的平台, 有免费的版本, 我从其他博客的订阅系统里找到它.
假设您已经完成了 OneSignal 的账号注册.
使用
- 转到
//app.onesignal.com/apps/
创建一个项目. - 打开项目, 点击 Settings(设置).
- 选择 Platforms(平台), 在 App Settings(应用设置) 中点击 Web(网络) 方框的 Activate(启用) 按钮.
- 选择 Typical Site(典型网站)
- 填入网站名称/ 网址和用于显示通知的网站图标(要求256x256分辨率)
- 添加 Subscription Bell(订阅铃)
- 随便
- 关闭 PERSISTENCE(持久显示), 防止消息一直固定在桌面上
- 将 OneSignal 的代码添加到网站.
测试消息推送
- 打开网站, 点击订阅.
- 回到主页面, 点击 Message. 点击 New Message. 选择 New Push.
- 填写信息, 发送, 观察桌面是否出现消息通知.
自动化添加新消息
参考文档: Create notification.
按需添加组件即可.
优化前端加载速度
大概是我的网络不好, 但它加载速度很慢且会造成堵塞, 所以需要让它在网页加载完成后运行.
下载 https://cdn.onesignal.com/sdks/OneSignalSDK.js 中的代码.
在网站的js中添加:
window.onload = function(){
// 下载下来的代码
// "使用" 部分第4步复制的代码
window.OneSignal = window.OneSignal || [];
OneSignal.push(function() {
OneSignal.init({
appId: "xxxxxxxxxxxxxxx",
});
});
};