訂閱使用者

Matt Gaunt

第一步是取得使用者同意,以便向他們傳送推送訊息,接著我們就能實作 PushSubscription

要利用 JavaScript API 執行此操作,基本上是直觀的,因此我們要逐步瞭解邏輯流程。

功能偵測

首先,我們需要檢查目前的瀏覽器是否確實支援推送訊息功能。我們可透過兩個簡單的檢查來確認系統是否支援推送。

  1. navigator 上檢查 serviceWorker
  2. 檢查 window 上的 PushManager
if (!('serviceWorker' in navigator)) {
  // Service Worker isn't supported on this browser, disable or hide UI.
  return;
}

if (!('PushManager' in window)) {
  // Push isn't supported on this browser, disable or hide UI.
  return;
}

儘管服務工作站和推送訊息的瀏覽器支援正在迅速增加,還是建議您針對功能和漸進式強化進行功能偵測。

註冊 Service Worker

功能偵測表示 Service Worker 和推送作業都受到支援。下一步是「註冊」Service Worker,

註冊 Service Worker 時,系統會向瀏覽器告知 Service Worker 檔案的位置。這個檔案仍然只是 JavaScript,但瀏覽器會「授予存取權」給 Service Worker API,包括推送。更明確地說,瀏覽器會在 Service Worker 環境中執行檔案。

如要註冊 Service Worker,請呼叫 navigator.serviceWorker.register(),將路徑傳入我們的檔案。像這樣:

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);
    });
}

這個函式會告訴瀏覽器我們有一個 Service Worker 檔案,以及檔案的所在位置。在這個情況下,Service Worker 檔案位於 /service-worker.js。呼叫 register() 後,瀏覽器會在背景執行下列步驟:

  1. 下載 Service Worker 檔案。

  2. 執行 JavaScript。

  3. 如果所有項目都能正確執行,且未發生任何錯誤,register() 傳回的 promise 就會解決問題。如果出現任何類型的錯誤,承諾會遭到拒絕。

如果 register() 拒絕,請仔細檢查 Chrome 開發人員工具中的 JavaScript,檢查是否有錯字或錯誤。

register() 解析時,會傳回 ServiceWorkerRegistration。我們會使用這項註冊作業存取 PushManager API

PushManager API 瀏覽器相容性

瀏覽器支援

  • 42
  • 17
  • 44
  • 16

資料來源

要求權限

我們已註冊服務工作處理程序,並準備好訂閱使用者了,下一步是取得使用者授予傳送推送訊息的權限。

用於取得權限的 API 相對簡單,缺點則是 API 最近從回呼改為傳回 Promise。但這個問題是無法判斷目前瀏覽器實作的 API 版本,因此您必須實作並處理這兩個版本。

function askPermission() {
  return new Promise(function (resolve, reject) {
    const permissionResult = Notification.requestPermission(function (result) {
      resolve(result);
    });

    if (permissionResult) {
      permissionResult.then(resolve, reject);
    }
  }).then(function (permissionResult) {
    if (permissionResult !== 'granted') {
      throw new Error("We weren't granted permission.");
    }
  });
}

在上述程式碼中,重要的程式碼片段是對 Notification.requestPermission() 的呼叫。這個方法會向使用者顯示提示:

電腦版和行動版 Chrome 上的權限提示。

當使用者按下「允許」、「封鎖」或直接關閉權限提示互動後,系統會以字串的形式提供結果:'granted''default''denied'

在上述程式碼範例中,askPermission() 傳回的承諾會在授予權限後解決,否則系統會擲回錯誤,導致 promise 拒絕。

需要處理的極端案例是使用者點選「封鎖」按鈕。在這種情況下,網頁應用程式將無法再次要求使用者授予權限。他們必須變更應用程式的權限狀態 (在設定面板中隱藏),手動「解除封鎖」應用程式。請仔細思考您向使用者要求權限的方式和時機,因為如果對方點選「封鎖」,要撤銷該項決定並不容易。

好消息是,只要使用者知道系統要求權限的原因,通常都能獲得權限。

我們稍後會說明部分熱門網站如何要求權限。

透過 PushManager 為使用者訂閱

註冊服務工作處理程序且取得權限後,即可呼叫 registration.pushManager.subscribe() 來訂閱使用者。

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;
    });
}

呼叫 subscribe() 方法時,我們會傳入 options 物件,該物件由必要和選用參數組成。

接著來看看可以傳入的所有選項。

userVisibleOnly 選項

推送首次新增到瀏覽器時,不確定開發人員是否應傳送推送訊息,以及未顯示通知。這通常稱為無聲推送,因為使用者不知道在背景發生了什麼事。

開發人員的問題在於,開發人員可以在不知情的情況下,持續追蹤使用者的位置,例如追蹤使用者的位置。

為了避免這種情況,並讓規格作者有時間考慮如何支援這項功能,已新增 userVisibleOnly 選項,並傳入 true 值是與瀏覽器之間的符號協議,每當收到推送推送時,網頁應用程式都會顯示通知 (即沒有靜音推送)。

目前,您必須傳入 true 值。如未加入 userVisibleOnly 鍵或傳入 false,就會收到下列錯誤訊息:

Chrome 目前僅支援 Push API,用於將引發使用者可見訊息的訂閱項目。您可以改為呼叫 pushManager.subscribe({userVisibleOnly: true}) 來表示。詳情請參閱 https://goo.gl/yqv4Q4

因為目前 Chrome 一律無法實作全面無聲推送功能。相反地,規格作者會探索預算 API 的概念,這個 API 將根據網頁應用程式的使用情形,允許網頁應用程式獲得特定數量的靜音推送訊息。

applicationServerKey 選項

上一節已簡單提過「應用程式伺服器金鑰」。推送服務會使用「應用程式伺服器金鑰」來識別訂閱使用者的應用程式,並確保相同的應用程式可以傳送訊息給該使用者。

應用程式伺服器金鑰是應用程式專屬的公開與私密金鑰組。私密金鑰應妥善保存在應用程式內,而且公開金鑰可以自由共用。

傳入 subscribe() 呼叫的 applicationServerKey 選項是應用程式的公開金鑰。瀏覽器會在使用者訂閱時,將其傳送至推送服務,這表示推送服務可以將應用程式的公開金鑰連結至使用者的 PushSubscription

下圖說明這些步驟。

  1. 系統會在瀏覽器中載入網頁應用程式,並呼叫 subscribe(),並傳入公開應用程式伺服器金鑰。
  2. 瀏覽器接著向推送服務發出網路要求,而服務會產生端點,將這個端點與應用程式公開金鑰建立關聯,並將端點傳回瀏覽器。
  3. 瀏覽器會將這個端點新增至 PushSubscription,並透過 subscribe() 承諾傳回。

插圖:在訂閱方法中使用公開應用程式伺服器金鑰。

當您之後想傳送推送訊息時,則需要建立 Authorization 標頭,其中包含以應用程式伺服器私密金鑰簽署的資訊。當推送服務收到傳送推送訊息的要求時,會查詢連結至接收要求的端點的公開金鑰,藉此驗證此已簽署的 Authorization 標頭。如果簽章有效,推送服務就能知道其必須透過相符的私密金鑰來自應用程式伺服器。基本上,這是一項安全措施,可防止他人傳送訊息給應用程式的使用者。

私人應用程式伺服器金鑰在傳送訊息時的使用方式

嚴格說來,applicationServerKey 為選用項目。不過,在 Chrome 中採用最簡單的實作方式,未來也可能會有其他瀏覽器要求使用。在 Firefox 中是選擇性的。

定義應用程式伺服器金鑰「什麼」的規格是 VAPID 規格。當您讀取提及「應用程式伺服器金鑰」或「VAPID 金鑰」的內容時,請記得兩者相同。

如何建立應用程式伺服器金鑰

您可以透過 web-push-codelab.glitch.me 建立一組公開和私密的應用程式伺服器金鑰,也可以使用 web-push 指令列產生金鑰,步驟如下:

    $ npm install -g web-push
    $ web-push generate-vapid-keys

您只需要為應用程式建立這些金鑰一次,只要確保私密金鑰的私密性即可。(沒錯,我剛說了)。

權限和 subscription()

呼叫 subscribe() 有一項副作用。如果網頁應用程式沒有在呼叫 subscribe() 時顯示通知的權限,瀏覽器會為您要求權限。如果您的 UI 能與此流程搭配運作,這個做法就很實用,但如果您想要進一步掌控 (我認為大多數開發人員也會這麼做),請繼續使用我們稍早使用的 Notification.requestPermission() API。

什麼是 PushSubscription?

我們會呼叫 subscribe() 並傳入部分選項,然後會傳回承諾會解析為 PushSubscription,進而產生一些程式碼,如下所示:

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;
    });
}

PushSubscription 物件包含向該使用者傳送推送訊息所需的所有必要資訊。如果使用 JSON.stringify() 列印內容,您會看到以下內容:

    {
      "endpoint": "https://some.pushservice.com/something-unique",
      "keys": {
        "p256dh":
    "BIPUL12DLfytvTajnryr2PRdAgXS3HGKiLqndGcJGabyhHheJYlNGCeXl1dn18gSJ1WAkAPIxr4gK0_dQds4yiI=",
        "auth":"FPssNDTKnInHVndSTdbKFw=="
      }
    }

endpoint 是推送服務網址。如要觸發推送訊息,請向這個網址發出 POST 要求。

keys 物件包含的值會用來加密透過推送訊息傳送的訊息資料 (本節稍後會說明)。

將訂閱項目傳送至您的伺服器

建立推送訂閱項目後,請傳送至您的伺服器。請決定如何執行,但有一個簡單的訣竅是使用 JSON.stringify(),將所有必要資料從訂閱物件中取得。或者,您也可以手動拼湊同一個結果,如下所示:

const subscriptionObject = {
  endpoint: pushSubscription.endpoint,
  keys: {
    p256dh: pushSubscription.getKeys('p256dh'),
    auth: pushSubscription.getKeys('auth'),
  },
};

// The above is the same output as:

const subscriptionObjectToo = JSON.stringify(pushSubscription);

訂閱是在網頁上完成,如下所示:

function sendSubscriptionToBackEnd(subscription) {
  return fetch('/api/save-subscription/', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(subscription),
  })
    .then(function (response) {
      if (!response.ok) {
        throw new Error('Bad status code from server.');
      }

      return response.json();
    })
    .then(function (responseData) {
      if (!(responseData.data && responseData.data.success)) {
        throw new Error('Bad response from server.');
      }
    });
}

節點伺服器會收到這項要求,並將資料儲存到資料庫,供日後使用。

app.post('/api/save-subscription/', function (req, res) {
  if (!isValidSaveRequest(req, res)) {
    return;
  }

  return saveSubscriptionToDatabase(req.body)
    .then(function (subscriptionId) {
      res.setHeader('Content-Type', 'application/json');
      res.send(JSON.stringify({data: {success: true}}));
    })
    .catch(function (err) {
      res.status(500);
      res.setHeader('Content-Type', 'application/json');
      res.send(
        JSON.stringify({
          error: {
            id: 'unable-to-save-subscription',
            message:
              'The subscription was received but we were unable to save it to our database.',
          },
        }),
      );
    });
});

透過伺服器上的 PushSubscription 詳細資料,我們隨時都能向使用者傳送訊息。

常見問題

當前使用者常提出的幾個常見問題:

我可以變更瀏覽器使用的推送服務嗎?

不會。瀏覽器會選取推送服務,而如同 subscribe() 呼叫所示,瀏覽器會向推送服務發出網路要求,以擷取組成 PushSubscription 詳細資料。

每個瀏覽器使用不同的推送服務,是否都擁有不同的 API?

所有推送服務都會預期相同的 API。

這個通用 API 稱為網路推播通訊協定,其中說明應用程式必須提出的網路要求,才能觸發推送訊息。

如果我透過電腦訂閱頻道,使用者會在手機上訂閱嗎?

很抱歉,不行。使用者必須在想接收訊息的每個瀏覽器上進行註冊,才能接收訊息。另外值得注意的是,此情況需要使用者在每部裝置上授予權限。

後續步驟

程式碼研究室