สร้างเซิร์ฟเวอร์ข้อความ Push

ใน Codelab นี้ คุณจะสร้างเซิร์ฟเวอร์ข้อความ Push โดยเซิร์ฟเวอร์จะจัดการรายการการสมัครรับข้อมูลแบบพุชและส่งการแจ้งเตือนไปให้

โค้ดไคลเอ็นต์เสร็จสมบูรณ์แล้ว โดยใน Codelab นี้ คุณจะได้ทำงานด้านฟังก์ชันการทำงานฝั่งเซิร์ฟเวอร์

รีมิกซ์แอปตัวอย่างและดูในแท็บใหม่

การแจ้งเตือนจะถูกบล็อกจากแอป Glitch ที่ฝังไว้โดยอัตโนมัติ คุณจึงดูตัวอย่างแอปในหน้านี้ไม่ได้ แต่ให้ดำเนินการดังนี้

  1. คลิกรีมิกซ์เพื่อแก้ไขเพื่อทำให้โปรเจ็กต์แก้ไขได้
  2. หากต้องการดูตัวอย่างเว็บไซต์ ให้กดดูแอป แล้วกดเต็มหน้าจอ เต็มหน้าจอ

แอปเวอร์ชันที่เผยแพร่อยู่จะเปิดขึ้นในแท็บ Chrome ใหม่ ใน Glitch ที่ฝัง ให้คลิกดูซอร์สโค้ดเพื่อแสดงโค้ดอีกครั้ง

ระหว่างทํางานใน Codelab นี้ ให้ทําการเปลี่ยนแปลงโค้ดใน Glitch ที่ฝังในหน้านี้ รีเฟรชแท็บใหม่ด้วยแอปที่ใช้งานอยู่เพื่อดูการเปลี่ยนแปลง

ทำความคุ้นเคยกับแอปเริ่มต้นและโค้ดของแอป

เริ่มด้วยการดูที่ UI ของไคลเอ็นต์ของแอป

ในแท็บ Chrome ใหม่ ให้ทำดังนี้

  1. กด "Control+Shift+J" (หรือ "Command+Option+J" ใน Mac) เพื่อเปิดเครื่องมือสำหรับนักพัฒนาเว็บ คลิกแท็บคอนโซล

  2. ลองคลิกปุ่มใน UI (ตรวจสอบคอนโซลของนักพัฒนาซอฟต์แวร์ Chrome เพื่อดูเอาต์พุต)

    • ลงทะเบียน Service Worker จะลงทะเบียน Service Worker สำหรับขอบเขตของ URL ของโปรเจ็กต์ Glitch ยกเลิกการลงทะเบียน Service Worker เพื่อนำ Service Worker ออก หากมีการแนบการสมัครใช้บริการพุชอยู่ ระบบจะปิดใช้งานการสมัครใช้บริการพุชดังกล่าวด้วย

    • ติดตามเพื่อพุชจะสร้างการติดตามแบบพุช โดยจะใช้ได้เมื่อมีการลงทะเบียน Service Worker เท่านั้นและมีค่าคงที่ VAPID_PUBLIC_KEY อยู่ในรหัสไคลเอ็นต์ (ดูรายละเอียดเพิ่มเติมในภายหลัง) คุณจึงยังไม่สามารถคลิกได้

    • เมื่อมีการสมัครใช้บริการพุชที่ใช้งานอยู่ ให้แจ้งการสมัครใช้บริการปัจจุบันจะขอให้เซิร์ฟเวอร์ส่งการแจ้งเตือนไปยังปลายทาง

    • แจ้งเตือนการสมัครใช้บริการทั้งหมด บอกให้เซิร์ฟเวอร์ส่งการแจ้งเตือนไปยังปลายทางทั้งหมดของการสมัครใช้บริการในฐานข้อมูล

      โปรดทราบว่าปลายทางบางส่วนเหล่านี้อาจไม่ทำงาน เป็นไปได้ว่าการสมัครรับข้อมูลจะหายไปเมื่อเซิร์ฟเวอร์ส่งการแจ้งเตือน

มาดูกันว่าเกิดอะไรขึ้นในฝั่งเซิร์ฟเวอร์ หากต้องการดูข้อความจากโค้ดเซิร์ฟเวอร์ ให้ดูบันทึก Node.js ภายในอินเทอร์เฟซ Glitch

  • ในแอป Glitch ให้คลิกเครื่องมือ -> บันทึก

    คุณอาจเห็นข้อความที่คล้ายกับ Listening on port 3000

    หากลองคลิกแจ้งการสมัครใช้บริการปัจจุบันหรือแจ้งการติดตามทั้งหมดใน UI ของแอปที่เผยแพร่อยู่ คุณจะเห็นข้อความต่อไปนี้ด้วย

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

ลองมาดูโค้ดกัน

  • public/index.js มีรหัสไคลเอ็นต์ที่สมบูรณ์ การดำเนินการนี้จะตรวจหาฟีเจอร์ ลงทะเบียนและยกเลิกการลงทะเบียน Service Worker และควบคุมการสมัครใช้บริการของผู้ใช้สำหรับข้อความ Push นอกจากนี้ยังส่งข้อมูลเกี่ยวกับการสมัครใช้บริการใหม่และที่ถูกลบไปยังเซิร์ฟเวอร์อีกด้วย

    เนื่องจากคุณจะดำเนินการเฉพาะฟังก์ชันของเซิร์ฟเวอร์เท่านั้น คุณจึงจะไม่แก้ไขไฟล์นี้ (นอกเหนือจากการใส่ค่าคงที่ VAPID_PUBLIC_KEY)

  • public/service-worker.js เป็น Service Worker แบบง่ายที่บันทึกเหตุการณ์พุชและแสดงการแจ้งเตือน

  • /views/index.html มี UI ของแอป

  • .env มีตัวแปรสภาพแวดล้อมที่ Glitch โหลดลงในเซิร์ฟเวอร์แอปของคุณเมื่อเริ่มทำงาน คุณจะป้อนข้อมูลรายละเอียดการตรวจสอบสิทธิ์สำหรับส่งการแจ้งเตือนใน .env

  • server.js คือไฟล์ที่คุณจะใช้ทำงานส่วนใหญ่ในระหว่างใช้ Codelab นี้

    โค้ดเริ่มต้นจะสร้างเว็บเซิร์ฟเวอร์ของ Express ง่ายๆ มีรายการสิ่งที่ต้องทำ 4 รายการสำหรับคุณ ซึ่งทำเครื่องหมายในความคิดเห็นโค้ดด้วย TODO: สิ่งที่ต้องทำ

    ใน Codelab นี้ คุณจะทำรายการสิ่งที่ต้องทำเหล่านี้ทีละรายการ

สร้างและโหลดรายละเอียด VAPID

สิ่งที่ต้องทำรายการแรกคือการสร้างรายละเอียด VAPID แล้วเพิ่มลงในตัวแปรสภาพแวดล้อม Node.js แล้วอัปเดตโค้ดของไคลเอ็นต์และเซิร์ฟเวอร์ด้วยค่าใหม่

ที่มา

เมื่อสมัครรับการแจ้งเตือน ผู้ใช้ต้องเชื่อถือข้อมูลประจำตัวของแอปและเซิร์ฟเวอร์ของแอป นอกจากนี้ ผู้ใช้ต้องมั่นใจว่าเมื่อได้รับการแจ้งเตือนนั้น การแจ้งเตือนนั้นมาจากแอปเดียวกับที่ตั้งค่าการสมัครใช้บริการ นอกจากนี้ยังต้องไว้วางใจว่าไม่มีใครอ่านเนื้อหาการแจ้งเตือนได้

โปรโตคอลที่ทำให้ข้อความ Push มีความปลอดภัยและเป็นส่วนตัวเรียกว่า Voluntary Application Server ID for Web Push (VAPID) VAPID ใช้วิทยาการเข้ารหัสคีย์สาธารณะเพื่อยืนยันตัวตนของแอป เซิร์ฟเวอร์ และปลายทางการสมัครใช้บริการ รวมถึงเข้ารหัสเนื้อหาการแจ้งเตือน

ในแอปนี้ คุณจะใช้แพ็กเกจ Web-push npm เพื่อสร้างคีย์ VAPID รวมถึงเข้ารหัสและส่งการแจ้งเตือน

การใช้งาน

ในขั้นตอนนี้ ให้สร้างคีย์ VAPID 1 คู่สำหรับแอป แล้วเพิ่มลงในตัวแปรสภาพแวดล้อม โหลดตัวแปรสภาพแวดล้อมในเซิร์ฟเวอร์และเพิ่มคีย์สาธารณะเป็นค่าคงที่ในโค้ดไคลเอ็นต์

  1. ใช้ฟังก์ชัน generateVAPIDKeys ของไลบรารี web-push เพื่อสร้างคู่ของคีย์ VAPID

    ใน server.js ให้นำความคิดเห็นออกจากบรรทัดโค้ดต่อไปนี้

    server.js

    // Generate VAPID keys (only do this once).
    /*
     * const vapidKeys = webpush.generateVAPIDKeys();
     * console.log(vapidKeys);
     */
    const vapidKeys = webpush.generateVAPIDKeys();
    console.log(vapidKeys);
    
  2. หลังจากที่ Glitch รีสตาร์ทแอป ระบบจะส่งคีย์ที่สร้างขึ้นในบันทึก Node.js ภายในอินเทอร์เฟซ Glitch (ไม่ใช่คอนโซล Chrome) หากต้องการดูคีย์ VAPID ให้เลือกเครื่องมือ -> บันทึกในอินเทอร์เฟซ Glitch

    ตรวจสอบให้แน่ใจว่าคุณคัดลอกคีย์สาธารณะและคีย์ส่วนตัวจากคู่คีย์เดียวกัน

    Glitch จะรีสตาร์ทแอปทุกครั้งที่คุณแก้ไขโค้ด ดังนั้นคีย์คู่แรกที่คุณสร้างอาจเลื่อนออกจากมุมมองเมื่อมีผลลัพธ์เพิ่มเติมตามมา

  3. คัดลอกและวางคีย์ VAPID ใน .env ใส่คีย์ในเครื่องหมายคำพูดคู่ ("...")

    สำหรับ VAPID_SUBJECT คุณสามารถป้อน "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. ใน server.js ให้แสดงความคิดเห็นของโค้ด 2 บรรทัดนั้นอีกครั้ง เนื่องจากคุณต้องสร้างคีย์ VAPID เพียงครั้งเดียวเท่านั้น

    server.js

    // Generate VAPID keys (only do this once).
    /*
    const vapidKeys = webpush.generateVAPIDKeys();
    console.log(vapidKeys);
    */
    const vapidKeys = webpush.generateVAPIDKeys();
    console.log(vapidKeys);
    
  5. ใน server.js ให้โหลดรายละเอียด VAPID จากตัวแปรสภาพแวดล้อม

    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. คัดลอกและวางคีย์สาธารณะลงในรหัสไคลเอ็นต์ด้วย

    ใน public/index.js ให้ป้อนค่า VAPID_PUBLIC_KEY เดียวกันกับที่คัดลอกลงในไฟล์ .env ดังนี้

    public/index.js

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

ใช้ฟังก์ชันเพื่อส่งการแจ้งเตือน

ที่มา

ในแอปนี้ คุณจะใช้แพ็กเกจ Web-push npm เพื่อส่งการแจ้งเตือน

แพ็กเกจนี้จะเข้ารหัสการแจ้งเตือนโดยอัตโนมัติเมื่อมีการเรียกใช้ webpush.sendNotification() คุณจึงไม่ต้องเป็นห่วง

Web-push ยอมรับตัวเลือกการแจ้งเตือนมากมาย เช่น คุณสามารถแนบส่วนหัวไปกับข้อความและระบุการเข้ารหัสเนื้อหา

ใน Codelab นี้ คุณจะใช้เพียง 2 ตัวเลือกเท่านั้น ที่กำหนดด้วยบรรทัดโค้ดต่อไปนี้

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

ตัวเลือก TTL (Time-to-Live) จะกำหนดระยะหมดเวลาของการแจ้งเตือน ซึ่งเป็นวิธีที่เซิร์ฟเวอร์จะหลีกเลี่ยงการส่งการแจ้งเตือนไปยังผู้ใช้หลังจากไม่เกี่ยวข้องอีกต่อไป

ตัวเลือก vapidDetails มีคีย์ VAPID ที่คุณโหลดจากตัวแปรสภาพแวดล้อม

การใช้งาน

ใน server.js ให้แก้ไขฟังก์ชัน sendNotifications ดังนี้

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() ส่งคืนสัญญา คุณจึงเพิ่มการจัดการข้อผิดพลาดได้อย่างง่ายดาย

ใน server.js ให้แก้ไขฟังก์ชัน 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} `);
    });
  });
}

จัดการการติดตามใหม่

ที่มา

ต่อไปนี้คือสิ่งที่จะเกิดขึ้นเมื่อผู้ใช้สมัครรับข้อความ Push

  1. ผู้ใช้คลิกสมัครรับข้อมูลเพื่อพุช

  2. ไคลเอ็นต์ใช้ค่าคงที่ VAPID_PUBLIC_KEY (คีย์ VAPID สาธารณะของเซิร์ฟเวอร์) เพื่อสร้างออบเจ็กต์ subscription ที่ไม่ซ้ำกันสำหรับเซิร์ฟเวอร์ ออบเจ็กต์ subscription มีลักษณะดังนี้

       {
         "endpoint": "https://fcm.googleapis.com/fcm/send/cpqAgzGzkzQ:APA9...",
         "expirationTime": null,
         "keys":
         {
           "p256dh": "BNYDjQL9d5PSoeBurHy2e4d4GY0sGJXBN...",
           "auth": "0IyyvUGNJ9RxJc83poo3bA"
         }
       }
    
  3. ไคลเอ็นต์จะส่งคำขอ POST ไปยัง URL ของ /add-subscription โดยรวมถึงการสมัครใช้บริการในรูปแบบ JSON แบบสตริงในเนื้อหา

  4. เซิร์ฟเวอร์จะเรียก subscription เป็นสตริงจากเนื้อหาของคำขอ POST แล้วแยกวิเคราะห์กลับไปยัง JSON และเพิ่มไปยังฐานข้อมูลการสมัครใช้บริการ

    ฐานข้อมูลจัดเก็บการสมัครใช้บริการโดยใช้ปลายทางของตนเองเป็นคีย์

    {
      "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: { ... }
      },
    }

ขณะนี้การสมัครใช้บริการใหม่พร้อมให้ใช้งานไปยังเซิร์ฟเวอร์สำหรับส่งการแจ้งเตือนแล้ว

การใช้งาน

คำขอสมัครรับข้อมูลใหม่จะเข้ามาที่เส้นทาง /add-subscription ซึ่งเป็น URL ของ POST คุณจะเห็นตัวแฮนเดิลเส้นทาง Stub ใน 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);
});

ในการใช้งาน เครื่องจัดการนี้ต้อง

  • ดึงข้อมูลการสมัครใช้บริการใหม่จากเนื้อหาของคำขอ
  • เข้าถึงฐานข้อมูลของการสมัครใช้บริการที่ใช้งานอยู่
  • เพิ่มการสมัครใช้บริการใหม่ลงในรายการการสมัครใช้บริการที่ใช้งานอยู่

วิธีจัดการการสมัครใช้บริการใหม่

  • ใน server.js ให้แก้ไขตัวแฮนเดิลเส้นทางสำหรับ /add-subscription ดังนี้

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

จัดการการยกเลิกการสมัครใช้บริการ

ที่มา

เซิร์ฟเวอร์จะไม่ทราบเสมอไปเมื่อการสมัครใช้บริการไม่มีการใช้งาน เช่น อาจมีการล้างข้อมูลการสมัครใช้บริการเมื่อเบราว์เซอร์ปิด Service Worker

แต่เซิร์ฟเวอร์จะค้นหาการสมัครใช้บริการที่ยกเลิกผ่าน UI ของแอปได้ ในขั้นตอนนี้ คุณจะได้ใช้ฟังก์ชันในการนำการสมัครใช้บริการออกจากฐานข้อมูล

วิธีนี้จะทำให้เซิร์ฟเวอร์หลีกเลี่ยงการส่งการแจ้งเตือนจำนวนมากไปยังปลายทางที่ไม่มีอยู่จริง เห็นได้ชัดว่าเรื่องนี้ไม่สำคัญมากนักสำหรับแอปทดสอบทั่วไป แต่กลายเป็นสิ่งสำคัญในระดับที่ใหญ่ขึ้น

การใช้งาน

คำขอยกเลิกการสมัครใช้บริการจะอยู่ใน URL POST ของ /remove-subscription

ตัวแฮนเดิลเส้นทางของ Stub ใน server.js จะมีลักษณะดังนี้

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

ในการใช้งาน เครื่องจัดการนี้ต้อง

  • ดึงข้อมูลปลายทางของการสมัครใช้บริการที่ยกเลิกแล้วจากเนื้อหาของคำขอ
  • เข้าถึงฐานข้อมูลของการสมัครใช้บริการที่ใช้งานอยู่
  • นำการสมัครใช้บริการที่ยกเลิกแล้วออกจากรายการการสมัครใช้บริการที่ใช้งานอยู่

ส่วนเนื้อหาของคำขอ POST จากไคลเอ็นต์จะมีปลายทางที่ต้องนำออก ดังนี้

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

วิธีจัดการการยกเลิกการสมัครใช้บริการ

  • ใน server.js ให้แก้ไขตัวแฮนเดิลเส้นทางสำหรับ /remove-subscription ดังนี้

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