美好的一天!我叫Vladimir Stolyarov,我是DomKlik客户沟通团队的后端开发人员。在本文中,我将引导您完成如何实现跨平台的推送通知。尽管已经写了很多有关此的文章,但我想谈一谈我们在实施过程中必须面对的一些细微差别。为了更好地了解正在发生的事情,我们还将与您一起编写一个小型的Web应用程序,该应用程序可以接受推送通知。
首先,您需要了解我们要在哪里发送推送通知。在我们的案例中,这是一个网站,一个iOS应用程序和一个Android应用程序。
让我们从Web推送通知开始。为了接收它们,浏览器连接到其推送服务器,对其进行标识并接收给服务工作者的通知(事件在其中触发push
)。这里的差别是每个浏览器都有自己的推送服务:
- Firefox Mozilla Push Service. , .
- Chrome Google Cloud Messaging ( Firebase Cloud Messaging, ), .
, - IETF (https://datatracker.ietf.org/wg/webpush/documents/), API , .
Android. :
- Google Apps, Firebase Cloud Messaging.
- Huawei Google Apps, Huawei Push Kit.
- - , , https://bubu1.eu/openpush/, .
iOS. Android, Apple — Apple Push Notification service (APNs).
: , API ? , , Firebase Cloud Messaging, Android, - APNs. : Huawei Google Apps Huawei Push Kit, Firebase Cloud Messaging.
, :
- - — .
- , .
- .
- Firebase . Firebase -. HTTP- .
:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title> </title>
</head>
<body>
<script src="https://www.gstatic.com/firebasejs/7.14.5/firebase-app.js"></script>
<script src="https://www.gstatic.com/firebasejs/7.14.5/firebase-messaging.js"></script>
<script>
function toClipboard(text) {
const tmp = document.createElement('textarea');
tmp.hidden = true;
tmp.value = text;
window.document.body.appendChild(tmp);
tmp.select();
window.document.execCommand("copy");
alert("Copied the text: " + text);
window.document.body.removeChild(tmp);
}
</script>
<button onclick="enableNotifications()"> </button>
<div id="pushTokenLayer" hidden>
Firebase token <code id="pushTokenValue" style="cursor:pointer" onclick="toClipboard(this.innerText)"></code><br/>
</div>
<script>
async function enableNotifications() {
// Insert your firebase project config here
const firebaseConfig = {};
const app = firebase.initializeApp(firebaseConfig);
const messaging = app.messaging();
const permission = await Notification.requestPermission();
if (permission !== 'granted') {
console.log("user denied notifications")
}
const token = await messaging.getToken();
window.document.getElementById("pushTokenLayer").removeAttribute("hidden");
const pushTokenValue = window.document.getElementById("pushTokenValue");
pushTokenValue.innerText = token
}
</script>
</body>
</html>
-. /firebase-messaging-sw.js
. .
, , . API ( ). :
curl -X POST 'https://fcm.googleapis.com/fcm/send' \
-H 'Authorization: key=<fcm server key>' \
-H 'Content-Type: application/json' \
-d '{
"to" : "< >",
"notification" : {
"body" : "Body of Your Notification",
"title": "Title of Your Notification"
}
}'
:
, : - , (.. ). .
, . setBackgroundMessageHandler
. -, :
messaging.setBackgroundMessageHandler((payload) => {
console.log('Message received. ', payload);
// ...
});
-, … , . ? :
Note: If you set notification fields in your message payload, your setBackgroundMessageHandler callback is not called, and instead the SDK displays a notification based on your payload.
notification
. , .
, , . firebase-messaging-sw.js
:
self.addEventListener("push", event => { event.waitUntil(onPush(event)) });
async function onPush(event) {
const push = event.data.json();
console.log("push received", push)
const { notification = {} } = {...push};
await self.registration.showNotification(notification.title, {
body: notification.body,
})
}
json js-, , . waitUntil
: , - onPush
.
:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title> </title>
</head>
<body>
<script src="https://www.gstatic.com/firebasejs/7.14.5/firebase-app.js"></script>
<script src="https://www.gstatic.com/firebasejs/7.14.5/firebase-messaging.js"></script>
<script>
function toClipboard(text) {
const tmp = document.createElement('textarea');
tmp.hidden = true;
tmp.value = text;
window.document.body.appendChild(tmp);
tmp.select();
window.document.execCommand("copy");
alert("Copied the text: " + text);
window.document.body.removeChild(tmp);
}
</script>
<form onsubmit="enableNotifications(this); return false" action="#">
User ID <input type="number" name="userID" required/>
<input type="submit" value=" "/>
</form>
<div id="pushTokenLayer" hidden>
Firebase token <code id="pushTokenValue" style="cursor:pointer" onclick="toClipboard(this.innerText)"></code><br/>
<button onclick="logout()"></button>
</div>
<script>
// Insert your firebase project config here
const firebaseConfig = {};
const app = firebase.initializeApp(firebaseConfig);
const messaging = app.messaging(); // this fails if browser not supported
async function getMe() {
const resp = await fetch(`${window.location.origin}/api/v1/users/me`, {
credentials: "include",
});
if (resp.status === 401) {
return null;
}
if (!resp.ok) {
throw `unexpected status code ${resp.status}`
}
return await resp.json();
}
async function sendToken(token) {
const me = await getMe();
if (me === null) {
console.error("unauthorized on send token");
return;
}
window.localStorage.getItem("push-token-user");
const resp = await fetch(`${window.location.origin}/api/v1/tokens`, {
method: "POST",
body: JSON.stringify({
token: {token: token, platform: "web"}
}),
credentials: "include",
})
if (!resp.ok) {
console.error("send token failed");
return;
}
// put current user to local storage for comparison
window.localStorage.setItem("push-token-user", JSON.stringify(me));
}
getMe().
then(me => {
if (!me) {
// if user not authorized we must invalidate firebase registration
// to prevent receiving pushes for unauthorized user
// this may happen i.e. if 'deleteToken' failed on logout
console.log(`user unauthorized, invalidate fcm registration`);
window.localStorage.removeItem("push-token-user");
messaging.deleteToken();
return null;
}
// if user authorized and it's not user that received push token earlier
// we also must invalidate token to prevent receiving pushes for wrong user
// this may happen if i.e. user not logged out explicitly
let pushTokenUser = window.localStorage.getItem("push-token-user");
if (pushTokenUser && JSON.parse(pushTokenUser).id !== me.id) {
console.log("token for wrong user, invalidate fcm registration");
window.localStorage.removeItem("push-token-user");
messaging.deleteToken();
pushTokenUser = null;
}
// if user authorized and permission granted but token wasn't send we should re-send it
if (!pushTokenUser && Notification.permission === "granted") {
console.log("token not sent to server while notification permission granted");
messaging.getToken().then(sendToken);
}
}).
catch(e => console.log("get me error", e))
// according to sources of firebase-js-sdk source code registration token refreshed once a week
messaging.onTokenRefresh(async () => {
const newToken = await messaging.getToken();
pushTokenValue.innerText = newToken;
console.log(`updated token to ${newToken}`)
await sendToken(newToken)
})
async function enableNotifications(form) {
const loginResponse = await fetch(`${window.location.origin}/api/v1/users/login`, {
method: "POST",
body: JSON.stringify({
id: Number(form.elements.userID.value),
})
})
if (!loginResponse.ok) {
alert("login failed");
return;
}
const permission = await Notification.requestPermission();
if (permission !== 'granted') {
console.log("user denied notifications")
return;
}
const token = await messaging.getToken();
window.document.getElementById("pushTokenLayer").removeAttribute("hidden");
const pushTokenValue = window.document.getElementById("pushTokenValue");
pushTokenValue.innerText = token
await sendToken(token)
}
async function logout() {
const messaging = firebase.messaging();
await messaging.deleteToken();
console.log(`deleted token from firebase`)
window.document.getElementById("pushTokenLayer").setAttribute("hidden", "");
await fetch(`${window.location.origin}/api/v1/users/logout`, {
method: "POST",
credentials: "include",
})
}
</script>
</body>
</html>
, Go. , :
type MemoryStorage struct {
mu sync.RWMutex
userTokens map[uint64][]Token
tokenOwners map[string]uint64
}
func NewMemoryStorage() *MemoryStorage {
return &MemoryStorage{
userTokens: map[uint64][]Token{},
tokenOwners: map[string]uint64{},
}
}
type Token struct {
Token string `json:"token"`
Platform string `json:"platform"`
}
func (ms *MemoryStorage) SaveToken(ctx context.Context, userID uint64, token Token) error {
ms.mu.Lock()
defer ms.mu.Unlock()
owner, ok := ms.tokenOwners[token.Token]
// if old user comes with some token it's ok
if owner == userID {
return nil
}
// if new user come with existing token we
// should change it's owner to prevent push target mismatch
if ok {
ms.deleteTokenFromUser(token.Token, owner)
}
ut := ms.userTokens[userID]
ut = append(ut, token)
ms.userTokens[userID] = ut
ms.tokenOwners[token.Token] = userID
return nil
}
func (ms *MemoryStorage) deleteTokenFromUser(token string, userID uint64) {
ut := ms.userTokens[userID]
for i, t := range ut {
if t.Token == token {
ut[i], ut[len(ut)-1] = ut[len(ut)-1], Token{}
ut = ut[:len(ut)-1]
break
}
}
ms.userTokens[userID] = ut
}
func (ms *MemoryStorage) UserTokens(ctx context.Context, userID uint64) ([]Token, error) {
ms.mu.RLock()
defer ms.mu.RUnlock()
tokens := ms.userTokens[userID]
ret := make([]Token, len(tokens))
copy(ret, tokens)
return ret, nil
}
func (ms *MemoryStorage) DeleteTokens(ctx context.Context, tokens []string) error {
ms.mu.Lock()
defer ms.mu.Unlock()
for _, token := range tokens {
user, ok := ms.tokenOwners[token]
if !ok {
return nil
}
ms.deleteTokenFromUser(token, user)
}
return nil
}
.
:
- , - . - / .
- -. Firebase , , .
- - . .
, ( ):
- - , , , . firebase-js-sdk, ,
onTokenRefresh
. - -. , Firebase . .
- . .. , . . - , . , - . : .
- , . : , ( Android/iOS , — ), .
, , . … ?
, Huawei . . , — HTTP- . , Firebase, Huawei : .
: ( UUID) . HTTP-, . firebase-messaging-sw.js
:
self.addEventListener("push", event => { event.waitUntil(onPush(event)) });
async function onPush(event) {
const push = event.data.json();
console.log("push received", push)
const { notification = {}, data = {} } = {...push};
await self.registration.showNotification(notification.title, {
body: notification.body,
})
if (data.id) {
await fetch(`${self.location.origin}/api/v1/notifications/${data.id}/confirm`, { method: "POST" })
}
}
, . setBackgroundMessageHandler
? , , Firebase ( Huawei) ( API) , , ( notification
) data-. , , data- , .
- , firebase-js-sdk -, Android . Android data notification
, .
APNs mutable-content
1, , , HTTP-. , - iOS , .
: data- , - , . , , Telegram , .
: , , , 15 , . , , TTL
.
. , :
- Android ( Huawei) — 40 %
- Web — 50 %
- iOS — 70 %
Huawei . , , , , ..
:
, -, , , -, .