透過網路推播程式庫傳送訊息

Matt Gaunt

使用網頁推播時,其中一個痛點是觸發推播訊息非常「麻煩」。如要觸發推送訊息,應用程式必須依照 網路推送通訊協定向推送服務提出 POST 要求。如要在所有瀏覽器上使用推播功能,您必須使用 VAPID (又稱為應用程式伺服器金鑰),這項功能基本上需要設定標頭,並提供證明應用程式可傳送訊息給使用者的值。如要透過推播訊息傳送資料,資料必須加密,並且需要加入特定標頭,讓瀏覽器能夠正確解密訊息。

觸發推播的主要問題是,如果發生問題,就很難診斷問題。隨著時間的推移和瀏覽器支援範圍的擴大,這個問題正在改善,但仍不容易解決。因此,強烈建議您使用程式庫來處理推播訊息的加密、格式設定和觸發功能。

如果您真的想瞭解程式庫的運作方式,我們會在下一節說明。我們現在將討論如何管理訂閱項目,以及使用現有的網路推送程式庫發出推送要求。

在本節中,我們將使用 web-push Node 程式庫。其他語言會有些許差異,但不會有太大差異。我們會採用 Node,因為它是 JavaScript,應該是最適合讀者的選擇。

我們將逐步完成以下步驟:

  1. 將訂閱項目傳送至後端並儲存。
  2. 擷取已儲存的訂閱項目,並觸發推送訊息。

儲存訂閱項目

從資料庫儲存及查詢 PushSubscription 的方式會因您選擇的伺服器端語言和資料庫而異,但查看示例可能會有所幫助。

在示範網頁中,我們會透過簡單的 POST 要求,將 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.');
      }
    });
}

在示範中,Express 伺服器會為 /api/save-subscription/ 端點提供相符的請求事件監聽器:

app.post('/api/save-subscription/', function (req, res) {

在這個路徑中,我們會驗證訂閱項目,確保要求沒有問題,且不會充斥垃圾內容:

const isValidSaveRequest = (req, res) => {
  // Check the request body has at least an endpoint.
  if (!req.body || !req.body.endpoint) {
    // Not a valid subscription.
    res.status(400);
    res.setHeader('Content-Type', 'application/json');
    res.send(
      JSON.stringify({
        error: {
          id: 'no-endpoint',
          message: 'Subscription must have an endpoint.',
        },
      }),
    );
    return false;
  }
  return true;
};

如果訂閱項目有效,我們需要儲存該項目並傳回適當的 JSON 回應:

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.',
        },
      }),
    );
  });

這個示範使用 nedb 儲存訂閱項目,這是一個簡單的檔案型資料庫,但您可以使用任何資料庫。我們只使用這個方法,因為它不需要任何設定。如要用於實際工作環境,建議您使用更可靠的工具。(我傾向於使用舊版 MySQL)。

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

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

傳送推送訊息

在傳送推播訊息時,我們最終需要一些事件來觸發傳送訊息給使用者的程序。常見做法是建立管理頁面,讓您設定及觸發推播訊息。不過,您可以建立本機執行程式,或使用任何其他方法存取 PushSubscription 清單,並執行程式碼來觸發推送訊息。

我們的示範內容包含一個「類似管理員」的頁面,可讓您觸發推送。由於這是示範,因此是公開網頁。

我將逐步說明如何讓這個示範功能運作。這些是初階步驟,因此所有人都能跟著操作,包括不熟悉 Node 的使用者。

在討論使用者訂閱時,我們已將 applicationServerKey 新增至 subscribe() 選項。我們需要這個私密金鑰來處理後端作業。

在示範中,這些值會以以下方式新增至 Node 應用程式 (我知道程式碼很無聊,但只是想讓您知道沒有任何神奇之處):

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

接下來,我們需要為 Node 伺服器安裝 web-push 模組:

npm install web-push --save

接著,在 Node 指令碼中要求 web-push 模組,如下所示:

const webpush = require('web-push');

我們現在可以開始使用 web-push 模組。首先,我們需要將應用程式伺服器金鑰告知 web-push 模組。(請注意,這些也稱為 VAPID 鍵,因為這是規格名稱)。

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

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

請注意,我們也加入了「mailto:」字串。這個字串必須是網址或 mailto 電子郵件地址。這項資訊實際上會傳送至網頁推播服務,做為觸發推播要求的一部分。這樣做的原因是,如果網路推播服務需要與傳送者聯絡,就能提供相關資訊,讓對方聯絡傳送者。

完成後,web-push 模組即可使用,下一步是觸發推播訊息。

這個示範會使用假設的管理員面板觸發推播訊息。

管理頁面的螢幕截圖。

按一下「觸發推送訊息」按鈕,系統就會向 /api/trigger-push-msg/ 發出 POST 要求,這是後端傳送推送訊息的信號,因此我們會在 Express 中為這個端點建立路由:

app.post('/api/trigger-push-msg/', function (req, res) {

收到這項要求後,我們會從資料庫擷取訂閱項目,並針對每個訂閱項目觸發推播訊息。

return getSubscriptionsFromDatabase().then(function (subscriptions) {
  let promiseChain = Promise.resolve();

  for (let i = 0; i < subscriptions.length; i++) {
    const subscription = subscriptions[i];
    promiseChain = promiseChain.then(() => {
      return triggerPushMsg(subscription, dataToSend);
    });
  }

  return promiseChain;
});

接著,函式 triggerPushMsg() 就能使用 Web-Push 程式庫,將訊息傳送至提供的訂閱項目。

const triggerPushMsg = function (subscription, dataToSend) {
  return webpush.sendNotification(subscription, dataToSend).catch((err) => {
    if (err.statusCode === 404 || err.statusCode === 410) {
      console.log('Subscription has expired or is no longer valid: ', err);
      return deleteSubscriptionFromDatabase(subscription._id);
    } else {
      throw err;
    }
  });
};

webpush.sendNotification() 的呼叫會傳回承諾。如果訊息已成功傳送,承諾就會解析,我們不需要採取任何行動。如果承諾遭到拒絕,您就需要檢查錯誤,因為系統會告知您 PushSubscription 是否仍有效。

如要判斷推播服務的錯誤類型,建議您查看狀態碼。不同推播服務的錯誤訊息各有不同,有些錯誤訊息比其他訊息更有幫助。

在這個範例中,系統會檢查狀態碼 404410,這是「找不到」和「已移除」的 HTTP 狀態碼。如果我們收到其中一種訊息,表示訂閱項目已過期或不再有效。在這些情況下,我們需要從資料庫中移除訂閱項目。

如果發生其他錯誤,我們會直接 throw err,這會讓 triggerPushMsg() 傳回的承諾遭到拒絕。

在下一節中,我們會進一步探討其他狀態碼,並詳細說明網路推送通訊協定。

迴圈處理訂閱項目後,我們需要傳回 JSON 回應。

.then(() => {
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-send-messages',
    message: `We were unable to send messages to all subscriptions : ` +
        `'${err.message}'`
    }
}));
});

我們已介紹主要的導入步驟:

  1. 建立 API,將網頁上的訂閱項目傳送至後端,以便儲存到資料庫。
  2. 建立 API 來觸發推播訊息傳送作業 (在本例中,這是從假裝的管理員面板呼叫的 API)。
  3. 從後端擷取所有訂閱項目,並使用其中一個 web-push 程式庫,向每個訂閱項目傳送訊息。

無論後端 (Node、PHP、Python 等) 為何,實作推送的步驟都會相同。

接下來,這些網路推播程式庫的作用究竟是什麼?

後續步驟

程式碼研究室