web-push實現原理及細節介紹

1、web push 使用動機與原理簡述

相較於移動端本地應用,web站點經常缺乏一項經常使用的功能:推送通知。此處的推送通知通常指由瀏覽器實現的消息推送,換個說法,就是用戶在打開瀏覽器時,不須要進入特定的網站,就能收到該網站推送而來的消息,例如:新評論,新動態等等。git

那麼web push到底是怎樣的一個流程呢,簡單地說,能夠分爲三個步驟:github

  1. 客戶端完成請求訂閱一個用戶的邏輯
  2. 服務端調用聽從web push協議的接口,傳送消息推送(push message)到推送服務器(該服務器由瀏覽器決定,開發者所能作的只有控制發送的數據)
  3. 推送服務器將該消息推送至對應的瀏覽器,用戶收到該推送

第一步,客戶端請求訂閱用戶,過程以下:web

圖片描述

說明一下這三步,在第一步以前,應用服務器須要生成應用服務器密鑰(application server keys),其做用是標識該服務器,保證每次發消息推送的都是同一個服務器。而後,客戶端將會請求用戶受權消息推送,一旦用戶受權,瀏覽器就會生成一個PushScription,而後這個PushScription將會被髮送至服務器,存入數據庫,在後面的消息推送中使用。ajax

第二步,應用服務器發送web push協議標準的api,觸發推送服務器的消息推送,其中headers必須配置正確,且傳送的數據必須是比特流。chrome

圖片描述

應用服務器發送消息推送請求(目的是爲了將更新推送到用戶的瀏覽器),爲了向推送服務器發出請求,須要查看先前得到的PushScription,取出其中的endpoint,即爲推送服務器配置給該用戶的訪問點。數據庫

一個PushScription對象以下:api

{
  "endpoint": "https://random-push-service.com/some-kind-of-unique-id-1234/v2/",
  "keys": {
    "p256dh" :
"BNcRdreALRFXTkOOUHK1EtK2wtaz5Ry4YfYCA_0QTpQtUbVlUls0VJXg7A8u-Ts1XbjhazAkj7I99e8QcYP7DkM=",
    "auth"   : "tBHItJI5svbpez7KI4CCXg=="
  }
}

其中的endpoint包含了推送服務器域名,path後面的部分爲推送服務器爲每一個用戶分配的一個標識符。promise

發送數據時,數據必須編碼(出於安全性考慮)。推送服務器在接收到這樣一個請求以後,當即開始監聽用戶瀏覽器是否處於在線狀態,如果,則將消息推送發送至瀏覽器。瀏覽器

第三步,瀏覽器端接收消息推送,觸發push事件並展現安全

圖片描述

瀏覽器在接收到推送服務器發來的推送後,將其解碼並觸發一個push事件。Service Worker因爲它能夠在瀏覽器頁面未打開,瀏覽器未打開時執行,所以通常選擇它完成web push的最後一步,即響應push事件完成展現通知等業務邏輯。

2、web push實現細節

按照上一部分所說,首先進行用戶訂閱。

首先註冊一個Service Worker,若註冊成功,返回的Promise爲resolve狀態,以下:

function registerServiceWorker() {
  return navigator.serviceWorker.register('service-worker.js')
  .then(function(registration) {
    console.log('Service worker successfully registered.');
    return registration;
  })
  .catch(function(err) {
    console.error('Unable to register service worker.', err);
  });
}

隨後測試window環境下是否有Notification對象(此處以chrome爲例,若使用firefox,uc等瀏覽器,須要遵循其相應標準,調用對應對象方法或引入JS SDK包),測試成功,調用Notification.requestPermission請求用戶受權發送推送,若受權成功,將會返回'granted'。

接下來要作的就是使用註冊好的Service Worker對象,調用pushManager.subscribe方法,從客戶端得到剛剛所說的PushScription對象。

function subscribeUserToPush() {
  return navigator.serviceWorker.register('service-worker.js')
  .then(function(registration) {
    const subscribeOptions = {
      userVisibleOnly: true,
      applicationServerKey: urlBase64ToUint8Array(
        'BEl62iUYgUivxIkv69yViEuiBIa-Ib9-SkvMeAtA3LFgDzkrxZJjSgSnfckjBJuBkr3qBUYIHBQFLXYp5Nksh8U'
      )
    };

    return registration.pushManager.subscribe(subscribeOptions);
  })
  .then(function(pushSubscription) {
    console.log('Received PushSubscription: ', JSON.stringify(pushSubscription));
    return pushSubscription;
  });
}

userVisibleOnly是爲了保證推送對用戶可見,application server key則如前文所說,是推送服務器用以識別應用服務器的密鑰,這裏的密鑰包含了公鑰和私鑰,傳輸的是公鑰。同時,PushScription的endpoint也是在這個過程當中生成的,生成公鑰和私鑰可使用web-push庫。

這裏再次說明一下推送服務器的不可選擇性,在調用subscribe生成PushScription時,瀏覽器會向它指定的中轉服務器發送請求來生成endpoint和其他部分,這是無法控制的。

PushScription中的auth和p256dh是用來控制帶載荷的push message的。

獲取到PushScription對象後,將其發往應用服務器,此處簡化了存儲,使用nedb存下PushScription並返回Promise:

function saveSubscriptionToDatabase(subscription) {
  return new Promise(function(resolve, reject) {
    db.insert(subscription, function(err, newDoc) {
      if (err) {
        reject(err);
        return;
      }

      resolve(newDoc._id);
    });
  });
};

存儲完畢後,接下來就是開發後臺管理邏輯,使得管理員可以觸發向用戶推送消息的事件,應用服務器所作的邏輯就是遍歷在數據庫中存儲的全部PushScription並推送消息,如下是使用web-push庫完成配置密鑰及聯繫郵箱的示例:

const vapidKeys = {
  publicKey:
'BEl62iUYgUivxIkv69yViEuiBIa-Ib9-SkvMeAtA3LFgDzkrxZJjSgSnfckjBJuBkr3qBUYIHBQFLXYp5Nksh8U',
  privateKey: 'UUxI4O8-FbRouAevSmBQ6o18hgE4nSG3qwvJTfKc-ls'
};

webpush.setVapidDetails(
  'mailto:web-push-book@gauntface.com',
  vapidKeys.publicKey,
  vapidKeys.privateKey
);

不要忘了配置你在谷歌雲服務(例如FCM)申請到的GCMApiKey:

webpush.setGCMAPIKey('<Your GCM API Key Here>');

配置完成後,就能夠將subscription發送出去,使用web-push的sendNotification接口:

webpush.sendNotification(pushSubscription, 'Your Push Payload Text');

推送服務器發送消息後,會觸發瀏覽器的push事件,爲了控制service worker的邏輯,須要使用event.waitUntil方法,此方法接收一個promise參數,在promise變爲resolved狀態後,瀏覽器就會檢查通知是否已被展現,如果,則關閉service worker。

若是不處理未正常執行的promise,部分瀏覽器如chrome會展現默認消息框:

圖片描述

展現一個通知調用的爲showNotification方法,傳的參數包括title等,以下:

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';

event.waitUntil(
    self.registration.showNotification(title, {
      body: body,
      icon: icon,
      tag: tag
    })
);

而展現notification時,除了控制它的視圖層之外,也能夠控制它的邏輯層,例如點擊消息通知後進行某些操做等等,在先前調用showNotification時能夠傳入一些參數,例如,根據不一樣的action執行不一樣的操做:

self.addEventListener('notificationclick', function(event) {
  if (!event.action) {
    // Was a normal notification click
    console.log('Notification Click.');
    return;
  }

  switch (event.action) {
    case 'coffee-action':
      console.log('User ❤️️\'s coffee.');
      break;
    case 'doughnut-action':
      console.log('User ❤️️\'s doughnuts.');
      break;
    case 'gramophone-action':
      console.log('User ❤️️\'s music.');
      break;
    case 'atom-action':
      console.log('User ❤️️\'s science.');
      break;
    default:
      console.log(`Unknown action clicked: '${event.action}'`);
      break;
  }
});

3、兼容性及其餘問題

與ajax輪詢、http長鏈接、WebSocket的對比

  • ajax輪詢是經過客戶端不斷向服務端發送http請求,如有新消息就取回的模式保持數據實時更新,但這種方式須要服務器有很快的處理速度和資源
  • http長鏈接是客戶端向服務器發送請求後,若服務器沒有新數據要發送,就不返回response,一旦有了新數據返回了response,客戶端就馬上再發一個request,周而復始。事實上這是把http協議的不對稱性從客戶端轉移到了服務端
  • WebSocket是HTML5中提出的一個新標準(也可視之爲協議),客戶端在發送請求時在請求頭加入額外的字段,以標識這是一個基於WebSocket協議的鏈接,服務器根據這個請求頭生成響應,與客戶端創建起WebSocket鏈接,以後服務端有新消息時,直接向客戶端推送便可

不一樣瀏覽器兼容性

  • chrome採用的推送服務器爲gcm或fcm,firefox也有本身的推送服務器
  • uc前些時間構建了本身的推送服務器,引入其官網上的sdk包,申請使用後便可用於開發

你們要是感興趣能夠看看個人github~https://github.com/proempire,這個項目可能會繼續跟進

相關文章
相關標籤/搜索