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

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

Douglas Parker
Douglas Parker
Joel Riley
Joel Riley
Dikla Cohen
Dikla Cohen

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

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

ข้อมูลเบื้องต้น

กระดานข่าวสารอยู่ระหว่างการพัฒนาตั้งแต่กลางปี 2017 ถึงกลางปี 2019

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

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

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

เฟรมเวิร์กของเรา

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

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

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

สร้างไฟล์หากทำได้

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

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

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

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

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

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

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

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

สร้างให้ยืดหยุ่น

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

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

อย่าใช้สถานะส่วนกลาง

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

การพัฒนาในพื้นที่

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

ประภาคาร

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

ใช้การส่งมอบอย่างต่อเนื่อง

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

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

การดึงค่าคุกกี้ใน Service Worker

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

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

ข้อควรระวังสำหรับ Service Worker ที่ไม่ได้สร้าง

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

รูปแบบ PWA ที่พบบ่อยคือ Service Worker จะติดตั้งไฟล์แอปพลิเคชันแบบคงที่ทั้งหมดในเฟส install ซึ่งช่วยให้ไคลเอ็นต์เข้าถึงแคชของ Cache Storage API ได้โดยตรงสำหรับการเข้าชมครั้งต่อๆ ไปทั้งหมด ระบบจะติดตั้ง Service Worker เฉพาะเมื่อเบราว์เซอร์ตรวจพบว่าสคริปต์ Service Worker มีการเปลี่ยนแปลงในลักษณะใดลักษณะหนึ่ง เราจึงต้องตรวจสอบว่าไฟล์สคริปต์ Service Worker เองมีการเปลี่ยนแปลงในลักษณะใดลักษณะหนึ่งเมื่อไฟล์ที่แคชไว้มีการเปลี่ยนแปลง ซึ่งเราดำเนินการด้วยตนเองโดยการฝังแฮชของชุดไฟล์ทรัพยากรแบบคงที่ไว้ในสคริปต์ Service Worker ดังนั้นทุกๆ รุ่นจึงสร้างไฟล์ JavaScript ของ Service Worker ที่แตกต่างกัน ไลบรารี 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 เราจึงเก็บสคริปต์ Service Worker หลักไว้ให้เรียบง่ายที่สุดเท่าที่จะเป็นไปได้ โดยแยกการติดตั้งใช้งานส่วนใหญ่ออกเป็นโมดูลอื่นๆ เนื่องจากไฟล์เหล่านั้นเป็นเพียงโมดูล JS มาตรฐาน จึงทำการทดสอบหน่วยได้ง่ายขึ้นด้วยไลบรารีทดสอบมาตรฐาน

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

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