Svelte-Specific PWA Patterns
Svelte store สำหรับสถานะ PWA
หัวข้อที่มีชื่อว่า “Svelte store สำหรับสถานะ PWA”Store contract ของ Svelte — object ใดก็ตามที่มี method subscribe — เหมาะอย่างยิ่งสำหรับเก็บ PWA lifecycle event เหตุการณ์เช่น การออกไปใช้งาน offline, SW ใหม่พร้อมใช้งาน, หรือผู้ใช้ปฏิเสธ install prompt จะเกิดขึ้นแบบ async และในเวลาที่คาดเดาไม่ได้ Store ทำให้ state นั้น reactive และพร้อมใช้งานในทุกส่วนของ component tree โดยไม่ต้องส่ง prop
Online / offline store
หัวข้อที่มีชื่อว่า “Online / offline store”import { readable } from 'svelte/store';
export const online = readable( typeof navigator !== 'undefined' ? navigator.onLine : true, (set) => { const handleOnline = () => set(true); const handleOffline = () => set(false);
window.addEventListener('online', handleOnline); window.addEventListener('offline', handleOffline);
return () => { window.removeEventListener('online', handleOnline); window.removeEventListener('offline', handleOffline); }; });ใช้งานใน component ใดก็ได้ด้วย shorthand $ สำหรับ auto-subscribe:
<script> import { online } from '$lib/stores/network';</script>
{#if !$online} <div class="offline-banner">คุณออฟไลน์อยู่ — กำลังแสดงเนื้อหาจาก cache</div>{/if}การป้องกัน typeof navigator !== 'undefined' จำเป็น เนื่องจาก SvelteKit รัน component บน server ระหว่าง SSR ซึ่ง navigator ไม่มีอยู่
Install-prompt store และ component
หัวข้อที่มีชื่อว่า “Install-prompt store และ component”Event beforeinstallprompt จะ fire เมื่อ browser ตัดสินใจว่า PWA ของคุณสามารถติดตั้งได้ คุณต้องจับ event object ไว้ — เป็น ephemeral — และเรียก .prompt() ในภายหลังเมื่อผู้ใช้โต้ตอบกับ install button ของคุณ
import { writable } from 'svelte/store';
type BeforeInstallPromptEvent = Event & { prompt: () => Promise<void>; userChoice: Promise<{ outcome: 'accepted' | 'dismissed' }>;};
export const installPromptEvent = writable<BeforeInstallPromptEvent | null>(null);
if (typeof window !== 'undefined') { window.addEventListener('beforeinstallprompt', (e) => { e.preventDefault(); // ป้องกัน mini-infobar installPromptEvent.set(e as BeforeInstallPromptEvent); });}Component ที่ใช้ store เพื่อแสดง install button:
<script lang="ts"> import { installPromptEvent } from '$lib/stores/install-prompt';
async function install() { if (!$installPromptEvent) return; await $installPromptEvent.prompt(); const { outcome } = await $installPromptEvent.userChoice; if (outcome === 'accepted') { installPromptEvent.set(null); // ซ่อน button หลังจากติดตั้ง } }</script>
{#if $installPromptEvent} <button on:click={install}>Install App</button>{/if}Reactive “update available” state
หัวข้อที่มีชื่อว่า “Reactive “update available” state”คุณสามารถเก็บ update state ใน plain writable store เพื่อให้ UI ทุกส่วน react ต่อ state นี้ได้:
import { writable } from 'svelte/store';
export const updateAvailable = writable(false);export let swRegistration: ServiceWorkerRegistration | null = null;
export async function initSW() { if (!('serviceWorker' in navigator)) return;
const registration = await navigator.serviceWorker.register( '/service-worker.js', { type: import.meta.env.DEV ? 'module' : 'classic' } ); swRegistration = registration;
registration.addEventListener('updatefound', () => { const newWorker = registration.installing; if (!newWorker) return; newWorker.addEventListener('statechange', () => { if (newWorker.state === 'installed' && navigator.serviceWorker.controller) { updateAvailable.set(true); } }); });}
export function applyUpdate() { swRegistration?.waiting?.postMessage({ type: 'SKIP_WAITING' }); window.location.reload();}เชื่อมต่อ store ใน src/routes/+layout.svelte:
<script lang="ts"> import { onMount } from 'svelte'; import { initSW, updateAvailable, applyUpdate } from '$lib/stores/sw-update';
onMount(() => { initSW(); });</script>
<slot />
{#if $updateAvailable} <div class="update-banner"> มีเวอร์ชันใหม่พร้อมใช้งาน <button on:click={applyUpdate}>อัปเดตตอนนี้</button> </div>{/if}SSR และ adapter notes
หัวข้อที่มีชื่อว่า “SSR และ adapter notes”Service worker ทำงานได้เฉพาะใน browser เท่านั้น สองปัญหาที่พบบ่อยใน SvelteKit:
navigator/windowใน store initializer ให้ป้องกันด้วยtypeof window !== 'undefined'หรือรันโค้ดภายในonMountเสมอadapter-staticสำหรับแอปที่ offline ทั้งหมด หากต้องการให้ผู้ใช้เปิดทุกหน้าขณะ offline ทุก route ต้องถูก prerender ตั้งค่าexport const prerender = trueใน root+layout.js(หรือใช้adapter-staticซึ่งบังคับ prerendering) SW จะ precache หน้าที่ prerender แล้วทั้งหมดผ่าน exportprerenderedจาก$service-worker
// src/routes/+layout.js — opt ทั้งแอปเข้าสู่ prerenderingexport const prerender = true;ด้วย adapter-node หรือ adapter-vercel เฉพาะ route ที่คุณ prerender อย่างชัดเจนเท่านั้นที่ใช้ได้ offline Route ที่ render บน server แบบ dynamic ต้องการการเชื่อมต่อเครือข่าย
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ตัวเลือก | Benefit | Cost |
|---|---|---|
| ผูก PWA state เข้ากับ Svelte store | UI reactive ต่อ install/update state ทันทีทั่วทั้งแอปผ่าน store เดียว | ต้องออกแบบ store structure ให้ดีไม่งั้น state กระจัดกระจาย |
| จัดการ PWA state แบบแยกเป็น local component variable | เขียนง่ายในเคสเล็กๆ ไม่ต้องตั้ง store | ไม่ sync กันข้าม component ทำให้ UI ส่วนต่างๆ เห็น state ไม่ตรงกัน |
ข้อผิดพลาดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ข้อผิดพลาดที่พบบ่อย”- subscribe store ใน component โดยไม่ unsubscribe (ไม่ใช้
$auto-subscription หรือ onDestroy) ทำให้เกิด memory leak - เขียน logic ของ service worker ปนอยู่ใน component โดยตรงแทนที่จะแยกเป็น store/module ทำให้ reuse ข้ามหน้าไม่ได้
- ไม่เช็คว่าโค้ดรันฝั่ง client จริง (SvelteKit SSR) ก่อนเข้าถึง
navigator.serviceWorkerทำให้ build พังตอน SSR
💡 ตัวอย่างจากของจริง
Vite PWA ecosystem (SvelteKit) — ใช้ store pattern ครอบ
virtual:pwa-register/svelteให้ UI reactive ต่อ update state ได้ทันทีเครื่องมือแบบ Squoosh ที่พอร์ตมาเป็น SvelteKit — ใช้ Svelte store เก็บสถานะ installable/offline ให้ทุก component อ่านค่าเดียวกัน