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

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

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

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

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

ที่มา

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

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

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

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

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

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

สิ่งที่เราได้เรียนรู้เกี่ยวกับโปรแกรมทำงานของบริการ

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

สร้างถ้าทำได้

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

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

ห้องสมุดบางแห่งอาจใช้งานร่วมกับผู้ปฏิบัติงานบริการไม่ได้

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

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

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

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

ใช้การแสดงโฆษณาอย่างต่อเนื่อง

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

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

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

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

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