良い一日!私の名前はウラジミールストリヤロフです。私はDomKlikのクライアントコミュニケーションチームのバックエンド開発者です。この記事では、クロスプラットフォームのプッシュ通知を実装する方法について説明します。これについてはすでに多くのことが書かれていますが、実装プロセスで直面しなければならないニュアンスのいくつかについてお話ししたいと思います。何が起こっているのかをよりよく理解するために、プッシュ通知を受け入れることができる小さなWebアプリケーションも作成します。
まず、プッシュ通知をどこに送信するかを理解する必要があります。私たちの場合、これはウェブサイト、iOSアプリ、Androidアプリです。
Webプッシュ通知から始めましょう。それらを受信するために、ブラウザーはプッシュサーバーに接続し、自身を識別して、Service Workerへの通知を受信します(イベントがトリガーされます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 . , , , , ..
:
, -, , , -, .