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

Matt Gaunt

使用網頁推送時,其中一個問題點是觸發推送訊息非常「巧妙」。如要觸發推送訊息,應用程式必須遵循網路推送通訊協定,向推送服務發出 POST 要求。如要在所有瀏覽器中使用推送功能,您必須使用 VAPID (又稱為應用程式伺服器金鑰),基本上必須在標頭中設定標頭,以證明應用程式可以向使用者傳送訊息。如要透過推送訊息傳送資料,資料必須加密,並需要新增特定標頭,才能讓瀏覽器正確解密訊息。

觸發推送的主要問題在於,如果發生問題,就很難診斷問題所在。這種方式能夠改善支援的瀏覽器時間和更多瀏覽器,但過程實在困難重重。因此,我們強烈建議使用程式庫處理推送訊息的加密、格式和觸發作業。

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

在本節中,我們將使用 Web-push Node 程式庫。其他語言的差異在於,但不會過於相似。我們之所以要查看 Node,是因為這個 JavaScript 是 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 的新手。

在討論訂閱使用者時,我們曾在 subscribe() 選項中加入 applicationServerKey。在後端需要這組私密金鑰。

在示範中,這些值會新增至 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 模組便可供使用,下一步就是觸發推送訊息。

示範使用虛擬管理面板來觸發推送訊息。

「管理」頁面的螢幕截圖。

按一下「Trigger Push Message」按鈕,就會向 /api/trigger-push-msg/ 發出 POST 要求,這是後端傳送推送訊息的信號,因此我們會針對此端點建立明確路徑:

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() 函式就能使用網頁推送程式庫,將訊息傳送至提供的訂閱項目。

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() 會傳回 promise。如果訊息傳送成功,承諾即可解決,而無須採取任何行動。如果承諾遭拒,您必須檢查錯誤,以便我們通知您 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、...),導入推送的步驟都相同。

接下來,這些網頁推送程式庫到底有哪些工作?

後續步驟

程式碼研究室