引言
在接下來的內(nèi)容里份氧,我們會(huì)探究 PWA 中的另一個(gè)重要功能——消息推送與提醒(Push & Notification)醉箕。這個(gè)能力讓我們可以從服務(wù)端向用戶推送各類消息并引導(dǎo)用戶觸發(fā)相應(yīng)交互
Push API 和 Notification API 其實(shí)是兩個(gè)獨(dú)立的技術(shù)刷袍,完全可以分開使用房匆;不過Push API 和 Notification API相結(jié)合是一個(gè)常見的模式
瀏覽器是如何實(shí)現(xiàn)服務(wù)器消息 Push 的
Web Push 的整個(gè)流程相較之前的內(nèi)容來說有些復(fù)雜补憾。因此酌伊,在進(jìn)入具體技術(shù)細(xì)節(jié)之前腾窝,我們需要先了解一下整個(gè) Push 的基本流程與相關(guān)概念。
如果你對(duì) Push 完全不了解,可能會(huì)認(rèn)為虹脯,Push 是我們的服務(wù)端直接與瀏覽器進(jìn)行交互驴娃,使用長連接、 WebSocket 或是其他技術(shù)手段來向客戶端推送消息循集。然而唇敞,這里的 Web Push 并非如此,它其實(shí)是一個(gè)三方交互的過程咒彤。
在 Push 中登場(chǎng)的三個(gè)重要“角色”分別是:
- 瀏覽器:就是我們的客戶端
- Push Service:專門的Push服務(wù)厚棵,你可以認(rèn)為是一個(gè)第三方服務(wù),目前chrome與firefox都有自己的Push Service Service蔼紧。理論上只要瀏覽器支持婆硬,可以使用任意的Push Service
- 后端服務(wù):這里就是指我們自己的后端服務(wù)
下面就介紹一下這三者在 Web Push 中是如何交互
消息推送流程
下圖來自Web Push協(xié)議草案,是 Web Push 的整個(gè)流程:
+-------+ +--------------+ +-------------+
| UA | | Push Service | | Application |
+-------+ +--------------+ | Server |
| | +-------------+
| Subscribe | |
|--------------------->| |
| Monitor | |
|<====================>| |
| | |
| Distribute Push Resource |
|-------------------------------------------->|
| | |
: : :
| | Push Message |
| Push Message |<---------------------|
|<---------------------| |
| | |
該時(shí)序圖表明了 Web Push 的各個(gè)步驟奸例,我們可以將其分為訂閱(subscribe)與推送(push)兩部分來看
訂閱 subscribe
- Ask Permission:這一步不在上圖的流程中彬犯,這其實(shí)是瀏覽器中的策略。瀏覽器會(huì)詢問用戶是否允許通知查吊,只有在用戶允許后谐区,才能進(jìn)行后面的操作
- Subscribe:瀏覽器(客戶端)需要向 Push Service 發(fā)起訂閱(subscribe),訂閱后會(huì)得到一個(gè)
PushSubscription
對(duì)象 - Monitor:訂閱操作會(huì)和 Push Service 進(jìn)行通信逻卖,生成相應(yīng)的訂閱信息宋列,Push Service 會(huì)維護(hù)相應(yīng)信息,并基于此保持與客戶端的聯(lián)系
- Distribute Push Resource:瀏覽器訂閱完成后评也,會(huì)獲取訂閱的相關(guān)信息(存在于
PushSubscription
對(duì)象中)炼杖,我們需要將這些信息發(fā)送到自己的服務(wù)端,在服務(wù)端進(jìn)行保存
推送 Push Message
- 我們的服務(wù)端需要推送消息時(shí)盗迟,不直接和客戶端交互坤邪,而是通過 Web Push 協(xié)議,將相關(guān)信息通知 Push Servic罚缕;
- Push Service 收到消息艇纺,通過校驗(yàn)后,基于其維護(hù)的客戶端信息邮弹,將消息推送給訂閱了的客戶端
- 最后黔衡,客戶端收到消息,完成整個(gè)推送過程
什么是 Push Service
在上面的 Push 流程中腌乡,出現(xiàn)了一個(gè)比較少接觸到的角色:Push Service盟劫。那么什么是 Push Service 呢?
我們來看官方解釋:
A push service receives a network request, validates it and delivers a push message to the appropriate browser. If the browser is offline, the message is queued until the browser comes online.
譯: 推送服務(wù)接收一個(gè)網(wǎng)絡(luò)請(qǐng)求导饲,對(duì)其進(jìn)行驗(yàn)證捞高,并將推送消息傳遞到適當(dāng)?shù)臑g覽器。如果瀏覽器離線渣锦,則消息將排隊(duì)等待瀏覽器在線硝岗。
目前,不同的瀏覽器廠商使用了不同的 Push Service袋毙。例如型檀,chrome 使用了 google 自家的 FCM(前身為GCM),firefox 也是使用自家的服務(wù)听盖。那么我們是否需要寫不同的代碼來兼容不同的瀏覽器所使用的服務(wù)呢胀溺?答案是并不用。Push Service 遵循 Web Push Protocol皆看,其規(guī)定了請(qǐng)求及其處理的各種細(xì)節(jié)仓坞,這就保證了,不同的 Push Service 也會(huì)具有標(biāo)準(zhǔn)的調(diào)用方式
這里再提一點(diǎn):我們?cè)谏弦还?jié)中說了 Push 的標(biāo)準(zhǔn)流程腰吟,其中第一步就是瀏覽器發(fā)起訂閱无埃,生成一個(gè)PushSubscription
對(duì)象。Push Service 會(huì)為每個(gè)發(fā)起訂閱的瀏覽器生成一個(gè)唯一的 URL毛雇,這樣我們?cè)诜?wù)端推送消息時(shí)嫉称,向這個(gè) URL 進(jìn)行推送后,Push Service 就會(huì)知道要通知哪個(gè)瀏覽器灵疮。而這個(gè) URL 信息也在PushSubscription
對(duì)象里织阅,叫做endpoint
那么,如果我們知道了endpoint
的值震捣,是否就代表我們可以向客戶端推送消息了呢荔棉?并非如此。下面會(huì)簡(jiǎn)單介紹一下 Web Push 中的安全策略
如何保證 Push 的安全性
在 Web Push 中蒿赢,為了保證客戶端只會(huì)收到其訂閱的服務(wù)端推送的消息(其他的服務(wù)端即使在拿到endpoint
也無法推送消息)江耀,需要對(duì)推送信息進(jìn)行數(shù)字簽名。該過程大致如下:
在 Web Push 中會(huì)有一對(duì)公鑰與私鑰诉植∠楣客戶端持有公鑰,而服務(wù)端持有私鑰晾腔∩嘞。客戶端在訂閱時(shí),會(huì)將公鑰發(fā)送給 Push Service灼擂,而 Push Service 會(huì)將該公鑰與相應(yīng)的endpoint
維護(hù)起來壁查。而當(dāng)服務(wù)端要推送消息時(shí),會(huì)使用私鑰對(duì)發(fā)送的數(shù)據(jù)進(jìn)行數(shù)字簽名剔应,并根據(jù)數(shù)字簽名生成一個(gè)叫Authorization
請(qǐng)求頭睡腿。Push Service 收到請(qǐng)求后语御,根據(jù)endpoint
取到公鑰,對(duì)數(shù)字簽名解密驗(yàn)證席怪,如果信息相符則表明該請(qǐng)求是通過對(duì)應(yīng)的私鑰加密而成应闯,也表明該請(qǐng)求來自瀏覽器所訂閱的服務(wù)端。反之亦然
而公鑰與私鑰如何生成挂捻,會(huì)在下面的實(shí)例中講解
如何使用 Push API 來推送向用戶推送信息
為了使文章與代碼更清晰碉纺,將Web Push分為這幾個(gè)部分:
- 瀏覽器發(fā)起訂閱,并將訂閱信息發(fā)送至后端刻撒;
- 將訂閱信息保存在服務(wù)端骨田,以便今后推送使用;
- 服務(wù)端推送消息声怔,向Push Service發(fā)起請(qǐng)求态贤;
- 瀏覽器接收Push信息并處理。
友情提醒:由于 Chrome 所依賴的 Push Service——FCM 在國內(nèi)不可訪問醋火,所以要正常運(yùn)行 demo 中的代碼需要“梯子”抵卫,或者可以選擇 Firefox 來進(jìn)行測(cè)試
瀏覽器(客戶端)生成 subscription 信息
首先,我們需要使用PushManager
的subscribe
方法來在瀏覽器中進(jìn)行訂閱
在上一節(jié)中我們已經(jīng)知道了如何注冊(cè) Service Worker胎撇。當(dāng)我們注冊(cè)完 Service Worker 后會(huì)得到一個(gè)Registration
對(duì)象介粘,通過調(diào)用Registration
對(duì)象的registration.pushManager.subscribe()
方法可以發(fā)起訂閱
為了使代碼更清晰,本篇 demo 在之前的基礎(chǔ)上晚树,先抽離出 Service Worker 的注冊(cè)方法:
- public/index.js
function registerServiceWorker() {
if (!navigator.serviceWorker) {
return Promise.reject('系統(tǒng)不支持 service worker')
}
return navigator.serviceWorker.register('./sw.js').then(function (reg) {
registration = reg
})
}
然后定義了 subscribeAndDistribute()
方法來發(fā)起訂閱:
- public/index.js
// 訂閱推送并將訂閱結(jié)果發(fā)送給后端
function subscribeAndDistribute(registration) {
if (!window.PushManager) {
return Promise.reject('系統(tǒng)不支持消息推送')
}
// 檢查是否已經(jīng)訂閱過
return registration.pushManager
.getSubscription()
.then(function (subscription) {
// 如果已經(jīng)訂閱過姻采,就不重新訂閱了
if (subscription) {
distributePushResource(subscription)
} else {
return (
// 訂閱
registration.pushManager
.subscribe({
userVisibleOnly: true,
applicationServerKey: window.base64ToUint8Array(VAPIDPublicKey),
})
.then(function (subscription) {
distributePushResource(subscription)
})
)
}
})
}
這里使用了registration.pushManager.subscribe()
方法中的兩個(gè)配置參數(shù):userVisibleOnly
和applicationServerKey
-
userVisibleOnly
userVisibleOnly
表明該推送是否需要顯性地展示給用戶,即推送時(shí)是否會(huì)有消息提醒爵憎。如果沒有消息提醒就表明是進(jìn)行“靜默”推送慨亲。在Chrome中,必須要將其設(shè)置為true宝鼓,否則瀏覽器就會(huì)在控制臺(tái)報(bào)錯(cuò):
-
applicationServerKey
applicationServerKey
是一個(gè)客戶端的公鑰刑棵,VAPID定義了其規(guī)范,因此也可以稱為 VAPID keys愚铡。該參數(shù)需要 Unit8Array 類型蛉签。因此定義了一個(gè)urlBase64ToUint8Array
方法將 base64 的公鑰字符串轉(zhuǎn)為 Unit8Array。subscribe()
也是一個(gè) Promise 方法沥寥,在 then 中我們可以得到訂閱的相關(guān)信息——一個(gè)PushSubscription
對(duì)象碍舍。下圖展示了這個(gè)對(duì)象中的一些信息。注意其中的endpoint
邑雅,Push Service 會(huì)為每個(gè)客戶端隨機(jī)生成一個(gè)不同的值
PushSubscription 信息
之后片橡,我們?cè)賹?code>PushSubscription信息發(fā)送到后端。這里定義了一個(gè)distributePushResource()
方法淮野,該方法就是一個(gè)普通的 XHR 請(qǐng)求捧书,會(huì)向接口 post 訂閱信息
- public/index.js
// 將訂閱信息傳給后端服務(wù)器
function distributePushResource(subscription) {
// 為了方便之后的推送吹泡,為每個(gè)客戶端簡(jiǎn)單生成一個(gè)標(biāo)識(shí)
const body = {
subscription,
uniqueid: new Date().getTime(),
}
console.log('uniqueid', body.uniqueid)
return new Promise(function (resolve, reject) {
var xhr = new XMLHttpRequest()
xhr.timeout = 60000
xhr.onreadystatechange = function () {
var response = {}
if (xhr.readyState === 4 && xhr.status === 200) {
try {
response = JSON.parse(xhr.responseText)
} catch (e) {
response = xhr.responseText
}
resolve(response)
} else if (xhr.readyState === 4) {
resolve()
}
}
xhr.onabort = reject
xhr.onerror = reject
xhr.ontimeout = reject
xhr.open('POST', '/subscription', true)
xhr.setRequestHeader('Content-Type', 'application/json')
xhr.send(JSON.stringify(body))
})
}
當(dāng)網(wǎng)站在完成推送訂閱之后,Web Push API 也提供了相應(yīng)的方法來取消訂閱经瓷。前面提到推送訂閱成功之后 PushManager.subscribe()
方法返回的 pushSubscription
對(duì)象上有一個(gè) unsubscribe()
就是用來取消訂閱的:
pushSubscription.unsubscribe().then(function () {
console.log('取消訂閱成功爆哑!')
})
在取消訂閱之前,我們可以通過 PushManager.getSubscription()
方法來判斷用戶是否已經(jīng)訂閱了嚎,如果用戶已經(jīng)訂閱過,那么該函數(shù)會(huì)返回 pushSubscription
對(duì)象廊营,這樣接下來再調(diào)用 unsubscribe()
方法最終取消訂閱歪泳。如下所示:
registration.pushManager.getSubscription().then(function (pushSubscription) {
if (!pushSubscription) {
// 用戶尚未訂閱
return
}
// 取消訂閱
return pushSubscription.unsubscribe()
})
.then(function () {
console.log('取消訂閱!')
})
最后露筒,將這一系列方法組合在一起:
- public/index.js
const VAPIDPublicKey =
'BOEQSjdhorIf8M0XFNlwohK3sTzO9iJwvbYU-fuXRF0tvRpPPMGO6d_gJC_pUQwBT7wD8rKutpNTFHOHN3VqJ0A'
// 注冊(cè) service worker 并緩存 registration
let registration
// 注冊(cè) service worker
registerServiceWorker()
// 申請(qǐng)桌面通知權(quán)限
.then(function () {
requestNotificationPermission()
})
// 訂閱推送
.then(function () {
subscribeAndDistribute(registration)
})
.catch(function (err) {
console.log(err)
})
function registerServiceWorker() {
if (!navigator.serviceWorker) {
return Promise.reject('系統(tǒng)不支持 service worker')
}
return navigator.serviceWorker.register('./sw.js').then(function (reg) {
registration = reg
})
}
// 申請(qǐng)桌面通知權(quán)限
function requestNotificationPermission() {
// 系統(tǒng)不支持桌面通知
if (!window.Notification) {
return Promise.reject('系統(tǒng)不支持桌面通知')
}
return Notification.requestPermission().then(function (permission) {
if (permission === 'granted') {
return Promise.resolve()
}
return Promise.reject('用戶已禁止桌面通知權(quán)限')
})
}
// 訂閱推送并將訂閱結(jié)果發(fā)送給后端
function subscribeAndDistribute(registration) {
if (!window.PushManager) {
return Promise.reject('系統(tǒng)不支持消息推送')
}
// 檢查是否已經(jīng)訂閱過
return registration.pushManager
.getSubscription()
.then(function (subscription) {
// 如果已經(jīng)訂閱過呐伞,就不重新訂閱了
if (subscription) {
distributePushResource(subscription)
} else {
return (
// 訂閱
registration.pushManager
.subscribe({
userVisibleOnly: true,
applicationServerKey: window.base64ToUint8Array(VAPIDPublicKey),
})
.then(function (subscription) {
distributePushResource(subscription)
})
)
}
})
}
// 將訂閱信息傳給后端服務(wù)器
function distributePushResource(subscription) {
// 為了方便之后的推送,為每個(gè)客戶端簡(jiǎn)單生成一個(gè)標(biāo)識(shí)
const body = {
subscription,
uniqueid: new Date().getTime(),
}
console.log('uniqueid', body.uniqueid)
return new Promise(function (resolve, reject) {
var xhr = new XMLHttpRequest()
xhr.timeout = 60000
xhr.onreadystatechange = function () {
var response = {}
if (xhr.readyState === 4 && xhr.status === 200) {
try {
response = JSON.parse(xhr.responseText)
} catch (e) {
response = xhr.responseText
}
resolve(response)
} else if (xhr.readyState === 4) {
resolve()
}
}
xhr.onabort = reject
xhr.onerror = reject
xhr.ontimeout = reject
xhr.open('POST', '/subscription', true)
xhr.setRequestHeader('Content-Type', 'application/json')
xhr.send(JSON.stringify(body))
})
}
注: 這里為了方便我們后面的推送慎式,為每個(gè)客戶端生成了一個(gè)唯一ID
uniqueid
伶氢,這里使用了時(shí)間戳生成簡(jiǎn)單的uniqueid
由于userVisibleOnly
為true
,所以需要用戶授權(quán)開啟通知權(quán)限瘪吏,因此我們會(huì)看到下面的提示框癣防,選擇“允許”即可。你可以在設(shè)置中進(jìn)行通知的管理
服務(wù)端存儲(chǔ)客戶端 subscription 信息
為了存儲(chǔ)瀏覽器 post 來的訂閱信息掌眠,服務(wù)端需要增加一個(gè)接口/subscription
蕾盯,同時(shí)添加中間件koa-body
用于處理 body
- routes/index.js
const router = require('koa-router')()
const { koaBody } = require('koa-body')
/**
* @description: 提交 subscription 信息,并保存
* @return {*}
*/
router.post('/subscription', koaBody(), async (ctx) => {
let body = ctx.request.body
await util.saveRecord(body)
ctx.response.body = {
success: true,
}
})
接收到 subscription 信息后蓝丙,需要在服務(wù)端進(jìn)行保存级遭,你可使用任何方式來保存它:mysql、redis渺尘、mongodb… 這里為了方便挫鸽,我使用了nedb來進(jìn)行簡(jiǎn)單的存儲(chǔ)。nedb不需要部署安裝鸥跟,可以將數(shù)據(jù)存儲(chǔ)在內(nèi)存中丢郊,也可以持久化,nedb 的 api 和 mongodb 也比較類似
這里util.saveRecord()
做了這些工作:首先医咨,查詢subscription
信息是否存在蚂夕,若已存在則只更新uniqueid
;否則腋逆,直接進(jìn)行存儲(chǔ)
推送信息
在實(shí)際中婿牍,我們一般會(huì)給運(yùn)營或產(chǎn)品同學(xué)提供一個(gè)推送配置后臺(tái)〕颓福可以選擇相應(yīng)的客戶端等脂,填寫推送信息俏蛮,并發(fā)起推送。為了簡(jiǎn)單起見上遥,我并沒有寫一個(gè)推送配置后臺(tái)搏屑,而只提供了一個(gè) post 接口/push
來提交推送信息。后期我們完全可以開發(fā)相應(yīng)的推送后臺(tái)來調(diào)用該接口
- routes/index.js
/**
* @description: 消息推送API粉楚,可以在管理后臺(tái)進(jìn)行調(diào)用
* @return {*}
*/
router.post('/push', koaBody(), async (ctx) => {
const data = ctx.request.body
let { uniqueid } = data
let list = uniqueid ? await util.find({ uniqueid }) : await util.findAll()
for (let i = 0; i < list.length; i++) {
let subscription = list[i].subscription
pushMessage(subscription, JSON.stringify(data))
}
ctx.response.body = {
data: list,
}
})
來看一下/push
接口辣恋。
- 首先根據(jù) post 的參數(shù)不同,我們可以通過
uniqueid
來查詢某條訂閱信息:util.find({uniqueid})
模软;也可以從數(shù)據(jù)庫中查詢出所有訂閱信息:util.findAll()
- 然后通過
pushMessage()
方法向 Push Service 發(fā)送請(qǐng)求伟骨。根據(jù)第二節(jié)的介紹,我們知道燃异,該請(qǐng)求需要符合Web Push協(xié)議携狭。然而,Web Push協(xié)議的請(qǐng)求封裝回俐、加密處理相關(guān)操作非常繁瑣逛腿。因此,Web Push為各種語言的開發(fā)者提供了一系列對(duì)應(yīng)的庫:Web Push Libaray仅颇,目前有NodeJS单默、PHP、Python忘瓦、Java 等雕凹。把這些復(fù)雜而繁瑣的操作交給它們可以讓我們事半功倍 - 最后返回結(jié)果,這里返回了所有的訂閱信息
web-push
安裝
npm install web-push --save
密鑰
公鑰和私鑰可以在線生成, 也可以通過下面方法用命令生成:
npm install web-push -g
web-push generate-vapid-keys
得到的結(jié)果如下所示:
=======================================
Public Key:
BO45dpS6296H9sSWdVcZsnsYeHXCXgYv-9jEUcyFrNfUvmvWF_c5iniytzHU_pEP9mE50xcUKbJrqh-hmMbvLZs
Private Key:
qXsFHan2sLkH0RFHGkNIkVgiUmox3uVYKwEAphUo7II
=======================================
轉(zhuǎn)碼
正如在訂閱推送中提到的政冻,subscribe 方法通過 applicationServerKey 傳入所需要的公鑰枚抵。一般來說得到的公鑰一般都是 base64 編碼后的字符串,需要將其轉(zhuǎn)換成 Uint8Array
格式才能作為 subscribe 的參數(shù)傳入明场。下面給出一個(gè) base64 轉(zhuǎn) Uint8Array 的函數(shù)實(shí)現(xiàn):
function base64ToUint8Array (base64String) {
let padding = '='.repeat((4 - base64String.length % 4) % 4)
let base64 = (base64String + padding)
.replace(/\-/g, '+')
.replace(/_/g, '/')
let rawData = atob(base64)
let outputArray = new Uint8Array(rawData.length)
for (let i = 0; i < rawData.length; i++) {
outputArray[i] = rawData.charCodeAt(i)
}
return outputArray
}
使用
const webpush = require('web-push')
// VAPID
const VAPIDKeys = {
publicKey: 'BOEQSjdhorIf8M0XFNlwohK3sTzO9iJwvbYU-fuXRF0tvRpPPMGO6d_gJC_pUQwBT7wD8rKutpNTFHOHN3VqJ0A',
privateKey: 'TVe_nJlciDOn130gFyFYP8UiGxxWd3QdH6C5axXpSgM'
}
// 設(shè)置 web-push 的 VAPID 值
webpush.setVapidDetails(
'mailto:shenxh0928@gmail.com', // 聯(lián)系郵箱
VAPIDKeys.publicKey,
VAPIDKeys.privateKey,
)
設(shè)置完成后即可使用webpush.sendNotification()
方法向 Push Service 發(fā)起請(qǐng)求汽摹。
最后我們來看下pushMessage()
方法的細(xì)節(jié):
/**
* 向push service推送信息
* @param {*} subscription
* @param {*} data
*/
function pushMessage(subscription, data = {}) {
webpush
.sendNotification(subscription, data, options)
.then((res) => {
console.log('push service的相應(yīng)數(shù)據(jù):', JSON.stringify(res))
return
})
.catch((err) => {
// 判斷狀態(tài)碼,440和410表示失效
if (err.statusCode === 410 || err.statusCode === 404) {
return util.remove(subscription)
} else {
console.log('失敗', err)
}
})
}
webpush.sendNotification
為我們封裝了請(qǐng)求的處理細(xì)節(jié)苦锨。狀態(tài)碼401和404表示該 subscription 已經(jīng)無效逼泣,可以從數(shù)據(jù)庫中刪除
Service Worker 監(jiān)聽 Push 消息
調(diào)用webpush.sendNotification()
后,我們就已經(jīng)把消息發(fā)送至 Push Service了舟舒;而 Push Service 會(huì)將我們的消息推送至瀏覽器
要想在瀏覽器中獲取推送信息拉庶,只需在 Service Worker 中監(jiān)聽push
的事件即可:
- public/sw.js
// 監(jiān)聽 push 事件
self.addEventListener('push', function (e) {
var data = e.data
if (e.data) {
data = data.json()
console.log('push 的數(shù)據(jù)為:', data)
} else {
console.log('沒有 push 任何數(shù)據(jù)')
}
})
Push Service可以在設(shè)備離線時(shí),幫你維護(hù)推送消息秃励。當(dāng)瀏覽器設(shè)備重新聯(lián)網(wǎng)時(shí)氏仗,就會(huì)收到該推送
兼容性
又到了查看兼容性的時(shí)間了。對(duì)于Push API夺鲜,目前 Safari 團(tuán)隊(duì)并沒有明確表態(tài)計(jì)劃支持
其實(shí)比兼容性更大的一個(gè)問題是皆尔,Chrome 所依賴的 FCM 服務(wù)在國內(nèi)是無法訪問的呐舔,而 Firefox 的服務(wù)在國內(nèi)可以正常使用。這也是為什么在代碼中會(huì)有這一項(xiàng)設(shè)置:
const options = {
proxy: 'http://34.82.107.67' // 使用FCM(Chrome)需要配置代理
}
注: 免費(fèi)代理獲取方法: 戳這里
雖然由于 google 服務(wù)被屏蔽慷蠕,導(dǎo)致國內(nèi) Push 功能無法在 chrome 上使用珊拼,但是作為一個(gè)重要的技術(shù)點(diǎn),Web Push 還是非常值得我們了解與學(xué)習(xí)的
本章分支: push