Gửi thông báo bằng thư viện đẩy của web

Một trong những điểm khó khăn khi làm việc với tính năng đẩy trên web là việc kích hoạt thông báo đẩy cực kỳ "bất ngờ". Để kích hoạt thông báo đẩy, ứng dụng cần gửi yêu cầu POST tới một dịch vụ đẩy theo giao thức đẩy trên web. Để sử dụng tính năng đẩy trên tất cả các trình duyệt, bạn cần sử dụng VAPID (còn gọi là khoá máy chủ ứng dụng). Về cơ bản, bạn cần đặt một tiêu đề có giá trị chứng minh rằng ứng dụng của bạn có thể nhắn tin cho người dùng. Để gửi dữ liệu bằng thông báo đẩy, dữ liệu cần được mã hoá và cần thêm các tiêu đề cụ thể để trình duyệt có thể giải mã thông báo chính xác.

Vấn đề chính với việc kích hoạt thao tác đẩy là khi gặp phải vấn đề, bạn sẽ gặp khó khăn trong việc chẩn đoán vấn đề đó. Điều này sẽ được cải thiện theo thời gian và trình duyệt được hỗ trợ rộng hơn nhưng không hề dễ dàng. Vì lý do này, bạn nên sử dụng thư viện để xử lý quá trình mã hoá, định dạng và kích hoạt thông báo đẩy.

Nếu bạn thực sự muốn tìm hiểu về chức năng của các thư viện, chúng tôi sẽ đề cập đến trong phần tiếp theo. Hiện tại, chúng ta sẽ xem xét cách quản lý gói thuê bao và sử dụng một thư viện đẩy web hiện có để đưa ra các yêu cầu đẩy.

Trong phần này, chúng ta sẽ sử dụng thư viện Nút đẩy web. Các ngôn ngữ khác cũng sẽ có điểm khác biệt, nhưng cũng không quá khác biệt. Chúng tôi đang xem xét Nút (Node) vì đó là JavaScript và đây là nút mà độc giả dễ tiếp cận nhất.

Chúng tôi sẽ thực hiện qua các bước sau:

  1. Gửi gói thuê bao tới chương trình phụ trợ của chúng tôi và lưu gói thuê bao đó.
  2. Truy xuất các gói thuê bao đã lưu và kích hoạt một thông báo đẩy.

Đang lưu gói thuê bao

Việc lưu và truy vấn PushSubscription từ một cơ sở dữ liệu sẽ thay đổi tuỳ thuộc vào ngôn ngữ phía máy chủ và lựa chọn cơ sở dữ liệu của bạn. Tuy nhiên, bạn nên xem ví dụ về cách thực hiện.

Trên trang web minh hoạ, PushSubscription được gửi đến phần phụ trợ của chúng tôi bằng cách gửi một yêu cầu POST đơn giản:

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

Máy chủ Express trong bản minh hoạ của chúng ta có một trình nghe yêu cầu trùng khớp cho điểm cuối /api/save-subscription/:

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

Trong lộ trình này, chúng tôi xác thực gói thuê bao chỉ để đảm bảo rằng yêu cầu vẫn hợp lệ và không chứa đầy rác:

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;
};

Nếu gói thuê bao hợp lệ, chúng ta cần lưu và trả về phản hồi JSON thích hợp:

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

Bản minh hoạ này sử dụng nedb để lưu trữ các gói thuê bao. Đây là một cơ sở dữ liệu dựa trên tệp đơn giản, nhưng bạn có thể dùng bất kỳ cơ sở dữ liệu nào tuỳ ý. Chúng tôi chỉ sử dụng tính năng này vì nó không yêu cầu thiết lập. Để phát hành chính thức, bạn nên sử dụng công cụ đáng tin cậy hơn. (Tôi có xu hướng sử dụng MySQL cũ tốt.)

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

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

Đang gửi thông báo đẩy

Để gửi thông báo đẩy, cuối cùng chúng ta cần có một số sự kiện để kích hoạt quá trình gửi thông báo cho người dùng. Một phương pháp phổ biến là tạo trang quản trị cho phép bạn định cấu hình và kích hoạt thông báo đẩy. Tuy nhiên, bạn có thể tạo một chương trình để chạy cục bộ hoặc bất kỳ phương pháp nào khác cho phép truy cập vào danh sách của PushSubscription và chạy mã để kích hoạt thông báo đẩy.

Bản minh hoạ của chúng ta có trang "thích quản trị viên" cho phép bạn kích hoạt lệnh đẩy. Vì đây chỉ là bản minh hoạ nên một trang công khai.

Tôi sẽ trình bày từng bước liên quan đến việc hoạt động của bản minh hoạ. Đây sẽ là các bước nhỏ để mọi người có thể theo dõi, bao gồm cả những người mới sử dụng Nút.

Khi thảo luận về cách đăng ký cho người dùng, chúng ta đã đề cập đến việc thêm applicationServerKey vào các tuỳ chọn subscribe(). Trên giao diện người dùng, chúng ta sẽ cần khoá riêng tư này.

Trong bản minh hoạ, các giá trị này được thêm vào ứng dụng Nút như vậy (tôi biết mã nhàm chán, nhưng tôi muốn bạn biết rằng không có kỳ diệu nào):

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

Tiếp theo, chúng ta cần cài đặt mô-đun web-push cho máy chủ Nút:

npm install web-push --save

Sau đó, trong tập lệnh Nút, chúng tôi yêu cầu mô-đun web-push như sau:

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

Bây giờ, chúng ta có thể bắt đầu sử dụng mô-đun web-push. Trước tiên, chúng ta cần cho mô-đun web-push biết về khoá máy chủ ứng dụng. (Hãy nhớ rằng chúng còn được gọi là khoá VAPID vì đó là tên của quy cách.)

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

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

Xin lưu ý rằng chúng tôi cũng đã đưa vào một chuỗi "mailto:". Chuỗi này cần là một URL hoặc địa chỉ email mailto. Thông tin này sẽ thực sự được gửi đến dịch vụ đẩy web trong yêu cầu kích hoạt quá trình đẩy. Lý do là để nếu một dịch vụ đẩy web cần liên hệ với người gửi, họ sẽ có một số thông tin để hỗ trợ.

Với mô-đun này, mô-đun web-push đã sẵn sàng để sử dụng, bước tiếp theo là kích hoạt một thông báo đẩy.

Bản minh hoạ sử dụng bảng điều khiển quản trị giả vờ để kích hoạt thông báo đẩy.

Ảnh chụp màn hình trang quản trị.

Thao tác nhấp vào nút "Kích hoạt thông báo đẩy" sẽ gửi yêu cầu POST tới /api/trigger-push-msg/. Đây là tín hiệu để phần phụ trợ của chúng tôi gửi thông báo đẩy, vì vậy, chúng ta sẽ tạo tuyến đường nhanh cho điểm cuối này:

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

Khi nhận được yêu cầu này, chúng tôi sẽ lấy các gói thuê bao từ cơ sở dữ liệu và đối với mỗi gói thuê bao, chúng tôi sẽ kích hoạt một thông báo đẩy.

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;
});

Sau đó, hàm triggerPushMsg() có thể sử dụng thư viện đẩy trên web để gửi thông báo đến gói thuê bao đã cung cấp.

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;
    }
  });
};

Lệnh gọi đến webpush.sendNotification() sẽ trả về một lời hứa. Nếu thông báo được gửi thành công, lời hứa sẽ được giải quyết và chúng ta không cần làm gì. Nếu lời hứa từ chối, bạn cần kiểm tra lỗi vì nó sẽ cho bạn biết liệu PushSubscription có còn hợp lệ hay không.

Để xác định loại lỗi của một dịch vụ đẩy, tốt nhất bạn nên xem mã trạng thái. Các thông báo lỗi giữa các dịch vụ đẩy sẽ có sự khác biệt, và một số thông báo lỗi hữu ích hơn so với các dịch vụ khác.

Trong ví dụ này, công cụ này sẽ kiểm tra các mã trạng thái 404410, đây là các mã trạng thái HTTP cho "Not Found" (Không tìm thấy) và "Gone" (Không tồn tại). Nếu chúng tôi nhận được một trong những thông báo như vậy, thì tức là gói thuê bao đã hết hạn hoặc không còn hợp lệ. Trong các trường hợp này, chúng ta cần xoá gói thuê bao khỏi cơ sở dữ liệu.

Trong trường hợp xảy ra một số lỗi khác, chúng ta chỉ throw err để thực hiện lời hứa được trả về bằng cách từ chối triggerPushMsg().

Chúng ta sẽ đề cập đến một số mã trạng thái khác trong phần tiếp theo khi xem xét chi tiết hơn về giao thức đẩy web.

Sau khi lặp lại các gói thuê bao, chúng ta cần trả về phản hồi 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}'`
    }
}));
});

Chúng ta đã đi qua các bước triển khai chính:

  1. Tạo một API để gửi gói thuê bao từ trang web đến hệ thống phụ trợ của chúng tôi nhằm có thể lưu các gói thuê bao đó vào cơ sở dữ liệu.
  2. Tạo một API để kích hoạt việc gửi thông báo đẩy (trong trường hợp này là API được gọi từ bảng điều khiển quản trị giả mạo).
  3. Truy xuất tất cả gói thuê bao từ phần phụ trợ của chúng tôi rồi gửi thông báo đến từng gói thuê bao bằng một trong các thư viện đẩy web.

Bất kể phần phụ trợ của bạn là gì (Node, PHP, Python, ...), các bước triển khai chế độ đẩy đều giống nhau.

Tiếp theo, chính xác thì các thư viện đẩy web này có tác dụng gì cho chúng ta?

Điểm đến tiếp theo

Lớp học lập trình