ข้ามไปยังเนื้อหา

Svelte-Specific PWA Patterns

Store contract ของ Svelte — object ใดก็ตามที่มี method subscribe — เหมาะอย่างยิ่งสำหรับเก็บ PWA lifecycle event เหตุการณ์เช่น การออกไปใช้งาน offline, SW ใหม่พร้อมใช้งาน, หรือผู้ใช้ปฏิเสธ install prompt จะเกิดขึ้นแบบ async และในเวลาที่คาดเดาไม่ได้ Store ทำให้ state นั้น reactive และพร้อมใช้งานในทุกส่วนของ component tree โดยไม่ต้องส่ง prop

src/lib/stores/network.ts
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 ไม่มีอยู่

Event beforeinstallprompt จะ fire เมื่อ browser ตัดสินใจว่า PWA ของคุณสามารถติดตั้งได้ คุณต้องจับ event object ไว้ — เป็น ephemeral — และเรียก .prompt() ในภายหลังเมื่อผู้ใช้โต้ตอบกับ install button ของคุณ

src/lib/stores/install-prompt.ts
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:

src/lib/InstallButton.svelte
<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}

คุณสามารถเก็บ update state ใน plain writable store เพื่อให้ UI ทุกส่วน react ต่อ state นี้ได้:

src/lib/stores/sw-update.ts
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:

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}

Service worker ทำงานได้เฉพาะใน browser เท่านั้น สองปัญหาที่พบบ่อยใน SvelteKit:

  1. navigator / window ใน store initializer ให้ป้องกันด้วย typeof window !== 'undefined' หรือรันโค้ดภายใน onMount เสมอ
  2. adapter-static สำหรับแอปที่ offline ทั้งหมด หากต้องการให้ผู้ใช้เปิดทุกหน้าขณะ offline ทุก route ต้องถูก prerender ตั้งค่า export const prerender = true ใน root +layout.js (หรือใช้ adapter-static ซึ่งบังคับ prerendering) SW จะ precache หน้าที่ prerender แล้วทั้งหมดผ่าน export prerendered จาก $service-worker
// src/routes/+layout.js — opt ทั้งแอปเข้าสู่ prerendering
export const prerender = true;

ด้วย adapter-node หรือ adapter-vercel เฉพาะ route ที่คุณ prerender อย่างชัดเจนเท่านั้นที่ใช้ได้ offline Route ที่ render บน server แบบ dynamic ต้องการการเชื่อมต่อเครือข่าย

ตัวเลือกBenefitCost
ผูก PWA state เข้ากับ Svelte storeUI 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 อ่านค่าเดียวกัน

ทำไมต้องเรียก e.preventDefault() บน event beforeinstallprompt?
ทำไมต้องป้องกัน navigator.onLine ด้วย typeof navigator !== "undefined" ใน store initializer?
SvelteKit adapter ใดที่ทำให้ทุกหน้าถูก prerender เพื่อให้ SW precache ไว้ใช้ offline ได้?
สถานที่ที่ถูกต้องในการเรียก initSW() ในแอป SvelteKit คือที่ไหน?