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

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 清單,並執行程式碼來觸發推送訊息。

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

我將逐步說明如何讓這個示範功能運作。我們將逐步引導完成這些步驟,讓所有人都能跟上腳步,包括新手節點的使用者。

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

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

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

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

npm install web-push --save

接著,在節點指令碼中,我們需要使用類似下方的 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、...),實作推送作業的步驟都相同。

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

後續步驟

程式碼研究室