訂閱使用者

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。

註冊服務工作者時,我們會告訴瀏覽器服務工作者檔案的位置。檔案仍只是 JavaScript,但瀏覽器會「授予」服務工作者 API 存取權,包括推送。更確切來說,瀏覽器會在 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.js 中。在幕後,瀏覽器會在呼叫 register() 後採取下列步驟:

  1. 下載 Service Worker 檔案。

  2. 執行 JavaScript。

  3. 如果一切運作正常且沒有錯誤,register() 傳回的承諾就會解析。如果發生任何錯誤,承諾就會遭到拒絕。

如果 register() 確實遭到拒絕,請在 Chrome 開發人員工具中仔細檢查 JavaScript 是否有錯字/錯誤。

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

PushManager API 瀏覽器相容性

瀏覽器支援

  • Chrome:42.
  • Edge:17。
  • Firefox:44。
  • Safari: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() 傳回的承諾會解析,否則會擲回錯誤,導致承諾遭到拒絕。

您需要處理的一個極端情況是,使用者點選「封鎖」按鈕。如果發生這種情況,您的網頁應用程式就無法再次要求使用者授權。他們必須手動變更應用程式的權限狀態,才能「解除封鎖」應用程式,而這項操作必須在設定面板中進行。請仔細考慮要求使用者授予權限的方式和時機,因為如果使用者點選「封鎖」,您就無法輕易撤銷該決定。

好消息是,只要知道要求權限的原因,大多數使用者都樂於授予權限。

我們稍後會介紹某些熱門網站要求取得權限。

使用 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 的概念,這項概念可讓網頁應用程式根據網頁應用程式的用量,發送一定數量的靜默推播訊息。

applicationServerKey 選項

我們在上一節中簡短提及「應用程式伺服器金鑰」。「應用程式伺服器金鑰」可供推播服務使用,用於識別訂閱使用者的應用程式,並確保同一個應用程式會傳送訊息給該使用者。

應用程式伺服器金鑰是應用程式專屬的公開和私密金鑰組。私密金鑰應保密,而公用金鑰則可自由分享。

傳入 subscribe() 呼叫的 applicationServerKey 選項是應用程式的公開金鑰。瀏覽器在訂閱使用者時,會將此資訊傳遞至推播服務,也就是說,推播服務可以將應用程式的公開金鑰繫結至使用者的 PushSubscription

下圖說明這些步驟。

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

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

日後如要傳送推播訊息,您必須建立授權標頭,其中包含使用應用程式伺服器的私密金鑰簽署的資訊。當推送服務收到傳送推送訊息的要求時,可以透過查詢與接收要求的端點相關聯的公開金鑰,驗證這個已簽署的「Authorization」標頭。如果簽章有效,推播服務就會知道該簽章必須來自具有相符的私密金鑰的應用程式伺服器。基本上,這是一種安全措施,可防止他人傳送訊息給應用程式使用者。

傳送訊息時如何使用應用程式私密伺服器金鑰

從技術層面來說,applicationServerKey 為選用項目。不過,Chrome 上最簡單的實作方式需要使用這個屬性,其他瀏覽器日後也可能會需要。在 Firefox 中,此欄位為選用。

定義應用程式伺服器金鑰「應該」的規格是 VAPID 規格。每當您閱讀「應用程式伺服器金鑰」或「VAPID 金鑰」的內容時,請記得這些二者是相同的內容。

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

您可以造訪 web-push-codelab.glitch.me 建立公開和私密的應用程式伺服器金鑰組合,也可以使用web-push 指令列產生金鑰,方法如下:

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

您只需要為應用程式建立這些金鑰一次,只要確保私密金鑰安全無虞即可。(是的,我剛剛說過)。

權限和 subscribe()

呼叫 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 物件包含用於透過推播訊息傳送訊息資料的值 (我們會在本節稍後討論)。

定期續訂,避免訂閱項目過期

訂閱推播通知時,您通常會收到 nullPushSubscription.expirationTime。理論上,這表示訂閱項目永遠不會到期 (與收到 DOMHighResTimeStamp 不同,後者會顯示訂閱項目的確切到期時間)。不過,在實際情況下,瀏覽器通常會讓訂閱項目過期,例如在一段時間內未收到推送通知,或是瀏覽器偵測到使用者並未使用具有推送通知權限的應用程式。避免這種情況的一種做法,是在每次收到通知時重新訂閱使用者,如以下程式碼片段所示。這項做法要求您頻繁傳送通知,以免瀏覽器自動讓訂閱項目過期。您必須非常謹慎地權衡合法通知需求的優缺點,避免為了避免訂閱項目過期而無意間向使用者傳送垃圾訊息。畢竟,您不應該反而為了保護使用者,避免他們因忘記通知而訂閱太多內容。

/* In the Service Worker. */

self.addEventListener('push', function(event) {
  console.log('Received a push message', event);

  // Display notification or handle data
  // Example: show a notification
  const title = 'New Notification';
  const body = 'You have new updates!';
  const icon = '/images/icon.png';
  const tag = 'simple-push-demo-notification-tag';

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

  // Attempt to resubscribe after receiving a notification
  event.waitUntil(resubscribeToPush());
});

function resubscribeToPush() {
  return self.registration.pushManager.getSubscription()
    .then(function(subscription) {
      if (subscription) {
        return subscription.unsubscribe();
      }
    })
    .then(function() {
      return self.registration.pushManager.subscribe({
        userVisibleOnly: true,
        applicationServerKey: urlBase64ToUint8Array('YOUR_PUBLIC_VAPID_KEY_HERE')
      });
    })
    .then(function(subscription) {
      console.log('Resubscribed to push notifications:', subscription);
      // Optionally, send new subscription details to your server
    })
    .catch(function(error) {
      console.error('Failed to resubscribe:', error);
    });
}

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

建立推播訂閱後,您需要將其傳送至伺服器。您可以自行決定如何執行這項操作,但小小提示:請使用 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 詳細資料,我們就能隨時傳送訊息給使用者。

定期續訂,避免訂閱項目過期

訂閱推播通知時,您通常會收到 nullPushSubscription.expirationTime。理論上,這表示訂閱項目永遠不會到期 (與收到 DOMHighResTimeStamp 不同,後者會顯示訂閱項目的確切到期時間)。但實際上,瀏覽器仍會使訂閱到期,例如長時間沒有收到推播通知,或是瀏覽器偵測到使用者未使用具備推播通知權限的應用程式時。為了避免這種情況,其中一種模式是在每則收到通知時重新訂閱使用者,如以下程式碼片段所示。您必須頻繁地傳送通知,瀏覽器才不會自動在訂閱到期。此外,請務必審慎評估正當通知所需要的優缺點,避免使用者濫發垃圾郵件,以免訂閱項目失效。最後,您不應試圖與瀏覽器對抗,因為瀏覽器會盡力保護使用者免於訂閱已遺忘多時的通知。

/* In the Service Worker. */

self.addEventListener('push', function(event) {
  console.log('Received a push message', event);

  // Display notification or handle data
  // Example: show a notification
  const title = 'New Notification';
  const body = 'You have new updates!';
  const icon = '/images/icon.png';
  const tag = 'simple-push-demo-notification-tag';

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

  // Attempt to resubscribe after receiving a notification
  event.waitUntil(resubscribeToPush());
});

function resubscribeToPush() {
  return self.registration.pushManager.getSubscription()
    .then(function(subscription) {
      if (subscription) {
        return subscription.unsubscribe();
      }
    })
    .then(function() {
      return self.registration.pushManager.subscribe({
        userVisibleOnly: true,
        applicationServerKey: urlBase64ToUint8Array('YOUR_PUBLIC_VAPID_KEY_HERE')
      });
    })
    .then(function(subscription) {
      console.log('Resubscribed to push notifications:', subscription);
      // Optionally, send new subscription details to your server
    })
    .catch(function(error) {
      console.error('Failed to resubscribe:', error);
    });
}

常見問題

以下是目前常見的問題:

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

否。瀏覽器會選取推播服務,如同我們在 subscribe() 呼叫中看到的,瀏覽器會向推播服務提出網路要求,以便擷取構成 PushSubscription 的詳細資料。

每種瀏覽器都使用不同的推送服務,沒有不同的 API 嗎?

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

這個常見的 API 稱為「Web Push Protocol」,可說明應用程式需要發出哪些網路要求,才能觸發推播訊息。

如果在電腦上訂閱使用者,他們是否也會在手機上訂閱?

很抱歉,不行。使用者必須在想接收訊息的每個瀏覽器上註冊推播功能。另外值得一提的是,使用者必須在每部裝置上授予權限。

後續步驟

程式碼研究室