提示:本网站现在为维护调试状态

PWA Offline App in Practice: Service Worker + IndexedDB + Background Sync

Choyeon
Choyeon
Sep 28, 2026 1 min read 330 Views
906 words 5,319 chars Updated Nov 8, 2026
PWA Offline App in Practice: Service Worker + IndexedDB + Background Sync
Table of contents

PWA is natively supported in Chrome/Edge/Safari/iOS 16.4+. Installs to user home screen, works offline, accepts writes offline, replays on network return. Experience approaches native without App Store review walls.

Service Worker Caching Strategies

App Shell (HTML + first-paint assets) uses NetworkFirst with 3s timeout fallback to cache for instant weak-network render. Static images/fonts/scripts use Cache First with capacity caps. API GET list endpoints use Stale-While-Revalidate — return cache instantly while background silently fetches fresh copy — always-new data with zero perceived latency.

// ====== sw.ts Service Worker - Vite 推荐用 vite-plugin-pwa ======
import { PrecacheEntry } from 'workbox-precaching/_types';
import { precacheAndRoute, cleanupOutdatedCaches } from 'workbox-precaching';
import { registerRoute, NavigationRoute } from 'workbox-routing';
import { CacheFirst, StaleWhileRevalidate, NetworkFirst } from 'workbox-strategies';
import { CacheableResponsePlugin } from 'workbox-cacheable-response';
import { ExpirationPlugin } from 'workbox-expiration';
import { backgroundSyncQueue } from './lib/bg-sync';

declare const self: ServiceWorkerGlobalScope;

cleanupOutdatedCaches();
precacheAndRoute(self.__WB_MANIFEST as unknown as PrecacheEntry[]);

// 1. SPA 导航请求:NetworkFirst,失败 fallback 到 /index.html
registerRoute(
  new NavigationRoute(new NetworkFirst({
    networkTimeoutSeconds: 3,
    cacheName: 'app-shell',
    plugins: [new CacheableResponsePlugin({ statuses: [0, 200] })]
  }))
);

// 2. 静态资源:Cache First 且限制数量
registerRoute(
  ({ request, sameOrigin }) => sameOrigin &&
    ['style', 'script', 'worker', 'font', 'image'].includes(request.destination),
  new CacheFirst({
    cacheName: 'static-assets-v1',
    plugins: [
      new CacheableResponsePlugin({ statuses: [0, 200] }),
      new ExpirationPlugin({ maxEntries: 200, maxAgeSeconds: 60 * 60 * 24 * 30 })
    ]
  })
);

// 3. API GET:Stale-While-Revalidate — 立返缓存,后台静默更新
registerRoute(
  ({ url, request }) => request.method === 'GET' && url.pathname.startsWith('/api/v1/'),
  new StaleWhileRevalidate({
    cacheName: 'api-read-v1',
    plugins: [
      new CacheableResponsePlugin({ statuses: [200] }),
      new ExpirationPlugin({ maxEntries: 500, maxAgeSeconds: 60 * 5 })
    ]
  })
);

// 4. API POST/PUT/DELETE:失败时进入 Background Sync 队列
self.addEventListener('fetch', (event: FetchEvent) => {
  const req = event.request;
  if (!['POST', 'PUT', 'PATCH', 'DELETE'].includes(req.method)) return;
  if (!req.url.includes('/api/v1/')) return;
  event.respondWith((async () => {
    try {
      const cloned = req.clone();
      return await fetch(req);
    } catch (err) {
      await backgroundSyncQueue.enqueue(cloned);
      await self.registration.sync.register('outbox-sync');
      return new Response(JSON.stringify({
        ok: false,
        queued: true,
        message: '已进入离线队列,网络恢复后自动发送'
      }), { status: 202, headers: { 'Content-Type': 'application/json' } });
    }
  })());
});

// ====== lib/idb.ts — IndexedDB 封装(用 idb 库)======
import { openDB, DBSchema, IDBPDatabase } from 'idb';

interface AppDB extends DBSchema {
  drafts: {
    key: string;
    value: { id: string; title: string; body: string; createdAt: number; pending: boolean };
    indexes: { 'by-created': number; 'by-pending': boolean };
  };
  settings: { key: string; value: any };
}

const DB_NAME = 'pwa-app-db';
const DB_VERSION = 2;

export async function getDB(): Promise<IDBPDatabase<AppDB>> {
  return openDB<AppDB>(DB_NAME, DB_VERSION, {
    upgrade(db, oldVer, newVer, tx) {
      if (!db.objectStoreNames.contains('drafts')) {
        const s = db.createObjectStore('drafts', { keyPath: 'id' });
        s.createIndex('by-created', 'createdAt');
        s.createIndex('by-pending', 'pending');
      }
      if (!db.objectStoreNames.contains('settings')) {
        db.createObjectStore('settings');
      }
    }
  });
}

export const DraftRepo = {
  async list(pending?: boolean) {
    const db = await getDB();
    if (pending !== undefined) return db.getAllFromIndex('drafts', 'by-pending', pending);
    return db.getAll('drafts');
  },
  async save(draft: AppDB['drafts']['value']) {
    const db = await getDB();
    await db.put('drafts', draft);
  },
  async delete(id: string) {
    const db = await getDB();
    await db.delete('drafts', id);
  }
};

// ====== 应用入口 main.ts 注册 SW + 离线状态监听 ======
import { registerSW } from 'virtual:pwa-register';

const updateSW = registerSW({
  immediate: true,
  onOfflineReady() { console.log('[PWA] 缓存就绪,可离线使用'); },
  onNeedRefresh() {
    if (confirm('检测到新版本,是否立即刷新?')) updateSW(true);
  },
});

window.addEventListener('online', async () => {
  if ('SyncManager' in window) return;
  // 浏览器不支持 SyncManager 时兜底:上线立即重放队列
  const pending = await DraftRepo.list(true);
  for (const d of pending) {
    try {
      await fetch('/api/v1/drafts', { method: 'POST', headers: {'Content-Type': 'application/json'},
        body: JSON.stringify(d) });
      d.pending = false; await DraftRepo.save(d);
    } catch {}
  }
});

export function useApp() {
  const isOnline = ref(navigator.onLine);
  onMounted(() => {
    window.addEventListener('online',  () => (isOnline.value = true));
    window.addEventListener('offline', () => (isOnline.value = false));
  });
  return { isOnline, DraftRepo };
}

IndexedDB Structured Store + Background Sync

localStorage is 5MB sync-blocking and unsuitable for structured data. IndexedDB stores drafts/chat/orders with queries, indexes, batches, multi-GB capacity. All writes (POST/PUT/DELETE) that fail go into Background Sync queue; sync event fires when user reconnects and SW replays them in sequence, guaranteeing zero loss for offline actions.

Storage Quota Async Structured/Indexes Persistence Use Case
localStorage ~5MB No No Partial manual purge Tokens, prefs
sessionStorage ~5MB No No Per session only Temp state
Cache API Unbounded (policy) Yes URL key / Response Per cache strategy Static files + API GET responses
IndexedDB Tens of GB Yes Full schema/indexes/transactions Persistent Structured business data
OPFS (File System) Hundreds GB Yes File stream API Persistent Large files, media editing

Best Practices

Use Vite + vite-plugin-pwa to skip manual Manifest/SW boilerplate. Three iOS gotchas: SW resets every 7 days, push requires add-to-home-screen, video autoplay needs mute. Always give app-layer UI feedback for writes: "Saved as draft. Will sync when you're back online."

X微博Telegram

About the author

Poll

你最希望 Rosetta 优先加入哪项功能?

社区投票(演示数据,可后台删除)

Comments (4)

0 / 3000
独立开发者阿明
独立开发者阿明aming.devSep 30, 2026

看到博主推荐的这个 UI 组件库了,之前一直在纠结选哪个,试了一下确实不错,文档也很友好!

奶茶味程序媛
奶茶味程序媛Sep 29, 2026

建议可以加一下关于无障碍访问(a11y)的部分,现在很多项目都不重视这块,但真的很重要。

熬夜攻城狮
熬夜攻城狮nightowl.techSep 29, 2026

有个小小的建议:代码示例如果能加上语法高亮和行号就更好啦,阅读体验会更上一层楼~

安全小白白
安全小白白seclab.digitalSep 28, 2026

作为一个后端转全栈的,看你的前端文章从来不会觉得难懂,讲得特别接地气,谢谢!

Related posts

Astro Deep Dive: Islands Architecture & Content Collections

Astro Deep Dive: Islands Architecture & Content Collections

Astro innovations: Islands Architecture for minimal JS with partial hydration, Content Collections for type-safe Markdown/MDX management, ultra-fast zero-JS static sites.

1 min read
Vue 3 Composition API in Practice

Vue 3 Composition API in Practice

Vue 3's Composition API fundamentally changed how Vue components are organized. Through building a complex e-commerce filter component, this article demonstrates ref vs reactive choices, proper computed/watch usage, custom Composable extraction principles, lifecycle hook timing, and the correct approach to cross-layer state via provide/inject.

1 min read
Tailwind CSS v4: Zero-config + CSS-native Variables

Tailwind CSS v4: Zero-config + CSS-native Variables

Tailwind CSS v4 is an architectural upgrade: goodbye tailwind.config.js, declare design tokens in @theme blocks inside native CSS files. Single @import "tailwindcss" entry replaces postcss.config; built-in CSS vars, native container queries, oxc engine 10-40x faster, simplified color functions.

1 min read
Frontend Performance: Core Web Vitals in Practice

Frontend Performance: Core Web Vitals in Practice

Practical Google Core Web Vitals playbook: LCP via preconnects/image-priority/CDN, CLS elimination via image dimensions/font-loading/placeholders, INP via long-task splitting + debouncing.

1 min read
React 18 Hooks Best Practices

React 18 Hooks Best Practices

React 18 introduces revolutionary features like concurrent rendering, automatic batching, useTransition, and useDeferredValue. This article systematically covers correct useState/useEffect usage, useMemo/useCallback optimization boundaries, custom Hook design patterns, and a complete guide to avoiding stale closures, race conditions, and other pitfalls in concurrent mode.

1 min read
Complete Guide to CSS Container Queries

Complete Guide to CSS Container Queries

Modern responsive CSS pillars: Container Queries for component-level responsiveness based on parent size (not viewport), plus :has() parent selector for ancestor styling based on descendants.

1 min read