# 理解 Web Push Notification
Web Push 和 Notification 是兩回事。
而透過 Web Push 來發送 Notification 則是一般常見的 Web Push Notification。
以下分開來討論。
[概觀圖解](https://docs.google.com/presentation/d/1L-fd1XZKY61iZxnu0TpQyo6gd4JbkMM65KH2w176pTA/edit#slide=id.p)
![](https://i.imgur.com/Y57ZYPb.png)
## Notification
前端執行 js 顯示一則 Notification。
需要獲得瀏覽器的 Notification 授權。
要求授權的程式:
```javascript
Notification.requestPermission((status) => console.log(status))
```
根據用戶的選擇,可能會出現 `default`, `granted`, `denied`。
根據用戶的選擇來進行接下來的操作。
你可以從瀏覽器介面上看到和編輯授權狀態:
![](https://i.imgur.com/97iGGlp.png)
你也可以用 Notification.permission 來取得目前用戶的授權狀態。
```javascript
Notification.permission
```
當 Notification.permission 的值是 `granted` 或 `denied` 時,無法透過 `Notification.requestPermission` 來修改狀態。
你可以透過以下程式碼來發送一則 Notification
```javascript
var notificaiton = new Notification("Hi!");
```
![](https://i.imgur.com/GI9y9oy.png)
由前端發送的 Notification 無法使用按鈕,若執行以下程式會獲得錯誤訊息:
```javascript
const dataToSend = {
title: 'Credit Card',
option: {
body: "Did you make a $1,000,000 purchase at Dr. Evil...",
actions: [
{ "action": "yes", "title": "Yes", "icon": "images/yes.png" },
{ "action": "no", "title": "No", "icon": "images/no.png" }
]
}
}
var notificaiton = new Notification(dataToSend.title, dataToSend.option);
```
以下為錯誤訊息:
```javascript
Failed to construct 'Notification': Actions are only supported for persistent notifications shown using ServiceWorkerRegistration.showNotification().
```
## Web Push
Web Push 事件是透過 [ServiceWorker](https://developers.google.com/web/fundamentals/push-notifications/subscribing-a-user#register_a_service_worker) 當中的 PushManager 來傳遞,所以需要建立一個 ServiceWorker。
前端透過 Service Worker 在背景[訂閱 push 事件](https://developers.google.com/web/fundamentals/push-notifications/handling-messages#the_push_event),接著由後端主動傳送一則 push 訊息到 Service Worker。
Service Worker 捕捉到事件後可以執行任意程式碼,不一定要顯示 Notification,但目前其實也沒什麼別的事好做。
### Service Worker
Service Worker 是一個在背景執行的 js 程式。
請建立 sw.js 如下:
```javascript
self.addEventListener('push', function(event) {
const data = event.data.json();
const promiseChain = self.registration.showNotification(data.title, data.option).then(()=>{
console.log('push success');
}).catch(() => {
console.log('push fail');
});
event.waitUntil(promiseChain);
});
```
他會在收到 push 事件的時候[顯示 Notification](https://developers.google.com/web/fundamentals/push-notifications/handling-messages#wait_until)。
### 註冊 Service Worker 以及 Push
然後建立一個網頁 backend_notification.html 內容如下:
```javascript
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
function urlBase64ToUint8Array(base64String) {
var padding = '='.repeat((4 - base64String.length % 4) % 4);
var base64 = (base64String + padding)
.replace(/\-/g, '+')
.replace(/_/g, '/');
var rawData = window.atob(base64);
var outputArray = new Uint8Array(rawData.length);
for (var i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
}
const publicKey = '...';
const subscribeOptions = {
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(publicKey)
};
function subscribeUserToPush() {
return navigator.serviceWorker.register('/sw.js')
.then(function(registration) {
registration.showNotification('test');
return registration.pushManager.subscribe(subscribeOptions);
})
.then(function(pushSubscription) {
console.log('Received PushSubscription: ', JSON.stringify(pushSubscription));
return pushSubscription;
});
}
if (Notification && Notification.permission !== "granted") {
Notification.requestPermission(function (status) {
if (Notification.permission !== status) {
Notification.permission = status;
}
});
}
const pushSubscription = subscribeUserToPush();
</script>
</body>
</html>
```
### 取得 Vapid-Keys
其中,[public key 要透過以下指令生成](https://developers.google.com/web/fundamentals/push-notifications/subscribing-a-user#how_to_create_application_server_keys):
```
npm install -g web-push
web-push generate-vapid-keys
```
以下是一個執行後返回的範例內容:
```
=======================================
Public Key:
BE1njMGjM1FInRM_awCU7rzLsF83NPsPmB8ul2qPRQXUnLTeFfK-SCJE_tlEMRYV8xiTTkUV4uNkKFLfhunJcOk
Private Key:
g1iloBbA8d_ouK4FN6zASmbWMzzt8tVAKZS-_YxUlqU
=======================================
```
而 Private Key 則是在後端發送 push 時會用到。
### 取得 PushSubscription
由於 Service Worker 無法在 file:// 開頭的網址下運作,所以要使用 live server 之類的工具來開啟網頁。
開啟 backend_notification.html 頁面,會在 console 當中看到以下訊息:
```javascript
Received PushSubscription: {"endpoint":"https://fcm.googleapis.com/fcm/send/dlOyQ-Q-o1w:APA91bEikTH2OEPc9_Ix2lF3GJkbyNDq0QFHMyFaN6RK8_rRGCNOwOBVJdGg09sV3GsN04MJOmT7Zx06shsvZC-xbzEAbzZk5falVnJV0gNADDwMRAY38YW-IqsZRO4mdqQslOPxBFE_","expirationTime":null,"keys":{"p256dh":"BFK0nt0OFKe5S6h1eLbudeFD8W5vb0FOmRF9brDkWkfffs7pe3PUAJb369OaxTKmRo7AXk4VdmRheqd7chxMzdE","auth":"DhLrjXptLX5jPAvjpiBTkg"}}
```
這也是發送 push 時需要的 [PushSubscription 資訊](https://developers.google.com/web/fundamentals/push-notifications/subscribing-a-user#what_is_a_pushsubscription),可以用來指定從後端發送的訊息是發送給誰。
### 發送 Push
現在我們要[發送 push](https://developers.google.com/web/fundamentals/push-notifications/sending-messages-with-web-push-libraries#sending_push_messages),請建立 sendPush.js 如下:
```javascript
const webpush = require('web-push');
const vapidKeys = {
publicKey: '...',
privateKey: '...'
};
webpush.setVapidDetails(
'mailto:web-push-book@gauntface.com',
vapidKeys.publicKey,
vapidKeys.privateKey
);
const subscription = 這裡要填入 PushSubscription
const dataToSend = {
title: 'Credit Card',
option: {
body: "Did you make a $1,000,000 purchase at Dr. Evil...",
actions: [
{ "action": "yes", "title": "Yes", "icon": "images/yes.png" },
{ "action": "no", "title": "No", "icon": "images/no.png" }
]
}
}
const triggerPushMsg = function(subscription, dataToSend) {
return webpush.sendNotification(subscription, JSON.stringify(dataToSend))
.catch((err) => {
if (err.statusCode === 404 || err.statusCode === 410) {
console.log('Subscription has expired or is no longer valid: ', err);
} else {
throw err;
}
});
};
triggerPushMsg(subscription, dataToSend).then(data =>{
console.log("success:")
console.log(JSON.stringify(data,null,2));
}).catch(error => {
console.log("fail:")
console.log(JSON.stringify(error,null,2));
});
```
並且[安裝對應的套件 `web-push`](https://developers.google.com/web/fundamentals/push-notifications/sending-messages-with-web-push-libraries#sending_push_messages)。
```
npm install web-push --save
```
再透過 node 執行此 js 即可發送 push。
### 接收 Notification 按鈕事件
接著修改 sw.js 如下:
```javascript
const page = 'https://ngrok.etrex.tw/backend_notification.html';
self.addEventListener('push', function(event) {
const data = event.data.json();
const promiseChain = self.registration.showNotification(data.title, data.option).then(()=>{
console.log('push success');
}).catch(() => {
console.log('push fail');
});
event.waitUntil(promiseChain);
});
self.addEventListener('notificationclick', function(event) {
if (event.action) {
console.log(`Action clicked: '${event.action}'`);
}else{
console.log('Notification Click.');
}
const promiseChain = clients.openWindow(page);
event.waitUntil(promiseChain);
});
self.addEventListener('notificationclose', function(event) {
console.log('Notification Close.');
const promiseChain = clients.openWindow(page);
event.waitUntil(promiseChain);
});
```
再次開啟 background_notification.html 更新 service worker。
就可以控制 notification 當中的按鈕按下時要做什麼。