การสร้าง 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 แต่จะใช้เฟรมเวิร์กสมัยใหม่ที่ได้รับการรองรับอย่างดีก็ได้

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

คุณจะมี PWA ไม่ได้หากไม่มี service worker 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 ที่โปรแกรมทำงานไม่ได้ใช้ร่วมกับโปรแกรมทำงานไม่ได้และหลีกเลี่ยงสถานะส่วนกลาง

หลีกเลี่ยงการเข้าถึง 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 จะทำงานในเบื้องหลัง แต่ก็สิ้นสุดได้ทุกเมื่อ แม้ในขณะที่ดำเนินการ 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 แล้ว เบราว์เซอร์ที่รองรับ API นี้จะไม่จําเป็นต้องใช้วิธีแก้ปัญหานี้อีกต่อไป เนื่องจาก API ดังกล่าวให้การเข้าถึงคุกกี้เบราว์เซอร์แบบไม่สอดคล้องกันและ Service Worker สามารถใช้ API ดังกล่าวได้โดยตรง

ข้อควรระวังสำหรับ 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)));

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

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

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