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 các gói thuê bao đó.

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

Thông báo sẽ tự động bị chặn khỏi ứng dụng Glitch được 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 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 vào biểu tượng Màn hình toàn cảnh toàn màn hình.

Ứng dụng trực tiếp sẽ mở trong một thẻ Chrome mới. Trong Nhiễu đượ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 phần Nhiễu được nhúng trên trang này. Làm mới thẻ mới bằng ứng dụng đang chạy để 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 ứng dụng.

Trong thẻ Chrome mới:

  1. Nhấn tổ hợp phím `Ctrl+Shift+J` (hoặc `Command+Option+J` trên máy Mac) để mở DevTools. 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 để xem 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 gặp sự cố. Huỷ đăng ký trình chạy dịch vụ sẽ xoá trình chạy dịch vụ. Nếu một gói thuê bao đẩy được đính kèm vào gói thuê bao đó, thì gói thuê bao đẩy cũng sẽ bị vô hiệu hoá.

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

    • Khi bạn có gói thuê bao đẩy đang hoạt động, Thông báo về 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 của gói thuê bao đó.

    • Notify all subscriptions (Thông báo cho tất cả gói thuê bao) 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 số điểm cuối 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 xem những gì đang diễn 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 nhiễu, hãy nhấp vào Tools -> Logs (Công cụ -> Nhật ký).

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

    Nếu đã thử nhấp vào Thông báo về gói thuê bao hiện tại hoặc Thông báo về tất cả gói thuê bao trong giao diện người dùng ứ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:  []

Bây giờ, hãy xem một số mã.

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

    Vì bạn chỉ làm việc trên chức năng 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 worker 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 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 động 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ý các mục TODO này.

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

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

Thông tin khái quát

Khi đăng ký nhận thông báo, người dùng 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 tin tưởng rằng khi nhận được thông báo, đó là thông báo của 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 web-push để 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 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á các nhận xét xung quanh 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 xuất vào 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 nhấp nháy.

    Hãy nhớ sao chép khoá công khai và khoá 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 chú thích 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 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. 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 web-push để 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 tuỳ 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 cách mã hoá 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 cho một thông báo. Đây là 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 liên quan nữa.

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 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ý 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ý nhận 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 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 chuyển đổi thành 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 lưu trữ các gói thuê bao bằng cách sử dụng các điểm cuối của riêng chú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ý tuyến giả 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 của 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ý 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ề 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 điều này không thực sự quan trọng đối với một ứng dụng kiểm thử đơn giản, nhưng sẽ 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ý tuyến đường giả lập trong server.js sẽ 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 gói thuê bao đang hoạt động.

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ý yêu cầu huỷ gói thuê bao:

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