訂閱使用者

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

透過功能偵測,我們知道系統支援服務工作者和推播。接下來,我們要「註冊」服務工作者。

註冊服務工作者時,我們會告訴瀏覽器服務工作者檔案的位置。檔案仍只是 JavaScript,但瀏覽器會「授予」服務工作者 API 存取權,包括推送。更準確地說,瀏覽器會在服務工作者環境中執行檔案。

如要註冊服務工作者,請呼叫 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. 下載服務工作者檔案。

  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 的概念,這項 API 會根據網頁應用程式的用量,允許網頁應用程式傳送一定數量的靜默推播訊息。

applicationServerKey 選項

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

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

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

下圖說明這些步驟。

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

插圖:在 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」,說明應用程式需要發出網路要求才能觸發推播訊息。

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

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

後續步驟

程式碼研究室