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ủ sẽ quản lý danh sách các gói thuê bao đẩy và gửi thông báo cho những 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ẽ thực hiện chức năng phía máy chủ.

Phối lại ứng dụng mẫu và xem ứng dụng đó trong thẻ mới

Thông báo sẽ tự động bị chặn khỏi ứng dụng trục trặc được nhúng, do đó 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 cần làm:

  1. Nhấp vào Phối lại để chỉnh sửa để có thể chỉnh sửa dự án.
  2. Để xem trước trang web, hãy nhấn vào Xem ứng dụng. Sau đó nhấn Toàn màn hình toàn màn hình.

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

Khi tham gia lớp học lập trình này, hãy thay đổi mã trong sự cố được 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ã nguồn của ứng dụng

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

Trong thẻ Chrome mới:

  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. Hãy 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 Chrome để biết kết quả).

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

    • Đăng ký đẩy tạo ra đăng ký đẩy. Dịch vụ 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 (sẽ tìm hiểu thêm về nội dung này ở phần sau), vì vậy, bạn chưa thể nhấp vào trình chạy 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 sẽ yêu cầu máy chủ gửi thông báo đến 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. Có thể 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 cùng 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 có sự cố.

  • Trong ứng dụng nhiễu, hãy nhấp vào Tools (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 thuê bao hiện tại hoặc Thông báo cho tất cả các kênh đăng ký trong giao diện người dùng đang hoạt động của ứng dụng, thì bạn cũng sẽ thấy thông báo sau:

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

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

  • public/index.js chứa mã ứng dụng khách hoàn chỉnh. 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 việc người dùng đăng ký nhận thông báo đẩy. Thao tác này cũng gửi thông tin về các gói thuê bao mới và đã bị xoá cho máy chủ.

    Vì bạn sẽ chỉ làm việc về 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 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 khi gửi thông báo.

  • server.js là tệp mà bạn sẽ làm hầu hết mọi việc của mình trong lớp học lập trình này.

    Đoạn mã khởi đầu sẽ tạo một máy chủ web Express đơn giản. Có 4 mục TODO (Việc cần làm) dành cho bạn, được đánh dấu trong nhận xét trong 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 xử lý từng mục TODO (VIỆC CẦN LÀM).

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

Mục VIỆC CẦN LÀM đầu tiên của bạn là tạo chi tiết VAPID, thêm các chi tiết đó vào biến môi trường Node.js, rồi cập nhật mã ứng dụng và mã máy chủ với các giá trị mới.

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ó tên là Nhận dạng máy chủ ứng dụng tự nguyện cho thông báo đẩy trên web (VAPID). VAPID sử dụng tiêu chuẩn mã hoá khoá công khai để xác minh danh tính của ứ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

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

  1. 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 xoá nhận xét trong 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 của bạn, ứng dụng sẽ xuất các khoá đã tạo vào nhật ký Node.js trong giao diện Node.js (không phải đối với Bảng điều khiển Chrome). Để xem các khoá VAPID, hãy chọn Tools -> (Công cụ ->) Nhật ký trong giao diện nhiễu.

    Hãy nhớ sao chép khoá công khai và riêng tư từ cùng một cặp khoá!

    Sự 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 mà bạn tạo có thể cuộn ra ngoà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 các khoá VAPID. Đặt 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 hai dòng mã đó một lần nữa 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 chi tiết về 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 khách.

    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á 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ông báo và chỉ định phương thức mã hóa nội dung.

Trong lớp học lập trình này, bạn chỉ sử dụng 2 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) thiết lập thời gian 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 sau 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 hàm hứa hẹn, nên bạn có thể dễ dàng thêm cách xử lý lỗi.

Trong server.js, hãy sửa đổi hàm sendNotifications một lần nữa:

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ý các gói thuê bao mới

Thông tin khái quát

Sau đây là những điều sẽ 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ý đẩ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 dạng chuỗi trong phần nội dung.

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

    Cơ sở dữ liệu này 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: { ... }
      },
    }

Giờ đây, máy chủ đã có thể gửi thông báo của gói thuê bao mới.

Triển khai

Các yêu cầu về gói thuê bao mới sẽ được đưa đến tuyến /add-subscription, đó là URL POST. Bạn sẽ thấy một trình xử lý định 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 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 các 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ý định tuyến 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ý trường hợp 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ề những 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ủ tránh gửi nhiều thông báo đến các điểm cuối không tồn tại. Rõ ràng đ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 nó trở nên quan trọng ở quy mô lớn hơn.

Triển khai

Yêu cầu huỷ gói thuê bao sẽ được gửi đến URL POST /remove-subscription.

Trình xử lý định 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ỷ từ phần nội dung của yêu cầu.
  • Truy cập 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ý các trường hợp huỷ gói thuê bao:

  • Trong server.js, hãy sửa đổi trình xử lý định tuyến 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);
  });