[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"public:announcements":3,"footer:categories:zh":4,"public:navigations:zh":132,"post:detail:post-30-PWA离线应用实战:zh":177},[],[5,23,39,55,71,88,103,117],{"id":6,"name":7,"slug":12,"description":13,"icon":18,"color":19,"cover_image":20,"created_at":21,"post_count":22},1,{"en":8,"ja":9,"zh":10,"zh_Hant":11},"Technology","テクノロジー","技术","技術","technology",{"en":14,"ja":15,"zh":16,"zh_Hant":17},"Cutting-edge technology exploration and in-depth analysis covering programming languages, frameworks, and architecture design","プログラミング言語、フレームワーク、アーキテクチャ設計を含む先端技術の探究","前沿技术探索与深度分析，涵盖编程语言、框架、架构设计等核心领域","前沿技術探索與深度分析，涵蓋編程語言、框架、架構設計等核心領域","heroicons:cog-6-tooth","#6366f1",null,"2026-09-30T18:09:53.633610Z",8,{"id":24,"name":25,"slug":29,"description":30,"icon":35,"color":36,"cover_image":20,"created_at":37,"post_count":38},2,{"en":26,"ja":27,"zh":28,"zh_Hant":28},"Tutorial","チュートリアル","教程","tutorial",{"en":31,"ja":32,"zh":33,"zh_Hant":34},"Hands-on tutorials from scratch with clear steps and complete code for developers at all levels","ゼロから始める実践的なチュートリアル。明確な手順と完全なコードで、全てのレベルの開発者に最適","从零开始的实战教程，步骤清晰，代码完整，适合各阶段开发者学习","從零開始的實戰教程，步驟清晰，代碼完整，適合各階段開發者學習","heroicons:academic-cap","#10b981","2026-09-30T18:09:53.641581Z",6,{"id":40,"name":41,"slug":45,"description":46,"icon":51,"color":52,"cover_image":20,"created_at":53,"post_count":54},3,{"en":42,"ja":43,"zh":44,"zh_Hant":44},"Frontend","フロントエンド","前端","frontend",{"en":47,"ja":48,"zh":49,"zh_Hant":50},"Full coverage of web frontend development including Vue, React, new CSS features, and performance optimization","Vue、React、CSSの新機能やパフォーマンス最適化を含むWebフロントエンド開発の全領域","Web前端开发全栈覆盖，包括Vue、React、CSS新特性及性能优化等话题","Web前端開發全棧覆蓋，包括Vue、React、CSS新特性及性能優化等話題","heroicons:computer-desktop","#ec4899","2026-09-30T18:09:53.644838Z",7,{"id":56,"name":57,"slug":62,"description":63,"icon":68,"color":69,"cover_image":20,"created_at":70,"post_count":40},4,{"en":58,"ja":59,"zh":60,"zh_Hant":61},"Backend","バックエンド","后端","後端","backend",{"en":64,"ja":65,"zh":66,"zh_Hant":67},"Server-side development best practices including API design, database optimization, and microservices architecture","API設計、データベース最適化、マイクロサービスアーキテクチャを含むサーバーサイド開発の実践","服务端开发实战经验，API设计、数据库优化、微服务架构等深度内容","服務端開發實戰經驗，API設計、數據庫優化、微服務架構等深度內容","heroicons:server-stack","#f59e0b","2026-09-30T18:09:53.647836Z",{"id":72,"name":73,"slug":78,"description":79,"icon":84,"color":85,"cover_image":20,"created_at":86,"post_count":87},5,{"en":74,"ja":75,"zh":76,"zh_Hant":77},"Fullstack","フルスタック","全栈","全棧","fullstack",{"en":80,"ja":81,"zh":82,"zh_Hant":83},"End-to-end development guides covering the full project lifecycle and technology stack selection","フロントエンドからバックエンドまでの開発プロセス全体をカバーするガイド","从前端到后端的全流程开发指南，覆盖完整项目生命周期与技术选型","從前端到後端的全流程開發指南，覆蓋完整項目生命週期與技術選型","heroicons:swatch","#8b5cf6","2026-09-30T18:09:53.650513Z",0,{"id":38,"name":89,"slug":94,"description":95,"icon":100,"color":101,"cover_image":20,"created_at":102,"post_count":56},{"en":90,"ja":91,"zh":92,"zh_Hant":93},"Essays","エッセイ","随笔","隨筆","essays",{"en":96,"ja":97,"zh":98,"zh_Hant":99},"Technical reflections and development insights recording the programmer's growth journey","プログラマーの成長の軌跡を記録する技術的考察と開発の洞察","技术思考与开发心得分享，记录程序员成长路上的点滴感悟","技術思考與開發心得分享，記錄程序員成長路上的點滴感悟","heroicons:pencil-square","#f43f5e","2026-09-30T18:09:53.653105Z",{"id":54,"name":104,"slug":108,"description":109,"icon":114,"color":115,"cover_image":20,"created_at":116,"post_count":56},{"en":105,"ja":106,"zh":107,"zh_Hant":107},"Tools","ツール","工具","tools",{"en":110,"ja":111,"zh":112,"zh_Hant":113},"Development toolchain and productivity tool recommendations to supercharge your development experience","開発効率を飛躍的に向上させるツールチェーンと生産性ツールの推奨","开发工具链与效率提升利器推荐，让你的开发体验如虎添翼","開發工具鏈與效率提升利器推薦，讓你的開發體驗如虎添翼","heroicons:wrench-screwdriver","#06b6d4","2026-09-30T18:09:53.655605Z",{"id":22,"name":118,"slug":123,"description":124,"icon":129,"color":130,"cover_image":20,"created_at":131,"post_count":87},{"en":119,"ja":120,"zh":121,"zh_Hant":122},"Translation","翻訳","翻译","翻譯","translation",{"en":125,"ja":126,"zh":127,"zh_Hant":128},"Curated translations of high-quality foreign technical articles to keep up with international trends","国際的な技術トレンドを追うための厳選された海外技術記事の翻訳","优质外文技术文章精选翻译，紧跟国际技术前沿动态","優質外文技術文章精選翻譯，緊跟國際技術前沿動態","heroicons:language","#84cc16","2026-09-30T18:09:53.658041Z",[133,144,150,157,164,171],{"id":6,"title":134,"url":139,"icon":20,"parent_id":20,"location":140,"order":6,"is_active":141,"target_blank":142,"created_at":143,"updated_at":20},{"zh":135,"en":136,"ja":137,"zh_TW":138},"首页","Home","ホーム","首頁","\u002F","header",true,false,"2026-09-30T18:09:54.251095Z",{"id":24,"title":145,"url":149,"icon":20,"parent_id":20,"location":140,"order":24,"is_active":141,"target_blank":142,"created_at":143,"updated_at":20},{"zh":146,"en":147,"ja":148,"zh_TW":146},"文章","Posts","記事","\u002Fposts",{"id":40,"title":151,"url":156,"icon":20,"parent_id":20,"location":140,"order":40,"is_active":141,"target_blank":142,"created_at":143,"updated_at":20},{"zh":152,"en":153,"ja":154,"zh_TW":155},"分类","Categories","カテゴリー","分類","\u002Fcategories",{"id":56,"title":158,"url":163,"icon":20,"parent_id":20,"location":140,"order":56,"is_active":141,"target_blank":142,"created_at":143,"updated_at":20},{"zh":159,"en":160,"ja":161,"zh_TW":162},"标签","Tags","タグ","標籤","\u002Ftags",{"id":72,"title":165,"url":170,"icon":20,"parent_id":20,"location":140,"order":72,"is_active":141,"target_blank":142,"created_at":143,"updated_at":20},{"zh":166,"en":167,"ja":168,"zh_TW":169},"关于","About","概要","關於","\u002Fpage\u002Fabout",{"id":38,"title":172,"url":176,"icon":20,"parent_id":20,"location":140,"order":38,"is_active":141,"target_blank":142,"created_at":143,"updated_at":20},{"zh":173,"en":174,"ja":175,"zh_TW":173},"留言板","Guestbook","ゲストブック","\u002Fpage\u002Fguestbook",{"id":178,"title":179,"subtitle":20,"slug":180,"source":181,"source_url":20,"audio":20,"video":20,"video_url":20,"content":182,"excerpt":183,"cover_image":20,"author":184,"category":187,"tags":188,"status":210,"visibility":211,"password":20,"views":212,"likes_count":87,"is_pinned":142,"allow_comments":141,"comments_count":38,"is_password_protected":142,"meta_title":179,"meta_description":183,"meta_keywords":213,"created_at":214,"published_at":214,"updated_at":215,"reading_time":6},30,"PWA 离线应用实战：Service Worker + IndexedDB + Background Sync","post-30-PWA离线应用实战","原创","# PWA 离线应用实战：Service Worker + IndexedDB + Background Sync\n\nPWA（Progressive Web App）在 Chrome\u002FEdge\u002FSafari\u002FiOS 16.4+ 都已原生支持，能把 Web 应用安装到用户桌面，离线可用、离线提交、网络恢复自动同步，体验已接近原生 App，且无需过审上架商店。\n\n## Service Worker 缓存策略\n\nApp Shell（HTML壳+首屏资源）用 NetworkFirst，3 秒未响应回退缓存，保障弱网秒开；静态图片\u002F字体\u002F脚本用 Cache First + 容量上限；API GET 列表页用 Stale-While-Revalidate（立返缓存 + 后台静默更新），让数据总是新的且加载无延迟。\n\n```typescript\n```typescript\n\u002F\u002F ====== sw.ts Service Worker - Vite 推荐用 vite-plugin-pwa ======\nimport { PrecacheEntry } from 'workbox-precaching\u002F_types';\nimport { precacheAndRoute, cleanupOutdatedCaches } from 'workbox-precaching';\nimport { registerRoute, NavigationRoute } from 'workbox-routing';\nimport { CacheFirst, StaleWhileRevalidate, NetworkFirst } from 'workbox-strategies';\nimport { CacheableResponsePlugin } from 'workbox-cacheable-response';\nimport { ExpirationPlugin } from 'workbox-expiration';\nimport { backgroundSyncQueue } from '.\u002Flib\u002Fbg-sync';\n\ndeclare const self: ServiceWorkerGlobalScope;\n\ncleanupOutdatedCaches();\nprecacheAndRoute(self.__WB_MANIFEST as unknown as PrecacheEntry[]);\n\n\u002F\u002F 1. SPA 导航请求：NetworkFirst，失败 fallback 到 \u002Findex.html\nregisterRoute(\n  new NavigationRoute(new NetworkFirst({\n    networkTimeoutSeconds: 3,\n    cacheName: 'app-shell',\n    plugins: [new CacheableResponsePlugin({ statuses: [0, 200] })]\n  }))\n);\n\n\u002F\u002F 2. 静态资源：Cache First 且限制数量\nregisterRoute(\n  ({ request, sameOrigin }) => sameOrigin &&\n    ['style', 'script', 'worker', 'font', 'image'].includes(request.destination),\n  new CacheFirst({\n    cacheName: 'static-assets-v1',\n    plugins: [\n      new CacheableResponsePlugin({ statuses: [0, 200] }),\n      new ExpirationPlugin({ maxEntries: 200, maxAgeSeconds: 60 * 60 * 24 * 30 })\n    ]\n  })\n);\n\n\u002F\u002F 3. API GET：Stale-While-Revalidate — 立返缓存，后台静默更新\nregisterRoute(\n  ({ url, request }) => request.method === 'GET' && url.pathname.startsWith('\u002Fapi\u002Fv1\u002F'),\n  new StaleWhileRevalidate({\n    cacheName: 'api-read-v1',\n    plugins: [\n      new CacheableResponsePlugin({ statuses: [200] }),\n      new ExpirationPlugin({ maxEntries: 500, maxAgeSeconds: 60 * 5 })\n    ]\n  })\n);\n\n\u002F\u002F 4. API POST\u002FPUT\u002FDELETE：失败时进入 Background Sync 队列\nself.addEventListener('fetch', (event: FetchEvent) => {\n  const req = event.request;\n  if (!['POST', 'PUT', 'PATCH', 'DELETE'].includes(req.method)) return;\n  if (!req.url.includes('\u002Fapi\u002Fv1\u002F')) return;\n  event.respondWith((async () => {\n    try {\n      const cloned = req.clone();\n      return await fetch(req);\n    } catch (err) {\n      await backgroundSyncQueue.enqueue(cloned);\n      await self.registration.sync.register('outbox-sync');\n      return new Response(JSON.stringify({\n        ok: false,\n        queued: true,\n        message: '已进入离线队列，网络恢复后自动发送'\n      }), { status: 202, headers: { 'Content-Type': 'application\u002Fjson' } });\n    }\n  })());\n});\n\n\u002F\u002F ====== lib\u002Fidb.ts — IndexedDB 封装（用 idb 库）======\nimport { openDB, DBSchema, IDBPDatabase } from 'idb';\n\ninterface AppDB extends DBSchema {\n  drafts: {\n    key: string;\n    value: { id: string; title: string; body: string; createdAt: number; pending: boolean };\n    indexes: { 'by-created': number; 'by-pending': boolean };\n  };\n  settings: { key: string; value: any };\n}\n\nconst DB_NAME = 'pwa-app-db';\nconst DB_VERSION = 2;\n\nexport async function getDB(): Promise\u003CIDBPDatabase\u003CAppDB>> {\n  return openDB\u003CAppDB>(DB_NAME, DB_VERSION, {\n    upgrade(db, oldVer, newVer, tx) {\n      if (!db.objectStoreNames.contains('drafts')) {\n        const s = db.createObjectStore('drafts', { keyPath: 'id' });\n        s.createIndex('by-created', 'createdAt');\n        s.createIndex('by-pending', 'pending');\n      }\n      if (!db.objectStoreNames.contains('settings')) {\n        db.createObjectStore('settings');\n      }\n    }\n  });\n}\n\nexport const DraftRepo = {\n  async list(pending?: boolean) {\n    const db = await getDB();\n    if (pending !== undefined) return db.getAllFromIndex('drafts', 'by-pending', pending);\n    return db.getAll('drafts');\n  },\n  async save(draft: AppDB['drafts']['value']) {\n    const db = await getDB();\n    await db.put('drafts', draft);\n  },\n  async delete(id: string) {\n    const db = await getDB();\n    await db.delete('drafts', id);\n  }\n};\n\n\u002F\u002F ====== 应用入口 main.ts 注册 SW + 离线状态监听 ======\nimport { registerSW } from 'virtual:pwa-register';\n\nconst updateSW = registerSW({\n  immediate: true,\n  onOfflineReady() { console.log('[PWA] 缓存就绪，可离线使用'); },\n  onNeedRefresh() {\n    if (confirm('检测到新版本，是否立即刷新？')) updateSW(true);\n  },\n});\n\nwindow.addEventListener('online', async () => {\n  if ('SyncManager' in window) return;\n  \u002F\u002F 浏览器不支持 SyncManager 时兜底：上线立即重放队列\n  const pending = await DraftRepo.list(true);\n  for (const d of pending) {\n    try {\n      await fetch('\u002Fapi\u002Fv1\u002Fdrafts', { method: 'POST', headers: {'Content-Type': 'application\u002Fjson'},\n        body: JSON.stringify(d) });\n      d.pending = false; await DraftRepo.save(d);\n    } catch {}\n  }\n});\n\nexport function useApp() {\n  const isOnline = ref(navigator.onLine);\n  onMounted(() => {\n    window.addEventListener('online',  () => (isOnline.value = true));\n    window.addEventListener('offline', () => (isOnline.value = false));\n  });\n  return { isOnline, DraftRepo };\n}\n```\n```\n\n## IndexedDB 结构化存储 + Background Sync\n\nlocalStorage 只有 5MB 同步阻塞，不适合结构化数据。用 IndexedDB 存草稿\u002F聊天记录\u002F订单，可查询、可索引、可批量，容量可达数 GB。所有写操作（POST\u002FPUT\u002FDELETE）失败时推入 Background Sync 队列，注册 sync 事件，用户重新联网后 Service Worker 自动按顺序重放，保证离线操作 100% 不丢。\n\n| 存储技术 | 容量 | 异步 | 结构化\u002F索引 | 持久化 | 适用场景 |\n|---------|-----|-----|-----------|-------|---------|\n| localStorage | ~5MB | 否 | 否 | 部分手动清 | Token\u002F偏好设置 |\n| sessionStorage | ~5MB | 否 | 否 | 会话内消失 | 临时状态 |\n| Cache API | 不限策略制 | 是 | URL键\u002FResponse | 按缓存策略 | 静态文件+API GET响应 |\n| IndexedDB | 数十GB | 是 | 完整Schema\u002F索引\u002F事务 | 持久保存 | 结构化业务数据 |\n| OPFS (File System) | 数百GB | 是 | 文件流API | 持久保存 | 大文件\u002F媒体编辑 |\n\n## 最佳实践\n\n用 Vite + vite-plugin-pwa 避免手写 Manifest\u002FSW。iOS 有 3 个大坑：SW 每 7 天重置、推送通知需添加到主屏后才可用、视频 autoplay 需静音。写操作一定要在应用层给 UI 反馈：\"已保存草稿，稍后联网自动同步\"。","逐步构建可安装、可离线运行、网络恢复自动同步的 Progressive Web App：Service Worker 缓存策略（Cache First \u002F Stale-While-Revalidate）、IndexedDB 封装结构化本地数据、Background Sync 后台队列在用户离线操作后网络恢复时自动提交变更，附完整 TS 代码。",{"id":6,"username":185,"nickname":185,"avatar":20,"cover_image":20,"bio":20,"website":20,"github":20,"avatar_source":20,"resolved_avatar_url":20,"title":20,"created_at":186},"Choyeon","2026-09-30T18:09:53.330367Z",{"id":40,"name":44,"slug":45,"description":49,"icon":51,"color":52,"cover_image":20,"created_at":53,"post_count":87},[189,194,199,204],{"id":40,"name":190,"slug":191,"color":192,"icon":20,"is_active":141,"created_at":193,"post_count":87},"Vue","vue","#42b883","2026-09-30T18:09:53.672512Z",{"id":38,"name":195,"slug":196,"color":197,"icon":20,"is_active":141,"created_at":198,"post_count":87},"TypeScript","typescript","#3178c6","2026-09-30T18:09:53.680697Z",{"id":54,"name":200,"slug":201,"color":202,"icon":20,"is_active":141,"created_at":203,"post_count":87},"JavaScript","javascript","#f7df1e","2026-09-30T18:09:53.683194Z",{"id":205,"name":206,"slug":207,"color":208,"icon":20,"is_active":141,"created_at":209,"post_count":87},20,"Next.js","nextjs","#000000","2026-09-30T18:09:53.721362Z","published","public",173,"JavaScript,TypeScript,Vue,Next.js","2026-09-28T13:09:53.733462Z","2026-10-01T10:32:41.195068Z"]