【PWA】web推送技術
伴隨著今年 Google I/O 大會的召開,一個很火的概念–Progressive Web Apps 誕生了。這代表著我們 web 端有了和原生 APP 媲美的能力。但是,有一個很重要的痛點,web 一直不能使用訊息推送,雖然,後面提出了 Notification
API,但這需要網頁持續開啟,這對於常規 APP 實現的推送,根本就不是一個量級的。所以,開發者一直在呼籲能不能退出一款能夠在網頁關閉情況下的 web 推送呢? 現在,Web 時代已經到來! 為了做到在網頁關閉的情況下,還能繼續傳送 Notification,我們就只能使用駐留程序。而現在 Web 的駐留程序就是現在正在大力普及的
- Push
- Notification
- Service Worker
這裡,我先一個簡單的 demo 樣式。
說實在的,我其實 TM 很煩的這 Noti。一般使用 PC 端的,也沒見有啥訊息彈出來,但是,現在好了 Web 一搞,結果三端通用。你如果不禁用的話,保不準天天彈。。。
SW(Service Worker) 我已經在前一篇文章裡面講清楚了。這裡主要探究一下另外兩個技術 Push
和 Notification
。首先,有一個問題,這兩個技術是用來幹嘛的呢?
Push && Notification
這兩個技術,我們可以理解為就是 server 和 SW 之間,SW 和 user 之間的訊息通訊。
- push: server 將更新的資訊傳遞給 SW
- notification: SW 將更新的資訊推送給使用者
可以看出,兩個技術是緊密連線到一起的。這裡,我們先來講解一下 notification
的相關技術。
Notification
那現在,我們想給使用者傳送一個訊息的話應該怎麼傳送呢? 程式碼很簡單,我直接放了:
self.addEventListener('push', function(event) {
var title = 'Yay a message.';
var body = 'We have received a push message.' ;
var icon = '/images/icon-192x192.png';
var tag = 'simple-push-demo-notification-tag';
var data = {
doge: {
wow: 'such amaze notification data'
}
};
event.waitUntil(
self.registration.showNotification(title, {
body: body,
icon: icon,
tag: tag,
data: data
})
);
});
大家一開始看見這個程式碼,可能會覺得有點陌生。實際上,這裡是結合 SW 來完成的。push
是 SW 接收到後臺的 push 資訊然後出發。當然,我們獲取資訊的主要途徑也是從 event
中獲取的。這裡為了簡便,就直接使用寫死的資訊了。大致解釋一下 API。
- event.waitUntil(promise): 該方法是用來延遲 SW 的結束。因為,SW 可能在任何時間結束,為了防止這樣的情況,需要使用 waitUntil 監聽 promise,使系統不會在 promise 執行時就結束 SW。
- ServiceWorkerRegistration.showNotification(title, [options]): 該方法執行後,會發回一個 promise 物件。
不過,我們需要記住的是 SW 中的 notification 只是很早以前就退出的桌面 notification 的繼承物件。這意味著,大家如果想要嘗試一下 notification,並不需要手動建立一個 notification,而只要使用
// 桌面端
var not = new Notification("show note", { icon: "newsong.svg", tag: "song" });
not.onclick = function() { dosth(this); };
// 在 SW 中使用
self.registration.showNotification("New mail from Alice", {
actions: [{action: 'archive', title: "Archive"}]
});
self.addEventListener('notificationclick', function(event) {
event.notification.close();
if (event.action === 'archive') {
silentlyArchiveEmail();
} else {
clients.openWindow("/inbox");
}
}, false);
不過,如果你想設定自己想要的 note 效果的話,則需要了解一下,showNotification 裡面具體每次引數代表的含義,參考 Mozilla,我們可以瞭解到基本的使用方式。如上,API 的基本格式為 showNotification(title, [options])
- title: 很簡單,就是該次 Not(Notification) 的標題
- options: 這個而是一個物件,裡面可以接受很多引數。
- actions[Array]:該物件是一個數組,裡面包含一個一個物件元素。每個物件包含內容為:
- action[String]: 表示該 Not 的行為。後面是通過監聽
notificationclick
來進行相關處理 - title[String]: 該 action 的標題
- icon[URL]: 該 action 顯示的 logo。大小通常為 24*24
- action[String]: 表示該 Not 的行為。後面是通過監聽
- actions[Array]:該物件是一個數組,裡面包含一個一個物件元素。每個物件包含內容為:
actions 的上限值,通常根據 Notification.maxActions
確定。通過在 Not 中定義好 actions 觸發,最後我們會通過,監聽的 notificationclick
來做相關處理:
self.addEventListener('notificationclick', function(event) {
var messageId = event.notification.data;
event.notification.close();
// 通過設定的 actions 來做適當的響應
if (event.action === 'like') {
silentlyLikeItem();
}
else if (event.action === 'reply') {
clients.openWindow("/messages?reply=" + messageId);
}
else {
clients.openWindow("/messages?reply=" + messageId);
}
}, false);
- body[String]: Not 顯示的主體資訊
- dir[String]: Not 顯示資訊的方向,通常可以取:auto, ltr, or rtl
- icon[String]:Not 顯示的 Icon 圖片路徑。
- image[String]:Not 在 body 裡面附帶顯示的圖片 URL,大小最好是 4:3 的比例。
- tag[String]:用來標識每個 Not。方便後續對 Not 進行相關管理。
- renotify[Boolean]:當重複的 Not 觸發時,標識是否禁用振動和聲音,預設為 false
- vibrate[Array]:用來設定振動的範圍。格式為:[振動,暫停,振動,暫停…]。具體取值單位為 ms。比如:[100,200,100]。振動 100ms,靜止 200ms,振動 100ms。這樣的話,我們可以設定自己 APP 都有的振動提示頻率。
- sound[String]: 設定音訊的地址。例如:
/audio/notification-sound.mp3
- data[Any]: 用來附帶在 Not 裡面的資訊。我們一般可以在
notificationclick
事件中,對回撥引數進行呼叫event.notification.data
。
針對於推送的圖片來說,可能會針對不同的手機用到的圖片尺寸會有所區別,例如,針對不同的 dpi。
具體參照:
看下 MDN 提供的 demo:
function showNotification() {
Notification.requestPermission(function(result) {
if (result === 'granted') {
navigator.serviceWorker.ready.then(function(registration) {
registration.showNotification('Vibration Sample', {
body: 'Buzz! Buzz!',
icon: '../images/touch/chrome-touch-icon-192x192.png',
vibrate: [200, 100, 200, 100, 200, 100, 200],
tag: 'vibration-sample'
});
});
}
});
}
當然,簡單 API 的使用就是上面那樣。但是,如果我們不加剋制的使用 Not,可能會讓使用者完全遮蔽掉我們的推送,得不償失。所以,我們需要遵循一定的原則去傳送。
推送原則
推送必須簡潔 遵循時間,地點,人物要素進行相關資訊的設定。
儘量不要讓使用者開啟網頁檢視 雖然這看起來有點違揹我們最初的意圖。不過,這樣確實能夠提高使用者的體驗。比如在資訊回覆中,直接顯示:
XX回覆:...
這樣的格式,可以完全省去使用者的開啟網頁的麻煩。不要在 title 和 body 出現一樣的資訊 比如: correct: incorrect
不要推薦原生 APP 因為很有可能造成推送資訊重複
不要寫上自己的網址 因為,Not 已經幫你寫好了
儘量讓 icon 和推送有關聯 沒用的 icon: 實用的 icon:
推送許可權
實際上,Not 並不全在 SW 中執行,對於設計使用者初始許可權,我們需要在主頁面中,做出相關的響應。當然,在設定推送的時候,我們需要考慮到使用者是否會禁用,這裡影響還是特別大的。 我們,獲取使用者許可權一般可以直接使用 Notification
上掛載的 permission
屬性來獲取的。
- defualt: 表示需要進行詢問。預設情況是不顯示推送
- denied: 不顯示推送
- granted: 顯示推送
簡單的來說為:
function initialiseState() {
if (!('showNotification' in ServiceWorkerRegistration.prototype)) {
return;
}
// 檢查是否可以進行伺服器推
if (!('PushManager' in window)) {
return;
}
// 是否被禁用
if (Notification.permission === 'denied') {
return;
}
if (Notification.permission === 'granted') {
// dosth();
return;
}
// 如果還處於預設情況下,則進行詢問
navigator.serviceWorker.ready.then(function(serviceWorkerRegistration) {
// 檢查訂閱
serviceWorkerRegistration.pushManager.getSubscription()
.then(function(subscription) {
// 檢查是否已經被訂閱
if (!subscription) {
// 沒有
return;
}
// 有
// doSth();
})
.catch(function(err) {
window.Demo.debug.log('Error during getSubscription()', err);
});
});
}
我們在載入的時候,需要先進行檢查一遍,如果是預設情況,則需要發起訂閱的請求。然後再開始進行處理。 那,我們上面的那段程式碼該放在哪個位置呢?首先,這裡使用到了 SW,這意味著,我們需要將 SW 先註冊成功才行。實際程式碼應放在 SW 註冊成功的回撥中:
window.addEventListener('load', function() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('./service-worker.js')
.then(initialiseState);
} else {
window.Demo.debug.log('Service workers aren\'t supported in this browser.');
}
});
為了更好的顯示資訊,我們還可以將授權程式碼放到後面去。比如,將 subscribe 和 btn 的 click 事件進行繫結。這時候,我們並不需要考慮 SW 是否已經註冊好了,因為SW 的註冊時間遠遠不及使用者的反應時間。 例如:
var pushButton = document.querySelector('.js-push-button');
pushButton.addEventListener('click', function() {
if (isPushEnabled) {
unsubscribe();
} else {
subscribe();
}
});
我們具體看一下 subscribe 內容:
function subscribe() {
var pushButton = document.querySelector('.js-push-button');
pushButton.disabled = true;
navigator.serviceWorker.ready.then(function(serviceWorkerRegistration) {
// 請求訂閱
serviceWorkerRegistration.pushManager.subscribe({userVisibleOnly: true})
.then(function(subscription) {
isPushEnabled = true;
pushButton.textContent = 'Disable Push Messages';
pushButton.disabled = false;
return sendSubscriptionToServer(subscription);
})
});
}
說道這裡,大家可能會看的雲裡霧裡,這裡我們來具體看一下 serviceWorkerRegistration.pushManager
具體含義。該引數是從 SW 註冊事件回撥函式獲取的。也就是說,它是我們和 SW 互動的通道。該物件上,綁定了幾個獲取訂閱相關的 API:
- subscribe(options) [Promise]: 該方法就是我們常常用來觸發詢問的 API。他返回一個 promise 物件.回撥引數為 pushSubscription 物件。這裡,我們後面再進行討論。這裡主要說一下 options 裡面有哪些內容
- options[Object]
- userVisibleOnly[Boolean]:用來表示後續資訊是否展示給使用者。通常設定為 true.
- applicationServerKey: 一個 public key。用來加密 server 端 push 的資訊。該 key 是一個 Uint8Array 物件。
- options[Object]
例如:
registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: new Uint8Array([...])
});
- getSubscription() [Promise]: 用來獲取已經訂閱的 push subscription 物件。
- permissionState(options) [Promise]: 該 API 用來獲取當前網頁訊息推送的狀態 ‘prompt’, ‘denied’, 或 ‘granted’。裡面的 options 和 subscribe 裡面的內容一致。
為了更好的體驗,我們可以將兩者結合起來,進行相關推送檢查,具體的 load 中,則為:
window.addEventListener('load', function() {
var pushButton = document.querySelector('.js-push-button');
pushButton.addEventListener('click', function() {
if (isPushEnabled) {
unsubscribe();
} else {
subscribe();
}
});
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('./service-worker.js')
.then(initialiseState);
} else {
window.Demo.debug.log('Service workers aren\'t supported in this browser.');
}
});
當然,這裡面還會涉及其他的一些細節,我這裡就不過多贅述了。詳情可以查閱: Notification demo。
我們開啟一個 Not 詢問很簡單,但關鍵是,如果讓使用者同意。如果我們一開始就進行詢問,這樣成功性的可能性太低。我們可以在頁面載入後進行詢問。這裡,也有一些提醒原則:
通過具體行為進行詢問 比如,當我在查詢車票時,就可以讓使用者在退出時選擇是否接受推送資訊。比如,國外的飛機延遲通知網頁:
讓使用者來決定是否進行推送 因為使用者不是技術人員,我們需要將一些介面,暴露給使用者。針對推送而言,我們可以讓使用者選擇是否進行推送,並且,在提示的同時,顯示的資訊應該儘量和使用者相關。
推送處理
web push 在實際協議中,會設計到兩個 server,比較複雜,這裡我們先來看一下。client 是如何處理接受到的資訊的。 當 SW 接受到 server 傳遞過來的資訊時,會先觸發 push
事件。我們通常做如下處理:
self.addEventListener('push', function(event) {
if (event.data) {
console.log('This push event has data: ',event.data.text());
} else {
console.log('This push event has no data.');
}
});
其中,我們通過 server push 過來的 msg 通常是掛載到 event.data
裡的。並且,該部署了 Response 的相關 API:
- text(): 返回 string 的內容
- json(): 返回 經過 json parse 的物件
- blob(): 返回 blob 物件
- arrayBuffer(): 返回 arrayBuffer 物件
我們知道 Service Worker 並不是常駐程序,有童鞋可能會問到,那怎麼利用 SW 監聽 push 事件呢? 這裡就不用擔心了,因為瀏覽器自己會開啟一個埠監聽接受到的資訊,然後喚起指定的 SW(如果你的瀏覽器是關閉的,那麼你可以洗洗睡了)。而且,由於這樣隨機關閉的機制,我們需要上述提到的 event.waitUntil
API 來幫助我們完成持續 alive SW 的效果,防止正在執行的非同步程式被終止。針對於我們的 notification 來說,實際上就是一個非同步,所以,我們需要使用上述 API 進行包裹。
self.addEventListener('push', function(event) {
const promiseChain = self.registration.showNotification('Hello, World.');
event.waitUntil(promiseChain);
});
當然,如果你想在 SW 裡面做更多的非同步事情的話,可以使用 Promise.all 進行包裹。
self.addEventListener('push', function(event) {
const promiseChain = Promise.all([ async1,async2 ]);
event.waitUntil(promiseChain);
});
之後,就是將具體資訊展示推送給使用者了。上面已經將了具體 showNotification
裡面的引數有哪些。不過,這可能不夠直觀,我們可以使用一張圖來感受一下:
(左:firefox,右:Chrome)
另外,在 showNotification options 裡面,還有一些屬性需要我們額外注意。
屬性注意
tag
對於指定的 Not 我們可以使用 tag
來表明其唯一性,這代表著當我們在使用相同 tag
的 Not 時,上一條 Not 會被最新擁有同一個 tag 的Not 替換。即:
const title = 'First Notification';
const options = {
body: 'With \'tag\' of \'message-group-1\'',
tag: 'message-group-1'
};
registration.showNotification(title, options);
顯示樣式為:
接著,我顯示一個不同 tag 的 Not: