PWA:
通過緩存數(shù)據(jù)讓app支持離線工作是個很好的特性。如果能讓用戶將web app安裝到主屏幕就更好了。但是除了這些依賴用戶操作的特性,我們還可以更進(jìn)一步,通過自動給用戶推送信息來增強(qiáng)用戶粘性,并且可以隨時提供最新的可用數(shù)據(jù)。
兩個API,一個目標(biāo)
Push API和Notification API是獨(dú)立的兩個API,但這兩個API可以合作提供更進(jìn)一步的功能。Push讓我們可以在客戶端不介入的情況下由服務(wù)器推送新內(nèi)容,由app的service worker控制。service worker還可以利用notifications向用戶推送信息,至少是通知用戶有新信息到達(dá)。
就像service worker一樣這兩個特性都工作于瀏覽器窗口之外,因此即使用戶沒有查看app的頁面,數(shù)據(jù)的更新和消息推送仍然能夠進(jìn)行。
消息推送(Notification)
讓我們從消息推送開始——消息推送不依賴于push,但是這兩者結(jié)合起來會更加強(qiáng)大。讓我們單獨(dú)的來研究他們。
請求許可(Request permission)
為了使用消息推送,首先我們需要取得許可。最佳實踐為先跳出窗口向用戶獲得許可權(quán),然后再獲得許可權(quán)的情況下使用消息推送。
var button = document.getElementById("notifications");
button.addEventListener('click', function(e) {
Notification.requestPermission().then(function(result) {
if(result === 'granted') {
randomNotification();
}
});
});
操作系統(tǒng)自己的消息推送服務(wù)會幫助顯示彈窗:

當(dāng)用戶同意接收消息推送時,app就可以利用這項功能了。彈窗可以返回三個結(jié)果,分別是默認(rèn),許可和拒絕。默認(rèn)表示用戶不作出選擇,其他兩個就是字面的意思。
當(dāng)用戶許可后,notification和push就都可以正常工作了。
創(chuàng)建一個notification
樣例代碼中,app創(chuàng)建了一個notification——從外部的數(shù)據(jù)中隨機(jī)獲取一個game。將game名設(shè)置為notification的title,author設(shè)置在body中,并利用icon顯示圖片。
function randomNotification() {
var randomItem = Math.floor(Math.random()*games.length);
var notifTitle = games[randomItem].name;
var notifBody = 'Created by '+games[randomItem].author+'.';
var notifImg = 'data/img/'+games[randomItem].slug+'.jpg';
var options = {
body: notifBody,
icon: notifImg
}
var notif = new Notification(notifTitle, options);
setTimeout(randomNotification, 30000);
}
這樣我們就創(chuàng)建了一個每30秒通知一次的notification。(當(dāng)然真實的app中,不可能這么頻繁的推送消息)Notification API的優(yōu)勢在于其利用了操作系統(tǒng)自己的消息推送功能。這樣無論用戶是否在利用web app,消息都可以被推送給用戶,同時這些notification就好像本地應(yīng)用的消息一樣。
Push
Push相較notification來說更加復(fù)雜一些——我們需要向服務(wù)器進(jìn)行注冊以便服務(wù)器可以向app發(fā)送數(shù)據(jù)。app的Service Worker將會接收從server端push來的數(shù)據(jù),然后可以進(jìn)一步利用notification來進(jìn)行消息推送,或者是用在其他的地方。
這項技術(shù)才剛剛起步——一些樣例使用了Google Cloud Messaging平臺,但又被重寫以便支持VAPID,這提供了一層安全保障。你可以試驗Service Workers Cookbook examples,設(shè)置一個例如Firebase的消息推送服務(wù)器,或者自己搭建一個服務(wù)器(比如使用Node.js)。
之前提到了,想要接收消息,你必須有一個service worker,# 利用Service workders使得PWA支持離線工作【翻譯】中已經(jīng)有所介紹。我們在service worker中創(chuàng)建push服務(wù)訂閱功能。
registration.pushManager.getSubscription() .then( /* ... */ );
一旦用戶訂閱成功,他們就可以接收到服務(wù)器push過來的內(nèi)容。
對于服務(wù)端,所有的過程都通過公有私有密鑰被嚴(yán)格的加密——讓用戶在不加密的環(huán)境中推送消息是個很糟糕的主意。查看更多的信息:Web Push data encryption test page關(guān)于如何使服務(wù)器更加安全。服務(wù)器保存了所有需要push的數(shù)據(jù),以便在用戶訂閱成功后可以推送給他們。
我們通過監(jiān)聽push事件來接受推送過來的消息:
self.addEventListener('push', function(e) { /* ... */ });
我們可以將接受到的消息直接用notification通知給用戶。例如提醒用戶做某事,或者告訴用戶有新消息到達(dá)。
Push用例
Push需要服務(wù)器配合來進(jìn)行工作,我們無法在js13kPWA中實現(xiàn)這個功能,因為js13kPWA搭建在GitHub Pages上,GitHub Pages只支持靜態(tài)文件。具體的細(xì)節(jié)請查看Service Worker Cookbook的Push Payload Demo。
這個demo包含三個文件:
- index.js:app本身的源碼
- server.js:服務(wù)端的代碼(由Node.js編寫)
- service-worker.js:Service Worker相關(guān)代碼
來具體看看吧
index.js
navigator.serviceWorker.register('service-worker.js')
.then(function(registration) {
return registration.pushManager.getSubscription()
.then(async function(subscription) {
// registration part
});
})
.then(function(subscription) {
// subscription part
});
這比我們之前看的js13kPWA的例子稍復(fù)雜一點(diǎn)。在這個例子中,當(dāng)注冊Service Worker成功后,我們可以用registration對象來進(jìn)行訂閱,然后用獲得的subscription對象來完成后續(xù)的操作。
在注冊部分(registration part),代碼大概長這樣:
if(subscription) {
return subscription;
}
如果用戶已經(jīng)訂閱了,我們直接返回subscription對象并移動到訂閱部分(subscription part)。否則,我們初始化一個新的subscription。
const response = await fetch('./vapidPublicKey');
const vapidPublicKey = await response.text();
const convertedVapidKey = urlBase64ToUint8Array(vapidPublicKey);
app獲取服務(wù)器的公有密鑰,然后將response轉(zhuǎn)換成text,為了支持Chrome,我們還需要將其轉(zhuǎn)換成Uint8Array。查看Sending VAPID identified WebPush Notification via Mozilla's Push Service獲取關(guān)于VAPID密鑰的更多信息。
app這時就可以使用PushManager來訂閱。PushManager.subscribe()需要兩個參數(shù)——第一個是userVisibleOnly: true,表示所有推送給用戶的消息均可見,第二個參數(shù)是applicationServerKey,包含了我們獲得的VAPID密鑰。
return registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: convertedVapidKey
});
再看下訂閱部分(subscription part)——app首先將訂閱的詳細(xì)信息通過Fetch以JSON的形式發(fā)送給服務(wù)器。
fetch('./register', {
method: 'post',
headers: {
'Content-type': 'application/json'
},
body: JSON.stringify({
subscription: subscription
}),
});
接著綁定在Subscribe button上的GlobalEventHandlers.onclick如下定義:
document.getElementById('doIt').onclick = function() {
const payload = document.getElementById('notification-payload').value;
const delay = document.getElementById('notification-delay').value;
const ttl = document.getElementById('notification-ttl').value;
fetch('./sendNotification', {
method: 'post',
headers: {
'Content-type': 'application/json'
},
body: JSON.stringify({
subscription: subscription,
payload: payload,
delay: delay,
ttl: ttl,
}),
});
};
當(dāng)點(diǎn)擊按鈕后,fetch讓服務(wù)器發(fā)送消息:payload是顯示在notification中的文本,delay為notification顯示的延遲時間,ttl為time-to-live,表示服務(wù)器保存notification的有效時間,也是以秒來計算。
下一個 JavaScript文件。
server.js
server部分用Node.js編寫,我們這里簡單的看一下:
web-push module用來設(shè)置VAPID密鑰,同時可以在密鑰不可用時有選擇的生成密鑰。
const webPush = require('web-push');
if (!process.env.VAPID_PUBLIC_KEY || !process.env.VAPID_PRIVATE_KEY) {
console.log("You must set the VAPID_PUBLIC_KEY and VAPID_PRIVATE_KEY "+
"environment variables. You can use the following ones:");
console.log(webPush.generateVAPIDKeys());
return;
}
webPush.setVapidDetails(
'https://serviceworke.rs/',
process.env.VAPID_PUBLIC_KEY,
process.env.VAPID_PRIVATE_KEY
);
下一步,module定義了app將會用到的routes:獲取VAPID公有密鑰,注冊,發(fā)送notifications。注意我們在index.js中使用的payload,delay,ttl在這里用上了。
module.exports = function(app, route) {
app.get(route + 'vapidPublicKey', function(req, res) {
res.send(process.env.VAPID_PUBLIC_KEY);
});
app.post(route + 'register', function(req, res) {
res.sendStatus(201);
});
app.post(route + 'sendNotification', function(req, res) {
const subscription = req.body.subscription;
const payload = req.body.payload;
const options = {
TTL: req.body.ttl
};
setTimeout(function() {
webPush.sendNotification(subscription, payload, options)
.then(function() {
res.sendStatus(201);
})
.catch(function(error) {
console.log(error);
res.sendStatus(500);
});
}, req.body.delay * 1000);
});
};
service-worker.js
最后我們來看下service worker:
self.addEventListener('push', function(event) {
const payload = event.data ? event.data.text() : 'no payload';
event.waitUntil(
self.registration.showNotification('ServiceWorker Cookbook', {
body: payload,
})
);
});
我們給push事件加了一個監(jiān)聽器,用數(shù)據(jù)中創(chuàng)建payload變量(若數(shù)據(jù)為空則生成字符串),然后等待直到notification推送給用戶。
至此為止就是一些全部內(nèi)容了,包括基本用法,web push,緩存策略,性能,離線工作等等。
完整例子請查看full source code is available on GitHub。