# 理解 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 當中的按鈕按下時要做什麼。