สร้าง PWA ด้วย Webpack และ Workbox
เผยแพร่แล้ว: 2022-03-10Progressive Web App (PWA) คือไซต์ที่ใช้เทคโนโลยีสมัยใหม่เพื่อมอบประสบการณ์ที่เหมือนแอปบนเว็บ เป็นคำศัพท์เฉพาะสำหรับเทคโนโลยีใหม่ๆ เช่น 'รายการแอปบนเว็บ', 'พนักงานบริการ' และอื่นๆ เมื่อรวมเข้าด้วยกัน เทคโนโลยีเหล่านี้จะช่วยให้คุณมอบประสบการณ์ผู้ใช้ที่รวดเร็วและมีส่วนร่วมกับเว็บไซต์ของคุณ
บทความนี้เป็นบทแนะนำทีละขั้นตอนสำหรับการเพิ่มพนักงานบริการไปยังเว็บไซต์หน้าเดียวที่มีอยู่ เจ้าหน้าที่บริการจะอนุญาตให้คุณทำให้เว็บไซต์ของคุณทำงานแบบออฟไลน์ได้ในขณะที่ยังแจ้งให้ผู้ใช้ทราบเกี่ยวกับการอัปเดตในเว็บไซต์ของคุณ โปรดทราบว่าสิ่งนี้อิงจากโปรเจ็กต์ขนาดเล็กที่มาพร้อมกับ Webpack ดังนั้นเราจะใช้ปลั๊กอิน Workbox Webpack (Workbox v4)
การใช้เครื่องมือเพื่อสร้างพนักงานบริการของคุณเป็นวิธีที่แนะนำ เนื่องจากจะช่วยให้คุณจัดการแคชได้อย่างมีประสิทธิภาพ เราจะใช้ Workbox ซึ่งเป็นชุดของไลบรารีที่ทำให้ง่ายต่อการสร้างรหัสพนักงานบริการของคุณ เพื่อสร้างพนักงานบริการของเราในบทช่วยสอนนี้
คุณสามารถใช้ Workbox ได้สามวิธีขึ้นอยู่กับโปรเจ็กต์ของคุณ:
- มี อินเทอร์เฟซบรรทัดคำสั่ง ซึ่งช่วยให้คุณรวมเวิร์กบ็อกซ์เข้ากับแอปพลิเคชันใดๆ ที่คุณมี
- มี โมดูล Node.js ซึ่งช่วยให้คุณรวมเวิร์กบ็อกซ์เข้ากับเครื่องมือสร้างโหนดใด ๆ เช่นอึกหรือเสียงฮึดฮัด
- มี ปลั๊กอิน webpack ซึ่งช่วยให้คุณรวมเข้ากับโปรเจ็กต์ที่สร้างด้วย Webpack ได้อย่างง่ายดาย
Webpack เป็นตัวรวมโมดูล เพื่อลดความซับซ้อน คุณสามารถคิดว่ามันเป็นเครื่องมือที่จัดการการพึ่งพา JavaScript ของคุณ ช่วยให้คุณสามารถนำเข้าโค้ด JavaScript จากไลบรารีและรวม JavaScript ของคุณเข้าด้วยกันเป็นไฟล์เดียวหรือหลายไฟล์
ในการเริ่มต้น ให้โคลนที่เก็บต่อไปนี้บนคอมพิวเตอร์ของคุณ:
git clone [email protected]:jadjoubran/workbox-tutorial-v4.git cd workbox-tutorial-v4 npm install npm run dev
จากนั้นไปที่ https://localhost:8080/
คุณควรจะเห็นแอปสกุลเงินที่เราจะใช้ตลอดบทช่วยสอนนี้:

เริ่มต้นด้วย App Shell
เปลือกแอปพลิเคชัน (หรือ 'เปลือกแอป') เป็นรูปแบบที่ได้รับแรงบันดาลใจจากแอปที่มาพร้อมเครื่อง จะช่วยให้แอปของคุณมีรูปลักษณ์ที่เป็นธรรมชาติมากขึ้น เพียงแค่ให้แอปมีเลย์เอาต์และโครงสร้างโดยไม่มีข้อมูลใด ๆ ซึ่งเป็นหน้าจอเปลี่ยนผ่านที่มุ่งปรับปรุงประสบการณ์การโหลดเว็บแอปของคุณ
ต่อไปนี้คือตัวอย่างบางส่วนของเชลล์แอปจากแอปที่มาพร้อมเครื่อง:


และนี่คือตัวอย่างเชลล์แอปจาก PWAs:


ผู้ใช้ชอบประสบการณ์การโหลด App Shell เพราะไม่ชอบหน้าจอเปล่า หน้าจอว่างทำให้ผู้ใช้รู้สึกว่าเว็บไซต์ไม่โหลด มันทำให้พวกเขารู้สึกว่าเว็บไซต์ติดขัด
App Shell จะพยายามลงสีโครงสร้างการนำทางของแอปโดยเร็วที่สุด เช่น แถบนำทาง แถบแท็บ และตัวโหลดที่แสดงว่าเนื้อหาที่คุณร้องขอกำลังถูกโหลด
คุณจะสร้าง App Shell ได้อย่างไร?
รูปแบบเชลล์ของแอปจัดลำดับความสำคัญการโหลด HTML, CSS และ JavaScript ที่จะแสดงผลก่อน ซึ่งหมายความว่าเราจำเป็นต้องให้ความสำคัญกับทรัพยากรเหล่านั้นอย่างเต็มที่ ดังนั้นคุณต้องอินไลน์เนื้อหาเหล่านั้น ดังนั้น ในการสร้าง App Shell คุณเพียงแค่ต้องอินไลน์ HTML, CSS และ JavaScript ที่รับผิดชอบ App Shell แน่นอน คุณไม่ควรอินไลน์ทุกอย่างแต่ควรอยู่ภายในขีดจำกัดทั้งหมดประมาณ 30 ถึง 40KB
คุณสามารถดู App Shell แบบอินไลน์ได้ใน index.html คุณสามารถตรวจสอบซอร์สโค้ดได้โดยดูที่ไฟล์ index.html และดูตัวอย่างได้ในเบราว์เซอร์โดยลบองค์ประกอบ <main>
ในเครื่องมือ dev:

มันทำงานแบบออฟไลน์หรือไม่?
มาจำลองออฟไลน์กันเถอะ! เปิด DevTools ไปที่แท็บเครือข่ายและทำเครื่องหมายที่ช่อง 'ออฟไลน์' เมื่อคุณโหลดหน้าซ้ำ คุณจะเห็นว่าเราจะได้รับหน้าออฟไลน์ของเบราว์เซอร์

นั่นเป็นเพราะคำขอเริ่มต้นไปยัง /
(ซึ่งจะโหลดไฟล์ index.html ) จะล้มเหลวเนื่องจากอินเทอร์เน็ตออฟไลน์ วิธีเดียวที่เราจะกู้คืนจากความล้มเหลวของคำขอนั้นคือการมีพนักงานบริการ
ลองนึกภาพคำขอโดยไม่มีพนักงานบริการ:

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

สิ่งนี้มีประโยชน์เพราะตอนนี้เราสามารถกำหนดเส้นทางคำขอที่ล้มเหลวนี้ใหม่ไปยังแคชได้ (สมมติว่าเรามีเนื้อหาในแคช)

พนักงานบริการยังเป็น Web Worker ประเภทหนึ่ง ซึ่งหมายความว่าทำงานแยกจากหน้าหลักของคุณ และไม่สามารถเข้าถึง window
หรือวัตถุ document
ได้
Precache The App Shell
เพื่อให้แอปของเราทำงานแบบออฟไลน์ เราจะเริ่มต้นด้วยการแคชล่วงหน้าของแอปเชลล์
เริ่มต้นด้วยการติดตั้งปลั๊กอิน Webpack Workbox:
npm install --save-dev workbox-webpack-plugin
จากนั้นเราจะเปิดไฟล์ index.js และลงทะเบียนพนักงานบริการ:
if ("serviceWorker" in navigator){ window.addEventListener("load", () => { navigator.serviceWorker.register("/sw.js"); }) }
ถัดไป เปิดไฟล์ webpack.config.js และกำหนดค่าปลั๊กอิน Webpack ของ Workbox:
//add at the top const WorkboxWebpackPlugin = require("workbox-webpack-plugin"); //add inside the plugins array: plugins: [ … , new WorkboxWebpackPlugin.InjectManifest({ swSrc: "./src/src-sw.js", swDest: "sw.js" }) ]
ซึ่งจะสั่งให้ Workbox ใช้ไฟล์ ./src/src-sw.js ของเราเป็นฐาน ไฟล์ที่สร้างขึ้นจะเรียกว่า sw.js และจะอยู่ในโฟลเดอร์ dist

จากนั้นสร้างไฟล์ ./src/src-sw.js ที่ระดับรูทและเขียนสิ่งต่อไปนี้ไว้ข้างใน:
workbox.precaching.precacheAndRoute(self.__precacheManifest);
หมายเหตุ : ตัวแปร self.__precacheManifest
จะถูกนำเข้าจากไฟล์ที่จะสร้างแบบไดนามิกโดยเวิร์กบ็อกซ์
ตอนนี้ คุณพร้อมที่จะสร้างโค้ดของคุณด้วย npm run build
และ Workbox จะสร้างไฟล์สองไฟล์ภายในโฟลเดอร์ dist
:
- precache-manifest.66cf63077c7e4a70ba741ee9e6a8da29.js
- sw.js
sw.js นำเข้าเวิร์กบ็อกซ์จาก CDN เช่นเดียวกับ precache-manifest.[chunkhash].js
//precache-manifest.[chunkhash].js file self.__precacheManifest = (self.__precacheManifest || []).concat([ "revision": "ba8f7488757693a5a5b1e712ac29cc28", "url": "index.html" }, "url": "main.49467c51ac5e0cb2b58e.js" ]);
รายการ precache แสดงรายการชื่อของไฟล์ที่ประมวลผลโดย webpack และจบลงในโฟลเดอร์ dist
ของคุณ เราจะใช้ไฟล์เหล่านี้เพื่อเตรียมแคชล่วงหน้าในเบราว์เซอร์ ซึ่งหมายความว่าเมื่อเว็บไซต์ของคุณโหลดในครั้งแรกและลงทะเบียนพนักงานบริการ เว็บไซต์จะแคชสินทรัพย์เหล่านี้เพื่อให้สามารถใช้งานได้ในครั้งต่อไป
คุณยังสังเกตได้ว่าบางรายการมี 'การแก้ไข' ในขณะที่บางรายการไม่มี นั่นเป็นเพราะว่าบางครั้งการแก้ไขสามารถอนุมานได้จาก chunkhash จากชื่อไฟล์ ตัวอย่างเช่น มาดูชื่อไฟล์กันดีกว่า main.49467c51ac5e0cb2b58e.js มีการแก้ไขอยู่ในชื่อไฟล์ซึ่งเป็น chunkhash 49467c51ac5e0cb2b58e
ซึ่งช่วยให้ Workbox เข้าใจว่าไฟล์ของคุณเปลี่ยนแปลงเมื่อใด เพื่อล้างหรืออัปเดตไฟล์ที่เปลี่ยนแปลงเท่านั้น แทนที่จะทิ้งแคชทั้งหมดทุกครั้งที่คุณเผยแพร่เวอร์ชันใหม่ของพนักงานบริการ
ครั้งแรกที่คุณโหลดหน้า เจ้าหน้าที่บริการจะทำการติดตั้ง คุณสามารถเห็นได้ใน DevTools ขั้นแรก ร้องขอไฟล์ sw.js ซึ่งจะร้องขอไฟล์อื่นๆ ทั้งหมด มีการทำเครื่องหมายด้วยไอคอนรูปเฟืองอย่างชัดเจน

ดังนั้น Workbox จะเริ่มต้นและจะทำการ precache ไฟล์ทั้งหมดที่อยู่ใน precache-manifest สิ่งสำคัญคือต้องตรวจสอบอีกครั้งว่าคุณไม่มีไฟล์ที่ไม่จำเป็นในไฟล์ precache -manifest เช่น ไฟล์ .map หรือไฟล์ที่ไม่ได้เป็นส่วนหนึ่งของเชลล์แอป
ในแท็บเครือข่าย เราจะเห็นคำขอที่มาจากพนักงานบริการ และตอนนี้หากคุณพยายามออฟไลน์ เปลือกของแอปนั้นได้รับแคชล่วงหน้าแล้ว ดังนั้นมันจึงทำงานแม้ว่าเราจะออฟไลน์อยู่!

แคชเส้นทางไดนามิก
คุณสังเกตไหมว่าเมื่อเราออฟไลน์ เปลือกของแอปทำงานได้ แต่ไม่ใช่ข้อมูลของเรา นั่นเป็นเพราะการเรียก API เหล่านี้ไม่ได้เป็นส่วนหนึ่งของ precached app shell เมื่อไม่มีการเชื่อมต่ออินเทอร์เน็ต คำขอเหล่านี้จะล้มเหลวและผู้ใช้จะไม่เห็นข้อมูลสกุลเงิน
อย่างไรก็ตาม คำขอเหล่านี้ไม่สามารถพรีแคชล่วงหน้าได้ เนื่องจากค่าของคำขอเหล่านี้มาจาก API นอกจากนี้ เมื่อคุณเริ่มมีหลายหน้า คุณไม่ต้องการแคชคำขอ API ทั้งหมดในครั้งเดียว คุณต้องการแคชไว้เมื่อผู้ใช้เข้าชมหน้านั้นแทน
เราเรียกสิ่งเหล่านี้ว่า 'ข้อมูลแบบไดนามิก' มักจะรวมถึงการเรียก API เช่นเดียวกับรูปภาพและทรัพย์สินอื่นๆ ที่ร้องขอเมื่อผู้ใช้ดำเนินการบางอย่างในเว็บไซต์ของคุณ (เช่น เมื่อพวกเขาเรียกดูหน้าใหม่)
คุณสามารถแคชสิ่งเหล่านี้ได้โดยใช้โมดูลการกำหนดเส้นทางของ Workbox โดยใช้วิธีดังนี้:
//add in src/src-sw.js workbox.routing.registerRoute( /https:\/\/api\.exchangeratesapi\.io\/latest/, new workbox.strategies.NetworkFirst({ cacheName: "currencies", plugins: [ new workbox.expiration.Plugin({ maxAgeSeconds: 10 * 60 // 10 minutes }) ] }) );
การดำเนินการนี้จะตั้งค่าการแคชแบบไดนามิกสำหรับ URL คำขอใดๆ ที่ตรงกับ URL https://api.exchangeratesapi.io/latest
กลยุทธ์แคชที่เราใช้ในที่นี้เรียกว่า NetworkFirst
; มีอีกสองตัวที่มักใช้:
-
CacheFirst
-
StaleWhileRevalidate
CacheFirst
จะค้นหาในแคชก่อน หากไม่พบก็จะได้รับจากเครือข่าย StaleWhileRevalidate
จะไปที่เครือข่ายและแคชพร้อมกัน ส่งคืนการตอบกลับของแคชไปยังหน้า (ในขณะที่อยู่ในพื้นหลัง) แคชจะใช้การตอบสนองของเครือข่ายใหม่เพื่ออัปเดตแคชในครั้งต่อไปที่มีการใช้งาน
สำหรับกรณีการใช้งาน เราต้องเลือกใช้ NetworkFirst
เพราะเรากำลังรับมือกับอัตราแลกเปลี่ยนที่เปลี่ยนแปลงบ่อยมาก อย่างไรก็ตาม เมื่อผู้ใช้ออฟไลน์ อย่างน้อย เราสามารถแสดงอัตราให้พวกเขาเหมือนเมื่อ 10 นาทีที่แล้ว นั่นคือเหตุผลที่เราใช้ปลั๊กอินการหมดอายุโดยตั้งค่า maxAgeSeconds
เป็น 10 * 60
วินาที
จัดการการอัปเดตแอป
ทุกครั้งที่ผู้ใช้โหลดหน้าของคุณ เบราว์เซอร์จะเรียกใช้รหัส navigator.serviceWorker.register
แม้ว่าพนักงานบริการจะได้รับการติดตั้งและใช้งานอยู่แล้ว ซึ่งช่วยให้เบราว์เซอร์ตรวจพบว่ามีพนักงานบริการเวอร์ชันใหม่หรือไม่ เมื่อเบราว์เซอร์สังเกตเห็นว่าไฟล์นั้นไม่มีการเปลี่ยนแปลง มันก็แค่ข้ามการโทรลงทะเบียน เมื่อไฟล์นั้นเปลี่ยนแปลงไป เบราว์เซอร์จะเข้าใจว่ามีพนักงานบริการรุ่นใหม่ ดังนั้นจึง ติดตั้งผู้ปฏิบัติงานบริการใหม่ควบคู่ไปกับพนักงานบริการที่กำลังทำงาน อยู่
อย่างไรก็ตาม จะหยุดชั่วคราวที่ขั้นตอนการ installed/waiting
เนื่องจากสามารถเปิดใช้งานพนักงานบริการได้เพียงคนเดียวในเวลาเดียวกัน

เฉพาะเมื่อปิดหน้าต่างเบราว์เซอร์ทั้งหมดที่ควบคุมโดยพนักงานบริการคนก่อน จึงจะปลอดภัยสำหรับการเปิดใช้งานพนักงานบริการใหม่
คุณยังสามารถควบคุมสิ่งนั้นได้ด้วยตนเองโดยการเรียก skipWaiting()
(หรือ self.skipWaiting()
เนื่องจาก self
เป็นบริบทการดำเนินการทั่วโลกในพนักงานบริการ) อย่างไรก็ตาม ส่วนใหญ่คุณควรทำอย่างนั้นหลังจากถามผู้ใช้ว่าต้องการรับการอัปเดตล่าสุดหรือไม่
โชคดีที่ workbox-window
ช่วยให้เราบรรลุเป้าหมายนี้ เป็นไลบรารีหน้าต่างใหม่ที่เปิดตัวใน Workbox v4 ที่มีจุดมุ่งหมายเพื่อลดความซับซ้อนของงานทั่วไปที่ด้านข้างของหน้าต่าง
เริ่มต้นด้วยการติดตั้งดังต่อไปนี้:
npm install workbox-window
ถัดไป นำเข้า Workbox
ที่ด้านบนของไฟล์ index.js :
import { Workbox } from "workbox-window";
จากนั้นเราจะแทนที่รหัสการลงทะเบียนของเราด้วยด้านล่าง:
if ("serviceWorker" in navigator) { window.addEventListener("load", () => { const wb = new Workbox("/sw.js"); wb.register(); }); }
จากนั้นเราจะพบปุ่มอัปเดตซึ่งมีรหัสworkbox-waiting
งาน:
//add before the wb.register() const updateButton = document.querySelector("#app-update"); // Fires when the registered service worker has installed but is waiting to activate. wb.addEventListener("waiting", event => { updateButton.classList.add("show"); updateButton.addEventListener("click", () => { // Set up a listener that will reload the page as soon as the previously waiting service worker has taken control. wb.addEventListener("controlling", event => { window.location.reload(); }); // Send a message telling the service worker to skip waiting. // This will trigger the `controlling` event handler above. wb.messageSW({ type: "SKIP_WAITING" }); }); });
รหัสนี้จะแสดงปุ่มอัปเดตเมื่อมีการอัปเดตใหม่ (ดังนั้น เมื่อพนักงานบริการอยู่ในสถานะรอ) และจะส่งข้อความ SKIP_WAITING
ไปยังพนักงานบริการ
เราจำเป็นต้องอัปเดตไฟล์พนักงานบริการและจัดการเหตุการณ์ SKIP_WAITING
ให้เรียก skipWaiting
:
//add in src-sw.js addEventListener("message", event => { if (event.data && event.data.type === "SKIP_WAITING") { skipWaiting(); });
ตอนนี้ให้รัน npm run dev
จากนั้นโหลดหน้าซ้ำ ไปที่รหัสของคุณและอัปเดตชื่อแถบนำทางเป็น "Navbar v2" โหลดหน้าซ้ำอีกครั้ง และคุณควรจะเห็นไอคอนอัปเดต
ห่อ
ขณะนี้เว็บไซต์ของเราทำงานแบบออฟไลน์และสามารถแจ้งให้ผู้ใช้ทราบเกี่ยวกับการอัปเดตใหม่ๆ โปรดจำไว้ว่า ปัจจัยที่สำคัญที่สุดในการสร้างการประปาส่วนภูมิภาคคือประสบการณ์ของผู้ใช้ มุ่งเน้นที่การสร้างประสบการณ์ที่ผู้ใช้ของคุณใช้งานง่ายเสมอ เราในฐานะนักพัฒนา มักจะตื่นเต้นกับเทคโนโลยีมากเกินไป และมักจะจบลงด้วยการลืมผู้ใช้ของเรา
หากคุณต้องการก้าวไปอีกขั้น คุณสามารถเพิ่มรายการเว็บแอป ซึ่งจะทำให้ผู้ใช้ของคุณเพิ่มไซต์ลงในหน้าจอหลักได้ และหากคุณต้องการทราบข้อมูลเพิ่มเติมเกี่ยวกับ Workbox คุณสามารถค้นหาเอกสารอย่างเป็นทางการได้จากเว็บไซต์ Workbox
อ่านเพิ่มเติม เกี่ยวกับ SmashingMag:
- คุณสามารถสร้างรายได้ด้วยแอพมือถือหรือ กปภ. ได้หรือไม่?
- คู่มือที่กว้างขวางสำหรับแอปพลิเคชันเว็บแบบก้าวหน้า
- Native และ PWA: ทางเลือก ไม่ใช่ผู้ท้าชิง!
- การสร้าง กปภ. โดยใช้ Angular 6