我已经按照本教程实现了推送和通知API。它在Mac OS的Chrome上运行得非常好。
但现在我正试图让它在iOS(16.4.3)的Safari上工作。我已经将我的应用程序添加到主屏幕,使其成为PWA。
我有一个按钮#enable-notifications
执行以下代码:我的应用的JS代码
document.getElementById("enable-notifications").addEventListener("click", () => {
main();
});
const check = () => {
if (!('serviceWorker' in navigator)) {
throw new Error('No Service Worker support!')
}
if (!('PushManager' in window)) {
throw new Error('No Push API Support!')
}
}
const registerServiceWorker = async () => {
const swRegistration = await navigator.serviceWorker.register('/assets/js/order-dashboard/serviceworker.js');
return swRegistration;
}
const requestNotificationPermission = async () => {
Promise.resolve(Notification.requestPermission()).then(function(permission) {
if (permission !== 'granted') {
throw new Error('Permission not granted for Notification')
}
});
}
const main = async () => {
check();
const swRegistration = await registerServiceWorker();
const permission = await requestNotificationPermission();
}
const showLocalNotification = (title, body, swRegistration) => {
const options = {
body,
};
swRegistration.showNotification(title, options);
}
// urlB64ToUint8Array is a magic function that will encode the base64 public key
// to Array buffer which is needed by the subscription option
const urlB64ToUint8Array = base64String => {
const padding = '='.repeat((4 - (base64String.length % 4)) % 4)
const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/')
const rawData = atob(base64)
const outputArray = new Uint8Array(rawData.length)
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i)
}
return outputArray
}
// saveSubscription saves the subscription to the backend
const saveSubscription = async subscription => {
const SERVER_URL = 'https://good-months-invite-109-132-150-239.loca.lt/save-subscription'
const response = await fetch(SERVER_URL, {
method: 'post',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(subscription),
})
console.log(response);
return response.json()
}
self.addEventListener('activate', async () => {
// This will be called only once when the service worker is activated.
try {
const applicationServerKey = urlB64ToUint8Array(
'BDLVKNq32B-Dr3HRd4wQ2oNZL9mw5JAGhB1XGCdKlDE9_KDEw7uTOLuPKH-374RRolaa0rr7UyfrJd7tvRvp304'
)
const options = { applicationServerKey, userVisibleOnly: true }
const subscription = await self.registration.pushManager.subscribe(options)
const response = await saveSubscription(subscription)
console.log(response)
} catch (err) {
console.log('Error', err)
}
})
self.addEventListener("push", function(event) {
if (event.data) {
console.log("Push event!!! ", event.data.text());
showLocalNotification("Yolo", event.data.text(), self.registration);
} else {
console.log("Push event but no data");
}
});
const showLocalNotification = (title, body, swRegistration) => {
const options = {
body
// here you can add more properties like icon, image, vibrate, etc.
};
swRegistration.showNotification(title, options);
};
这是我的node.js后台:
const express = require('express')
const cors = require('cors')
const bodyParser = require('body-parser')
const webpush = require('web-push')
const app = express()
app.use(cors())
app.use(bodyParser.json())
const port = 4000
app.get('/', (req, res) => res.send('Hello World!'))
const dummyDb = { subscription: null } //dummy in memory store
const saveToDatabase = async subscription => {
// Since this is a demo app, I am going to save this in a dummy in memory store. Do not do this in your apps.
// Here you should be writing your db logic to save it.
dummyDb.subscription = subscription
}
// The new /save-subscription endpoint
app.post('/save-subscription', async (req, res) => {
const subscription = req.body
await saveToDatabase(subscription) //Method to save the subscription to Database
console.log("saved");
res.json({ message: 'success' })
})
const vapidKeys = {
publicKey:
'BDLVKNq32B-Dr3HRd4wQ2oNZL9mw5JAGhB1XGCdKlDE9_KDEw7uTOLuPKH-374RRolaa0rr7UyfrJd7tvRvp304',
privateKey: 'BGbNIt2twl1XsbDHPNe_w6FrKsWcZrys6anByEKyCGo',
}
//setting our previously generated VAPID keys
webpush.setVapidDetails(
'mailto:myuserid@email.com',
vapidKeys.publicKey,
vapidKeys.privateKey
)
//function to send the notification to the subscribed device
const sendNotification = (subscription, dataToSend) => {
webpush.sendNotification(subscription, dataToSend)
}
//route to test send notification
app.get('/send-notification', (req, res) => {
const subscription = dummyDb.subscription //get subscription from your databse here.
const message = 'Hello World'
sendNotification(subscription, message)
res.json({ message: 'message sent' })
})
app.listen(port, () => console.log(`Example app listening on port ${port}!`))
当我在iOS上点击#enable-notifications
时,我得到了允许通知的弹出窗口。但是什么也没发生。我的后端也没有被调用。
这里有什么问题吗?
编辑:我已经在Safari MacOS(工作),Chrome MacOS(工作),Chrome Windows(其他设备,工作),…只有Safari iOS不能用
编辑:navigator.serviceWorker.controller
返回null
编辑:更新了js代码。
document.getElementById("enable-notifications").addEventListener("click", (event) => {
event.preventDefault()
askNotificationPermission().then(alert).then(registerServiceWorker);
})
function askNotificationPermission() {
return new Promise((resolve, reject) => {
if (checkNotificationPromise()) {
Notification.requestPermission().then(resolve);
} else {
Notification.requestPermission(resolve)
}
})
}
function checkNotificationPromise() {
try {
Notification.requestPermission().then();
} catch(e) {
return false;
}
return true;
}
const registerServiceWorker = async () => {
// alert("registering service worker");
const swRegistration = await navigator.serviceWorker.register('/assets/js/order-dashboard/serviceworker.js');
// alert(swRegistration.active);
console.log(swRegistration);
return swRegistration;
}
现在接受通知的消息被提示两次+ serviceworker甚至不再联系我在其他浏览器上的服务器了…
可能与这里列出的问题有关https://developer.apple.com/forums/thread/725619?answerId=749431022#749431022
基本上,无论用户选择什么,Notification.requestPermission()
总是与denied
解决。您可以通过使用alert(permission)
来检查是否存在这种情况。
你的代码也可以简化,当然是这样:
Notification.requestPermission().then(function(permission) {
if (permission !== 'granted') {
throw new Error('Permission not granted for Notification')
}
})
不需要使用额外的Promise.resolve
调用。
刚刚注意到你的代码使用Notification.requestPermission
承诺版本,但其中一些浏览器和Safari使用旧的回调样式版本。
这是跨浏览器兼容的代码:
document.getElementById("enable-notifications").addEventListener("click", (event) => {
event.preventDefault()
askNotificationPermission().then(alert)
})
function askNotificationPermission() {
return new Promise((resolve, reject) => {
if (checkNotificationPromise()) {
Notification.requestPermission().then(resolve)
} else {
Notification.requestPermission(resolve)
}
})
}
function checkNotificationPromise() {
try {
Notification.requestPermission().then();
} catch(e) {
return false;
}
return true;
}
现在检查警报显示给您的内容。
更新# 2
不确定PushManager.subscribe
函数是否甚至在ServiceWorker中工作,就像你试图做的那样,因为它只适用于用户手势(例如,在按钮单击上)。不确定ServiceWorker.activate
事件是否可以这样分类。
基本上流应该是这样的:
- 注册
ServiceWorker
或通过getRegistration
函数获得其注册-如果成功则继续 - 在某些按钮/链接上点击-请求
requestPermission
通知权限,以确保用户在第一次使用时将显示通知权限对话框,下次只需检查权限是否"授予";(然而,就像之前在iOS上所提到的那样,它似乎总是被"拒绝";所以基本上忽略它) - 尝试订阅
PushManager
-如果它成功,你得到所有的订阅细节,你可以发送到后端,然后注册到"推送">
你的流程是混合和匹配当前。重新组织你的流程,并添加警告语句,看看顺序是否正确,是否每个承诺都像你期望的那样解决。
更新# 3
不知道为什么通知弹出框显示两次,但你有一个问题是,你正在传递undefined
到第二个Promise.then
函数,因为alert
总是返回undefined
的值。以下是相关代码:
askNotificationPermission().then(alert).then(registerServiceWorker);
此alert
仅建议查看权限状态的值。无论如何,更正确的代码应该是这样的:
askNotificationPermission().then(registerServiceWorker);
async function registerServiceWorker(permissionStatus) {
if (permissionStatus !== "granted") return alert("no permission!")
const swRegistration = await navigator.serviceWorker.register('/assets/js/order-dashboard/serviceworker.js');
const subscription = swRegistration.pushManager.subscribe(...)
// save subscription at backend
fetch(.., {body: JSON.stringify(subscription)})
}
现在不能麻烦检查文档,但是navigator.serviceWorker.register
在多次调用时也可能是一个问题-使用navigator.serviceWorker.getRegistration
来检查注册本身是否已经存在或重新注册。另外,设置scope
用于注册,以防万一。
无论如何,如果没有访问所有的代码,很难确定为什么代码不能为您工作,但是在必要的地方添加alert
语句,以了解为什么以及在哪里它会中断。好运!
我能解决它。
这是解开谜题的关键。
const main = async () => {
check();
Notification.requestPermission().then(async function() {
await registerServiceWorker()
});
}
完整代码。
panel.js
document.getElementById("enable-notifications").addEventListener("click", () => {
main();
});
const check = () => {
if (!('serviceWorker' in navigator)) {
throw new Error('No Service Worker support!')
}
if (!('PushManager' in window)) {
throw new Error('No Push API Support!')
}
}
const registerServiceWorker = async () => {
const swRegistration = await navigator.serviceWorker.register('/assets/js/order-dashboard/serviceworker.js');
return swRegistration;
}
const main = async () => {
check();
Notification.requestPermission().then(async function() {
await registerServiceWorker()
});
}
const showLocalNotification = (title, body, swRegistration) => {
const options = {
body,
};
swRegistration.showNotification(title, options);
}
serviceworker.js
// urlB64ToUint8Array is a magic function that will encode the base64 public key
// to Array buffer which is needed by the subscription option
const urlB64ToUint8Array = base64String => {
const padding = '='.repeat((4 - (base64String.length % 4)) % 4)
const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/')
const rawData = atob(base64)
const outputArray = new Uint8Array(rawData.length)
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i)
}
return outputArray
}
// saveSubscription saves the subscription to the backend
const saveSubscription = async subscription => {
console.log("saving");
const SERVER_URL = 'A-NGROK-URL/save-subscription'
const response = await fetch(SERVER_URL, {
method: 'post',
headers: {
'Content-Type': 'application/json',
'Bypass-Tunnel-Reminder': 'shit',
},
body: JSON.stringify(subscription),
})
console.log(response);
return response.json()
}
self.addEventListener('activate', async () => {
// This will be called only once when the service worker is activated.
try {
const applicationServerKey = urlB64ToUint8Array(
'BPh1TJ1mbPDgdlJTmogpLcQH1FqAxZpjId7WfEQu6xrME2QGkBaLc0inC6mSFuF17L8_rGno_MDZFrZWmEwwE3k'
)
const options = { applicationServerKey, userVisibleOnly: true }
const subscription = await self.registration.pushManager.subscribe(options)
const response = await saveSubscription(subscription)
console.log(response)
} catch (err) {
console.log('Error', err)
}
})
self.addEventListener("push", function(event) {
if (event.data) {
console.log("Push event!!! ", event.data.text());
showLocalNotification("Yolo", event.data.text(), self.registration);
} else {
console.log("Push event but no data");
}
});
const showLocalNotification = (title, body, swRegistration) => {
const options = {
body
// here you can add more properties like icon, image, vibrate, etc.
};
swRegistration.showNotification(title, options);
};
后端测试完全相同。
我也遇到了同样的问题。我通过添加清单解决了这个问题。使用"display"; "standalone"指令,我完全省略了它,并链接到html文件中的文件。
部署修复后,确保在Safari设置中清除网站数据(缓存)并将网站添加到主屏幕,它工作了。无需在Safari实验功能中启用Web推送和通知。
manifest.json
{
"name": "pwa-ios-test",
"short_name": "pwa-ios-test",
"description": "pwa-ios-test",
"display": "standalone",
"theme_color": "#ffffff",
"icons": [
{
"src": "pwa-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "pwa-512x512.png",
"sizes": "512x512",
"type": "image/png"
},
{
"src": "pwa-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
}
]
}
index . html
<!DOCTYPE html>
<html>
<head>
<link href="manifest.json" rel="manifest">
...
</html>