การสร้าง PWA ที่ Google ตอนที่ 1

สิ่งที่ทีม Bulletin ได้เรียนรู้เกี่ยวกับ Service Worker ในขณะที่พัฒนา PWA

ดักลาส พาร์กเกอร์
Douglas Parker
โจเอล ไรลีย์
โจล ไรลีย์
ดิกลา โคเฮน
ดิกลา โคเฮน

นี่เป็นบล็อกโพสต์แรกในชุดบล็อกโพสต์เกี่ยวกับบทเรียนที่ทีม Google Bulletin ได้เรียนรู้ขณะสร้าง PWA สำหรับภายนอก ในโพสต์เหล่านี้ เราจะแชร์ความท้าทายบางส่วนที่เราพบ วิธีที่เราใช้เอาชนะอุปสรรค และคำแนะนำทั่วไปเพื่อหลีกเลี่ยงข้อผิดพลาด ซึ่งไม่ได้หมายถึงภาพรวมทั้งหมดของ PWA โดยมีเป้าหมายเพื่อแชร์สิ่งที่ได้เรียนรู้จากประสบการณ์ของทีม

สำหรับโพสต์แรกนี้ เราจะพูดถึงข้อมูลเบื้องต้นเล็กๆ น้อยๆ ก่อน จากนั้นก็เจาะลึกทุกอย่างที่เราได้เรียนรู้เกี่ยวกับ Service Worker

ที่มา

Bulletin มีการพัฒนาอย่างต่อเนื่องตั้งแต่กลางปี 2017 จนถึงกลางปี 2019

เหตุผลที่เราเลือกสร้าง PWA

ก่อนที่เราจะเข้าสู่กระบวนการพัฒนา เรามาดูกันว่าทำไมการสร้าง PWA จึงเป็นตัวเลือกที่น่าสนใจสำหรับโปรเจ็กต์นี้

  • ความสามารถในการปรับปรุงอย่างรวดเร็ว ซึ่งมีประโยชน์อย่างยิ่งเนื่องจาก Bulletin จะนำร่องในหลายตลาด
  • ฐานโค้ดเดี่ยว ผู้ใช้ของเราใช้ Android และ iOS มีสัดส่วนที่เท่าๆ กัน PWA ทำให้เราสามารถสร้างเว็บแอปเดียวที่ใช้งานได้ในทั้ง 2 แพลตฟอร์ม ทำให้ทีมเติบโตได้เร็วขึ้น
  • อัปเดตได้อย่างรวดเร็วและไม่ขึ้นอยู่กับพฤติกรรมของผู้ใช้ PWA จะอัปเดตได้โดยอัตโนมัติ ซึ่งจะช่วยลดจำนวนไคลเอ็นต์ที่ล้าสมัย เราสามารถเร่งการเปลี่ยนแปลงแบ็กเอนด์ ที่เสียหายได้โดยใช้เวลาย้ายข้อมูลไคลเอ็นต์ที่สั้นมาก
  • ผสานรวมกับแอปของบุคคลที่หนึ่งและแอปของบุคคลที่สามได้ง่ายๆ การผสานรวมดังกล่าวเป็นสิ่งจำเป็นสำหรับแอป การใช้ PWA มักหมายถึงการเปิด URL เพียงอย่างเดียว
  • ขจัดความยุ่งยากในการติดตั้งแอป

กรอบการทำงานของเรา

สำหรับ Bulletin เราใช้ Polymer แต่เฟรมเวิร์กสมัยใหม่ที่ได้รับการสนับสนุนเป็นอย่างดีจะยังคงใช้งานได้

สิ่งที่เราได้เรียนรู้เกี่ยวกับ Service Worker

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

สร้างเลยหากทำได้

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

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

ไลบรารีบางรายการใช้งานร่วมกับ Service Worker ไม่ได้

ไลบรารี JS บางรายการสันนิษฐานว่าไม่ทำงานตามที่คาดไว้เมื่อเรียกใช้โดย Service Worker สำหรับอินสแตนซ์ สมมติว่ามี window หรือ document ว่าง หรือใช้ API ไม่พร้อมใช้สำหรับผู้ให้บริการ (XMLHttpRequest, พื้นที่เก็บข้อมูลในเครื่อง เป็นต้น) ตรวจสอบว่าไลบรารีสำคัญที่คุณจำเป็นต้องใช้กับแอปพลิเคชันนั้นเข้ากันได้กับ Service Worker สำหรับ PWA เฉพาะนี้ เราต้องการใช้ gapi.js สำหรับการตรวจสอบสิทธิ์ แต่ไม่สามารถทำได้เนื่องจากไม่รองรับ Service Worker ผู้เขียนไลบรารีควรลดหรือนำสมมติฐานที่ไม่จำเป็นเกี่ยวกับบริบทของ JavaScript ออกเมื่อเป็นไปได้ เพื่อสนับสนุนกรณีการใช้งานของ Service Worker เช่น โดยการหลีกเลี่ยง API ที่เข้ากันไม่ได้กับโปรแกรมทำงาน และการหลีกเลี่ยงสถานะทั่วโลก

หลีกเลี่ยงการเข้าถึง IndexedDB ระหว่างการเริ่มต้น

อย่าอ่าน IndexedDB ขณะเริ่มต้นสคริปต์ Service Worker มิฉะนั้นคุณอาจตกอยู่ในสถานการณ์ที่ไม่พึงประสงค์นี้

  1. ผู้ใช้มีเว็บแอปที่มี IndexedDB (IDB) เวอร์ชัน N
  2. เว็บแอปใหม่มีการพุชพร้อม IDB เวอร์ชัน N+1
  3. ผู้ใช้ไปที่ PWA ซึ่งจะทริกเกอร์การดาวน์โหลด Service Worker ใหม่
  4. โปรแกรมทำงานของบริการใหม่จะอ่านจาก IDB ก่อนลงทะเบียนตัวแฮนเดิลเหตุการณ์ install ซึ่งจะทริกเกอร์รอบการอัปเกรด IDB เพื่อเปลี่ยนจาก N เป็น N+1
  5. เนื่องจากผู้ใช้มีไคลเอ็นต์เก่าที่มีเวอร์ชัน N กระบวนการอัปเกรดโปรแกรมทำงานของบริการจึงค้าง เนื่องจากการเชื่อมต่อที่ใช้งานอยู่ยังคงเปิดอยู่สำหรับฐานข้อมูลเวอร์ชันเก่า
  6. Service Worker ค้าง และไม่ติดตั้ง

ในกรณีของเรา แคชใช้งานไม่ได้ในการติดตั้ง Service Worker ดังนั้นถ้าโปรแกรมทำงานของบริการไม่เคยติดตั้ง ผู้ใช้ก็จะไม่ได้รับแอปที่อัปเดตเลย

ทำให้ปรับตัวได้

แม้ว่าสคริปต์ของ Service Worker จะทำงานอยู่เบื้องหลัง แต่ก็มีการหยุดทำงานได้ทุกเมื่อแม้ในระหว่างการดำเนินการ I/O (เครือข่าย, IDB ฯลฯ) กระบวนการที่ใช้เวลานานควรสามารถทำงานต่อได้ทุกเมื่อ

ในกรณีของกระบวนการซิงค์ที่อัปโหลดไฟล์ขนาดใหญ่ไปยังเซิร์ฟเวอร์และบันทึกไว้ใน IDB วิธีแก้ปัญหาของเราสำหรับการอัปโหลดบางส่วนที่ถูกขัดจังหวะคือการใช้ประโยชน์จากระบบที่สามารถดำเนินการต่อได้ของไลบรารีการอัปโหลดภายในของเรา บันทึก URL การอัปโหลดที่ดำเนินการต่อไปยัง IDB ได้ก่อนอัปโหลด และใช้ URL นั้นเพื่ออัปโหลดต่อหากอัปโหลดไม่สำเร็จในครั้งแรก นอกจากนี้ ก่อนการดำเนินการ I/O ที่ยาวนานใดๆ ระบบได้บันทึกสถานะไว้ใน IDB เพื่อระบุตำแหน่งในกระบวนการสำหรับระเบียนแต่ละรายการ

ไม่ต้องขึ้นอยู่กับรัฐทั่วโลก

เนื่องจากโปรแกรมทำงานของบริการอยู่ในบริบทที่แตกต่างกัน สัญลักษณ์จำนวนมากที่คุณอาจคาดไว้จึงไม่มีอยู่ โค้ดจำนวนมากทำงานทั้งในบริบท window และบริบท Service Worker (เช่น การบันทึก แฟล็ก การซิงค์ ฯลฯ) โค้ดต้องมีการปกป้องบริการต่างๆ ที่โค้ดใช้ เช่น พื้นที่เก็บข้อมูลในเครื่องหรือคุกกี้ คุณใช้ globalThis เพื่ออ้างอิงออบเจ็กต์ส่วนกลางในรูปแบบที่ใช้งานได้กับทุกบริบท นอกจากนี้ ให้ใช้ข้อมูลที่จัดเก็บไว้ในตัวแปรร่วมเท่าที่จําเป็น เนื่องจากไม่มีการรับประกันว่าสคริปต์จะสิ้นสุดลงเมื่อใดและจะปลดสถานะออกเมื่อใด

การพัฒนาในท้องถิ่น

องค์ประกอบหลักของ Service Worker คือการแคชทรัพยากรในเครื่อง อย่างไรก็ตาม ในระหว่างการพัฒนา สิ่งนี้จะตรงกันข้ามกับสิ่งที่คุณต้องการ โดยเฉพาะเมื่อการอัปเดตเสร็จสิ้นอย่างช้าๆ แต่คุณยังต้องติดตั้งโปรแกรมทำงานของเซิร์ฟเวอร์ไว้เพื่อให้แก้ไขข้อบกพร่องหรือทำงานร่วมกับ API อื่นๆ เช่น การซิงค์ในเบื้องหลังหรือการแจ้งเตือน ใน Chrome คุณสามารถดำเนินการได้ผ่านเครื่องมือสำหรับนักพัฒนาเว็บใน Chrome โดยเปิดใช้ช่องทำเครื่องหมายการข้ามสำหรับเครือข่าย (แผงแอปพลิเคชัน > แผงโปรแกรมทำงานของบริการ) เพื่อเปิดใช้งานช่องทำเครื่องหมายปิดใช้แคชในแผงเครือข่ายเพื่อปิดแคชหน่วยความจำด้วย เพื่อให้ครอบคลุมเบราว์เซอร์อื่นๆ เราเลือกใช้โซลูชันอื่นโดยใส่การแจ้งว่าไม่เหมาะสมให้ปิดใช้การแคชใน Service Worker ของเราซึ่งเปิดใช้โดยค่าเริ่มต้นในบิลด์ของนักพัฒนาซอฟต์แวร์ วิธีนี้ช่วยให้นักพัฒนาซอฟต์แวร์ได้รับการเปลี่ยนแปลงล่าสุดอยู่เสมอโดยไม่มีปัญหาการแคช สิ่งสำคัญคือต้องใส่ส่วนหัว Cache-Control: no-cache ด้วยเพื่อป้องกันไม่ให้เบราว์เซอร์แคชเนื้อหา

ประภาคาร

Lighthouseมีเครื่องมือแก้ไขข้อบกพร่องมากมายซึ่งเป็นประโยชน์สำหรับ PWA ซึ่งจะสแกนเว็บไซต์และสร้างรายงานเกี่ยวกับ PWA, ประสิทธิภาพ, การช่วยเหลือพิเศษ, SEO และแนวทางปฏิบัติแนะนำอื่นๆ เราขอแนะนำให้เรียกใช้ Lighthouse ในการผสานรวมอย่างต่อเนื่องเพื่อแจ้งเตือนคุณหากไม่เป็นไปตามเกณฑ์ใดเกณฑ์หนึ่งของ PWA เรื่องนี้เกิดขึ้นจริงครั้งหนึ่ง โดยที่โปรแกรมทำงานของบริการไม่ได้ติดตั้ง และเราไม่รู้ตัวก่อนที่จะมีการพุชเวอร์ชันที่ใช้งานจริง การมี Lighthouse เป็นส่วนหนึ่งของ CI สามารถป้องกันปัญหานี้ได้

รองรับการแสดงโฆษณาอย่างต่อเนื่อง

เนื่องจากโปรแกรมทำงานของบริการสามารถอัปเดตได้โดยอัตโนมัติ ผู้ใช้จึงจำกัดการอัปเกรดไม่ได้ การดำเนินการนี้ช่วยลดจำนวนไคลเอ็นต์ที่ล้าสมัยได้อย่างมาก เมื่อผู้ใช้เปิดแอปของเรา โปรแกรมทำงานของบริการจะแสดงไคลเอ็นต์เดิมในขณะที่ดาวน์โหลดไคลเอ็นต์ใหม่อย่างช้าๆ เมื่อดาวน์โหลดไคลเอ็นต์ใหม่แล้ว ระบบจะแจ้งให้ผู้ใช้รีเฟรชหน้าเว็บเพื่อเข้าถึงฟีเจอร์ใหม่ๆ แม้ว่าผู้ใช้จะละเว้นคำขอนี้ในครั้งถัดไปที่รีเฟรชหน้าเว็บ ผู้ใช้จะได้รับไคลเอ็นต์เวอร์ชันใหม่ ด้วยเหตุนี้ ผู้ใช้ปฏิเสธการอัปเดตในลักษณะเดียวกับแอป iOS/Android จึงเป็นเรื่องยาก

เราสามารถเร่งการเปลี่ยนแปลงแบ็กเอนด์ที่ส่งผลกับส่วนอื่นในระบบได้ด้วยเวลาย้ายข้อมูลที่สั้นมากสำหรับลูกค้า โดยปกติแล้ว เราจะให้เวลาผู้ใช้ 1 เดือนในการอัปเดตไปยังลูกค้าใหม่ก่อนที่จะทำการเปลี่ยนแปลงใดๆ ที่ผิดพลาด เนื่องจากแอปจะให้บริการแม้ไม่มีอัปเดต อาจเป็นไปได้ที่ไคลเอ็นต์รุ่นเก่า จะอยู่ในป่าได้หากผู้ใช้ไม่ได้เปิดแอปมาเป็นเวลานาน ใน iOS โปรแกรมทำงานของบริการจะถูกนำออกหลังจากผ่านไป 2-3 สัปดาห์ ดังนั้นกรณีนี้จะไม่เกิดขึ้น สำหรับ Android ปัญหานี้อาจบรรเทาลงได้ด้วยการไม่แสดงในระหว่างที่เนื้อหาไม่มีอัปเดตหรือหมดอายุด้วยตัวเองหลังจากผ่านไป 2-3 สัปดาห์ ในทางปฏิบัติ เราไม่เคยเจอ ปัญหาจากลูกค้าที่ไม่มีอัปเดตเลย ความเข้มงวดที่ทีมที่กำหนดต้องการเข้าร่วมนั้นขึ้นอยู่กับกรณีการใช้งานเฉพาะ แต่ PWA นั้นมีความยืดหยุ่นมากกว่าแอป iOS/Android อย่างมาก

การรับค่าคุกกี้ในโปรแกรมทำงานของบริการ

บางครั้งอาจจำเป็นต้องเข้าถึงค่าคุกกี้ในบริบทของโปรแกรมทำงานบริการ ในกรณีของเรา เราจำเป็นต้องเข้าถึงค่าคุกกี้เพื่อสร้างโทเค็นเพื่อตรวจสอบสิทธิ์คำขอ API ของบุคคลที่หนึ่ง สำหรับโปรแกรมทำงานของบริการ API แบบซิงโครนัส เช่น document.cookies จะใช้งานไม่ได้ คุณส่งข้อความถึงไคลเอ็นต์ที่ใช้งานอยู่ (อยู่ในกรอบเวลา) จาก Service Worker เพื่อขอค่าคุกกี้ได้เสมอ แต่โปรแกรมดังกล่าวอาจทำงานในเบื้องหลังได้หากไม่มีไคลเอ็นต์ที่อยู่ในโหมดหน้าต่าง เช่น ในระหว่างการซิงค์ในเบื้องหลัง ในการแก้ปัญหานี้ เราจึงสร้างปลายทางบนเซิร์ฟเวอร์ฟรอนท์เอนด์ที่เพียงแค่สะท้อนค่าคุกกี้กลับไปยังไคลเอ็นต์ โปรแกรมทำงานของบริการได้ส่งคำขอเครือข่ายไปยังปลายทางนี้และอ่านการตอบกลับเพื่อรับค่าคุกกี้

เมื่อเปิดตัว Cookie Store API แล้ว วิธีแก้ไขเบื้องต้นนี้อาจไม่จำเป็นสำหรับเบราว์เซอร์ที่รองรับอีกต่อไป เนื่องจากมีการเข้าถึงคุกกี้ของเบราว์เซอร์แบบอะซิงโครนัสและ Service Worker ใช้งานได้โดยตรง

ข้อผิดพลาดสำหรับ Service Worker ที่ไม่ได้สร้าง

ตรวจสอบว่าสคริปต์ Service Worker เปลี่ยนแปลงหากไฟล์แคชแบบคงที่มีการเปลี่ยนแปลง

รูปแบบ PWA ทั่วไปมีไว้สำหรับ Service Worker ในการติดตั้งไฟล์แอปพลิเคชันแบบคงที่ทั้งหมดในระยะ install ซึ่งทำให้ไคลเอ็นต์กดแคช Cache Storage API ได้โดยตรงสำหรับการเข้าชมครั้งต่อๆ ไปทั้งหมด ระบบจะติดตั้งโปรแกรมทำงานของบริการก็ต่อเมื่อเบราว์เซอร์ตรวจพบว่าสคริปต์โปรแกรมทำงานของบริการมีการเปลี่ยนแปลงอย่างใดอย่างหนึ่ง เราจึงต้องตรวจสอบว่าไฟล์สคริปต์โปรแกรมทำงานของบริการมีการเปลี่ยนแปลงในทางใดทางหนึ่งเมื่อไฟล์แคชมีการเปลี่ยนแปลง ซึ่งเราได้ดำเนินการด้วยตนเองโดยการฝังแฮชของชุดไฟล์ทรัพยากรแบบคงที่ไว้ในสคริปต์โปรแกรมทำงานบริการของเรา เพื่อให้ทุกรุ่นสร้างไฟล์ JavaScript สำหรับโปรแกรมทำงานของบริการที่แตกต่างกัน ไลบรารีของ Service Worker เช่น Workbox จะดำเนินการนี้ให้คุณโดยอัตโนมัติ

การทดสอบ 1 หน่วย

Service Work API ทำงานด้วยการเพิ่ม Listener เหตุการณ์ลงในออบเจ็กต์ส่วนกลาง เช่น

self.addEventListener('fetch', (evt) => evt.respondWith(fetch('/foo')));

การทดสอบอาจเป็นเรื่องยุ่งยากเนื่องจากคุณต้องจําลองทริกเกอร์เหตุการณ์ ออบเจ็กต์เหตุการณ์ รอโค้ดเรียกกลับ respondWith() จากนั้นจึงรอคําสัญญา ก่อนที่จะยืนยันผลลัพธ์ในท้ายที่สุด วิธีจัดโครงสร้างที่ง่ายกว่าคือมอบสิทธิ์การติดตั้งใช้งานทั้งหมดให้กับไฟล์อื่น ซึ่งจะทดสอบได้ง่ายกว่า

import fetchHandler from './fetch_handler.js';
self.addEventListener('fetch', (evt) => evt.respondWith(fetchHandler(evt)));

เนื่องจากความยุ่งยากในการทดสอบสคริปต์ของ Service Worker ทำให้สคริปต์ของ Worker Service นั้นเรียบง่ายที่สุดเท่าที่จะทำได้ โดยแยกการติดตั้งใช้งานส่วนใหญ่ออกเป็นโมดูลอื่นๆ เนื่องจากไฟล์เหล่านี้เป็นเพียงโมดูล JS มาตรฐาน จึงสามารถทดสอบหน่วยด้วยไลบรารีทดสอบมาตรฐานได้ง่ายขึ้น

รอติดตามส่วนที่ 2 และ 3 นะ

ในส่วนที่ 2 และ 3 ของซีรีส์นี้ เราจะพูดถึงการจัดการสื่อและปัญหาเฉพาะของ iOS หากต้องการสอบถามข้อมูลเพิ่มเติมเกี่ยวกับการสร้าง PWA ที่ Google โปรดไปที่โปรไฟล์ผู้เขียนเพื่อดูวิธีติดต่อเรา