Xây dựng máy chủ thông báo đẩy

Trong lớp học lập trình này, bạn sẽ xây dựng một máy chủ thông báo đẩy. Máy chủ này sẽ quản lý danh sách các gói thuê bao đẩy và gửi thông báo cho các gói thuê bao đó.

Mã ứng dụng khách đã hoàn tất – trong lớp học lập trình này, bạn sẽ tìm hiểu về chức năng phía máy chủ.

Phối lại ứng dụng mẫu rồi xem trong thẻ mới

Thông báo tự động bị chặn trong ứng dụng Nhiễu dạng đã nhúng, vì vậy, bạn sẽ không thể xem trước ứng dụng trên trang này. Thay vào đó, dưới đây là những việc nên làm:

  1. Nhấp vào Remix để chỉnh sửa (Remix) để chỉnh sửa dự án.
  2. Để xem trước trang web, hãy nhấn vào View App (Xem ứng dụng), sau đó nhấn vào Fullscreen toàn màn hình (Toàn màn hình).

Ứng dụng trực tiếp sẽ mở trong một thẻ Chrome mới. Trong Nhược điểm được nhúng, hãy nhấp vào Xem nguồn để hiển thị lại mã.

Khi bạn thực hiện lớp học lập trình này, hãy thực hiện các thay đổi đối với mã trong Glitch đã nhúng trên trang này. Hãy làm mới thẻ mới bằng ứng dụng đang hoạt động để xem các thay đổi.

Làm quen với ứng dụng khởi động và mã của ứng dụng

Hãy bắt đầu bằng cách xem giao diện người dùng của ứng dụng.

Trong thẻ mới của Chrome:

  1. Nhấn tổ hợp phím "Control+Shift+J" (hoặc "Command+Option+J" trên máy Mac) để mở Công cụ cho nhà phát triển. Nhấp vào thẻ Bảng điều khiển.

  2. Thử nhấp vào các nút trong giao diện người dùng (kiểm tra bảng điều khiển dành cho nhà phát triển của Chrome để biết kết quả).

    • Đăng ký trình chạy dịch vụ đăng ký một trình chạy dịch vụ cho phạm vi URL của dự án Glitch. Huỷ đăng ký trình chạy dịch vụ sẽ xoá trình chạy dịch vụ. Nếu liên kết với một gói thuê bao thông báo đẩy, gói thuê bao thông báo đẩy cũng sẽ bị huỷ kích hoạt.

    • Tính năng Đăng ký đẩy sẽ tạo một gói thuê bao đẩy. Mã này chỉ có sẵn khi một trình chạy dịch vụ đã được đăng ký và hằng số VAPID_PUBLIC_KEY có trong mã ứng dụng (tìm hiểu thêm về nội dung này sau), vì vậy bạn chưa thể nhấp vào hằng số này.

    • Khi bạn có một gói thuê bao đẩy đang hoạt động, tuỳ chọn Thông báo cho gói thuê bao hiện tại sẽ yêu cầu máy chủ gửi thông báo đến điểm cuối.

    • Thông báo cho tất cả các gói thuê bao yêu cầu máy chủ gửi thông báo cho tất cả điểm cuối của gói thuê bao trong cơ sở dữ liệu.

      Xin lưu ý rằng một vài điểm cuối trong số này có thể không hoạt động. Luôn có khả năng một gói thuê bao sẽ biến mất vào thời điểm máy chủ gửi thông báo đến gói thuê bao đó.

Hãy xem điều gì đang xảy ra ở phía máy chủ. Để xem thông báo từ mã máy chủ, hãy xem nhật ký Node.js trong giao diện Glitch.

  • Trong ứng dụng Glitch, hãy nhấp vào Tools -> Logs (Công cụ -> Nhật ký).

    Bạn có thể sẽ thấy một thông báo tương tự như Listening on port 3000.

    Nếu đã thử nhấp vào Thông báo cho gói đăng ký hiện tại hoặc Thông báo tất cả các gói đăng ký trong giao diện người dùng của ứng dụng đang hoạt động, bạn cũng sẽ thấy thông báo sau:

    TODO: Implement sendNotifications()
    Endpoints to send to:  []
    

Giờ hãy xem một số mã.

  • public/index.js chứa mã ứng dụng khách đã hoàn tất. Dịch vụ này thực hiện việc phát hiện tính năng, đăng ký và huỷ đăng ký trình chạy dịch vụ, đồng thời kiểm soát hoạt động đăng ký nhận thông báo đẩy của người dùng. Mã này cũng gửi thông tin về các gói thuê bao mới và đã bị xoá đến máy chủ.

    Vì bạn sẽ chỉ xử lý chức năng của máy chủ nên bạn sẽ không chỉnh sửa tệp này (ngoài việc điền hằng số VAPID_PUBLIC_KEY).

  • public/service-worker.js là một trình chạy dịch vụ đơn giản giúp ghi lại các sự kiện đẩy và hiển thị thông báo.

  • /views/index.html chứa giao diện người dùng của ứng dụng.

  • .env chứa các biến môi trường mà Glitch tải vào máy chủ ứng dụng của bạn khi khởi động. Bạn sẽ điền thông tin xác thực vào .env để gửi thông báo.

  • server.js là tệp mà bạn sẽ thực hiện hầu hết công việc trong lớp học lập trình này.

    Mã khởi đầu sẽ tạo một máy chủ web Express đơn giản. Có 4 mục TODO cho bạn, được đánh dấu trong nhận xét mã bằng TODO:. Bạn cần:

    Trong lớp học lập trình này, bạn sẽ lần lượt tìm hiểu từng mục TODO sau.

Tạo và tải thông tin chi tiết về VAPID

Mục TODO đầu tiên của bạn là tạo thông tin chi tiết về VAPID, thêm chúng vào biến môi trường Node.js rồi cập nhật các giá trị mới cho mã máy khách và máy chủ.

Thông tin khái quát

Khi người dùng đăng ký nhận thông báo, họ cần tin tưởng danh tính của ứng dụng và máy chủ của ứng dụng. Người dùng cũng cần yên tâm rằng khi họ nhận được thông báo thì thông báo đó đến từ chính ứng dụng đã thiết lập gói thuê bao. Họ cũng cần tin tưởng rằng không ai khác có thể đọc nội dung thông báo.

Giao thức giúp thông báo đẩy trở nên an toàn và riêng tư được gọi là Nhận dạng máy chủ ứng dụng tự nguyện cho tính năng đẩy web (VAPID). VAPID sử dụng tiêu chuẩn mã hoá khoá công khai để xác minh danh tính của các ứng dụng, máy chủ và điểm cuối của gói thuê bao, cũng như để mã hoá nội dung thông báo.

Trong ứng dụng này, bạn sẽ sử dụng gói npm đẩy trên web để tạo khoá VAPID cũng như để mã hoá và gửi thông báo.

Triển khai

Trong bước này, hãy tạo một cặp khoá VAPID cho ứng dụng của bạn và thêm các khoá đó vào biến môi trường. Tải các biến môi trường trong máy chủ và thêm khoá công khai làm hằng số trong mã ứng dụng.

  1. Sử dụng hàm generateVAPIDKeys của thư viện web-push để tạo một cặp khoá VAPID.

    Trong server.js, hãy xóa nhận xét khỏi các dòng mã sau:

    server.js

    // Generate VAPID keys (only do this once).
    /*
     * const vapidKeys = webpush.generateVAPIDKeys();
     * console.log(vapidKeys);
     */
    const vapidKeys = webpush.generateVAPIDKeys();
    console.log(vapidKeys);
    
  2. Sau khi khởi động lại ứng dụng, Glitch sẽ xuất các khoá đã tạo vào nhật ký Node.js trong giao diện Glitch (không phải trong bảng điều khiển Chrome). Để xem các khoá VAPID, hãy chọn Tools -> Logs (Công cụ -> Nhật ký) trong giao diện Glitch.

    Hãy đảm bảo rằng bạn sao chép cả khoá công khai và riêng tư từ cùng một cặp khoá!

    Sự cố trục trặc sẽ khởi động lại ứng dụng của bạn mỗi khi bạn chỉnh sửa mã, vì vậy, cặp khoá đầu tiên bạn tạo có thể cuộn ra khỏi khung hiển thị khi có thêm kết quả tiếp theo.

  3. Trong .env, hãy sao chép và dán khoá VAPID. Đặt các khoá trong dấu ngoặc kép ("...").

    Đối với VAPID_SUBJECT, bạn có thể nhập "mailto:test@test.test".

    .env

    # process.env.SECRET
    VAPID_PUBLIC_KEY=
    VAPID_PRIVATE_KEY=
    VAPID_SUBJECT=
    VAPID_PUBLIC_KEY="BN3tWzHp3L3rBh03lGLlLlsq..."
    VAPID_PRIVATE_KEY="I_lM7JMIXRhOk6HN..."
    VAPID_SUBJECT="mailto:test@test.test"
    
  4. Trong server.js, hãy đánh dấu lại hai dòng mã đó, vì bạn chỉ cần tạo khoá VAPID một lần.

    server.js

    // Generate VAPID keys (only do this once).
    /*
    const vapidKeys = webpush.generateVAPIDKeys();
    console.log(vapidKeys);
    */
    const vapidKeys = webpush.generateVAPIDKeys();
    console.log(vapidKeys);
    
  5. Trong server.js, hãy tải thông tin VAPID từ các biến môi trường.

    server.js

    const vapidDetails = {
      // TODO: Load VAPID details from environment variables.
      publicKey: process.env.VAPID_PUBLIC_KEY,
      privateKey: process.env.VAPID_PRIVATE_KEY,
      subject: process.env.VAPID_SUBJECT
    }
    
  6. Đồng thời, sao chép và dán khoá công khai vào mã ứng dụng.

    Trong public/index.js, hãy nhập cùng một giá trị cho VAPID_PUBLIC_KEY mà bạn đã sao chép vào tệp .env:

    public/index.js

    // Copy from .env
    const VAPID_PUBLIC_KEY = '';
    const VAPID_PUBLIC_KEY = 'BN3tWzHp3L3rBh03lGLlLlsq...';
    ````
    

Triển khai chức năng gửi thông báo

Thông tin khái quát

Trong ứng dụng này, bạn sẽ sử dụng gói npm đẩy trên web để gửi thông báo.

Gói này tự động mã hoá các thông báo khi webpush.sendNotification() được gọi, vì vậy bạn không cần phải lo lắng về điều đó.

web-push chấp nhận nhiều tùy chọn cho thông báo – ví dụ: bạn có thể đính kèm tiêu đề vào thư và chỉ định mã hóa nội dung.

Trong lớp học lập trình này, bạn sẽ chỉ sử dụng hai tuỳ chọn được xác định bằng các dòng mã sau:

let options = {
  TTL: 10000; // Time-to-live. Notifications expire after this.
  vapidDetails: vapidDetails; // VAPID keys from .env
};

Tuỳ chọn TTL (thời gian tồn tại) đặt thời gian chờ hết hạn trên một thông báo. Đây là một cách để máy chủ tránh gửi thông báo cho người dùng khi thông báo đó không còn phù hợp.

Tuỳ chọn vapidDetails chứa các khoá VAPID mà bạn đã tải từ các biến môi trường.

Triển khai

Trong server.js, hãy sửa đổi hàm sendNotifications như sau:

server.js

function sendNotifications(database, endpoints) {
  // TODO: Implement functionality to send notifications.
  console.log('TODO: Implement sendNotifications()');
  console.log('Endpoints to send to: ', endpoints);
  let notification = JSON.stringify(createNotification());
  let options = {
    TTL: 10000, // Time-to-live. Notifications expire after this.
    vapidDetails: vapidDetails // VAPID keys from .env
  };
  endpoints.map(endpoint => {
    let subscription = database[endpoint];
    webpush.sendNotification(subscription, notification, options);
  });
}

webpush.sendNotification() trả về một lời hứa nên bạn có thể dễ dàng bổ sung chức năng xử lý lỗi.

Trong server.js, hãy sửa đổi lại hàm sendNotifications:

server.js

function sendNotifications(database, endpoints) {
  let notification = JSON.stringify(createNotification());
  let options = {
    TTL: 10000; // Time-to-live. Notifications expire after this.
    vapidDetails: vapidDetails; // VAPID keys from .env
  };
  endpoints.map(endpoint => {
    let subscription = database[endpoint];
    webpush.sendNotification(subscription, notification, options);
    let id = endpoint.substr((endpoint.length - 8), endpoint.length);
    webpush.sendNotification(subscription, notification, options)
    .then(result => {
      console.log(`Endpoint ID: ${id}`);
      console.log(`Result: ${result.statusCode} `);
    })
    .catch(error => {
      console.log(`Endpoint ID: ${id}`);
      console.log(`Error: ${error.body} `);
    });
  });
}

Xử lý gói thuê bao mới

Thông tin khái quát

Sau đây là những gì xảy ra khi người dùng đăng ký nhận thông báo đẩy:

  1. Người dùng nhấp vào Đăng ký để gửi thông báo đẩy.

  2. Ứng dụng sử dụng hằng số VAPID_PUBLIC_KEY (khoá VAPID công khai của máy chủ) để tạo một đối tượng subscription duy nhất dành riêng cho máy chủ. Đối tượng subscription sẽ có dạng như sau:

       {
         "endpoint": "https://fcm.googleapis.com/fcm/send/cpqAgzGzkzQ:APA9...",
         "expirationTime": null,
         "keys":
         {
           "p256dh": "BNYDjQL9d5PSoeBurHy2e4d4GY0sGJXBN...",
           "auth": "0IyyvUGNJ9RxJc83poo3bA"
         }
       }
    
  3. Ứng dụng gửi yêu cầu POST đến URL /add-subscription, bao gồm cả gói thuê bao dưới dạng JSON có chuỗi trong phần nội dung.

  4. Máy chủ truy xuất subscription đã chuỗi được từ phần nội dung của yêu cầu POST, phân tích cú pháp yêu cầu trở lại JSON và thêm vào cơ sở dữ liệu gói thuê bao.

    Cơ sở dữ liệu lưu trữ các gói thuê bao bằng cách sử dụng điểm cuối riêng làm khoá:

    {
      "https://fcm...1234": {
        endpoint: "https://fcm...1234",
        expirationTime: ...,
        keys: { ... }
      },
      "https://fcm...abcd": {
        endpoint: "https://fcm...abcd",
        expirationTime: ...,
        keys: { ... }
      },
      "https://fcm...zxcv": {
        endpoint: "https://fcm...zxcv",
        expirationTime: ...,
        keys: { ... }
      },
    }

Hiện tại, máy chủ đã có thể gửi thông báo cho gói thuê bao mới.

Triển khai

Các yêu cầu về gói thuê bao mới sẽ chuyển đến tuyến /add-subscription, đây là URL POST. Bạn sẽ thấy trình xử lý tuyến mã giả lập trong server.js:

server.js

app.post('/add-subscription', (request, response) => {
  // TODO: implement handler for /add-subscription
  console.log('TODO: Implement handler for /add-subscription');
  console.log('Request body: ', request.body);
  response.sendStatus(200);
});

Trong quá trình triển khai, trình xử lý này phải:

  • Truy xuất gói thuê bao mới từ phần nội dung của yêu cầu.
  • Truy cập vào cơ sở dữ liệu về các gói thuê bao đang hoạt động.
  • Thêm gói thuê bao mới vào danh sách gói thuê bao đang hoạt động.

Cách xử lý các gói thuê bao mới:

  • Trong server.js, hãy sửa đổi trình xử lý tuyến đường cho /add-subscription như sau:

    server.js

    app.post('/add-subscription', (request, response) => {
      // TODO: implement handler for /add-subscription
      console.log('TODO: Implement handler for /add-subscription');
      console.log('Request body: ', request.body);
      let subscriptions = Object.assign({}, request.session.subscriptions);
      subscriptions[request.body.endpoint] = request.body;
      request.session.subscriptions = subscriptions;
      response.sendStatus(200);
    });

Xử lý hoạt động huỷ gói thuê bao

Thông tin khái quát

Không phải lúc nào máy chủ cũng biết khi nào một gói thuê bao chuyển sang trạng thái không hoạt động – ví dụ: gói thuê bao có thể bị xoá khi trình duyệt tắt trình chạy dịch vụ.

Tuy nhiên, máy chủ có thể tìm hiểu về các gói thuê bao bị huỷ thông qua giao diện người dùng của ứng dụng. Ở bước này, bạn sẽ triển khai chức năng xoá gói thuê bao khỏi cơ sở dữ liệu.

Bằng cách này, máy chủ sẽ tránh gửi một loạt thông báo đến các điểm cuối không tồn tại. Rõ ràng là điều này không thực sự quan trọng với một ứng dụng kiểm thử đơn giản nhưng lại quan trọng ở quy mô lớn hơn.

Triển khai

Các yêu cầu huỷ gói thuê bao sẽ chuyển đến URL POST của /remove-subscription.

Trình xử lý tuyến mã giả lập trong server.js có dạng như sau:

server.js

app.post('/remove-subscription', (request, response) => {
  // TODO: implement handler for /remove-subscription
  console.log('TODO: Implement handler for /remove-subscription');
  console.log('Request body: ', request.body);
  response.sendStatus(200);
});

Trong quá trình triển khai, trình xử lý này phải:

  • Truy xuất điểm cuối của gói thuê bao đã huỷ trong phần nội dung của yêu cầu.
  • Truy cập vào cơ sở dữ liệu về các gói thuê bao đang hoạt động.
  • Xoá gói thuê bao đã huỷ khỏi danh sách các gói thuê bao đang hoạt động.

Phần nội dung của yêu cầu POST từ ứng dụng chứa điểm cuối mà bạn cần xoá:

{
  "endpoint": "https://fcm.googleapis.com/fcm/send/cpqAgzGzkzQ:APA9..."
}

Cách xử lý trường hợp huỷ gói thuê bao:

  • Trong server.js, hãy sửa đổi trình xử lý tuyến đường cho /remove-subscription như sau:

    server.js

  app.post('/remove-subscription', (request, response) => {
    // TODO: implement handler for /remove-subscription
    console.log('TODO: Implement handler for /remove-subscription');
    console.log('Request body: ', request.body);
    let subscriptions = Object.assign({}, request.session.subscriptions);
    delete subscriptions[request.body.endpoint];
    request.session.subscriptions = subscriptions;
    response.sendStatus(200);
  });