SuperAPI AI 工具集

capacitor-offline-first

编程与开发

查看项目

更新于 10-14 17:54

技能介绍

离线优先 Capacitor 应用

何时使用

触发条件:

  • 应用必须在离线状态下读写,之后再同步
  • 需要为用户数据选择本地存储方案(Preferences、SQLite 还是 IndexedDB)
  • 同步过程中出现写入重复、丢失或被覆盖
  • 缓存在 iOS 低存储事件后消失
  • 添加 Service Worker 后原生插件失效(Plugin not implemented)

不要用于:

  • 替换 @capacitor-community/sqlite / Ionic secure storage:应使用 sqlite-to-fast-sql
  • 向设备下发新的 Web 代码(OTA):应使用 capgo-live-updates
  • 加密密钥存储、SSL 固定:应使用 capacitor-security
  • 查询速度 / 桥接开销调优:应使用 capacitor-performance

决策(写代码前先确定)

问题 | 默认答案 UI 从哪里读取? | 仅本地数据库。网络写入数据库;UI 观察数据库。 记录存储 | 通过 @capgo/capacitor-fast-sql 使用 SQLite 小型设置 / 令牌存储 | @capacitor/preferences(不用于机密;见 capacitor-security) localStorage / IndexedDB | 仅临时:iOS 在压力下可能清除 WebView 存储 应用外壳离线 | 已经是离线的:Capacitor 将 Web 资源打包进二进制(或 Capgo 实时更新包)。无需 Service Worker。 原生构建中的 Service Worker | 避免。Android 的 SW 会阻止 Capacitor 插件注入;iOS 的 SW 需要 WKAppBoundDomains,除非设置 ios.limitsNavigationsToAppBoundDomains,否则也会阻止注入。SW 仅保留给 PWA 构建。 ID | 客户端生成 UUID(crypto.randomUUID()),因此离线创建无需服务器往返 冲突策略 | 询问用户 / 产品负责人:按记录最后写入优先、按字段合并,还是服务器权威并让用户复核 删除 | 软删除(墓碑记录),直到服务器确认

工作流

  1. 检查:当前存储(localStorage、@capacitor/preferences、其他 SQLite 插件)、API 形态(是否支持 updated_since / version 字段 / 幂等键?)、认证刷新流程、现有 Service Worker 注册。
  2. 在侵入性变更前,向用户报告计划(schema、outbox、冲突策略)。对于大型应用,将每个实体列为 TODO。
  3. 安装:npm install @capgo/capacitor-fast-sql @capacitor/network && npx cap sync,然后按 references/fast-sql-setup.md 进行平台设置(必需:iOS ATS 本地网络、Android localhost 明文)。
  4. Schema + outbox:references/sync-engine.md。
  5. 同步触发:应用启动、networkStatusChange -> connected、App 恢复、每次本地写入后(防抖)、可选后台任务。
  6. UI:显示待处理数量 / 上次同步时间;绝不让写入被网络阻塞。
  7. 验证(见下文)。

仅当相关主题出现时才加载对应引用。

最小形态

import { FastSQL } from '@capgo/capacitor-fast-sql'; import { Network } from '@capacitor/network'; import { App } from '@capacitor/app';

const db = await FastSQL.connect({ database: 'app' }); await db.execute(CREATE TABLE IF NOT EXISTS outbox ( id TEXT PRIMARY KEY, entity TEXT NOT NULL, op TEXT NOT NULL, payload TEXT NOT NULL, created_at INTEGER NOT NULL, attempts INTEGER NOT NULL DEFAULT 0 ));

export async function saveTodo(todo: Todo) { await db.transaction(async (tx) => { await tx.run('INSERT OR REPLACE INTO todos (id, text, done, updated_at, dirty) VALUES (?, ?, ?, ?, 1)', [todo.id, todo.text, todo.done ? 1 : 0, Date.now()]); await tx.run('INSERT INTO outbox (id, entity, op, payload, created_at) VALUES (?, ?, ?, ?, ?)', [crypto.randomUUID(), 'todo', 'upsert', JSON.stringify(todo), Date.now()]); }); void sync(); // fire and forget; sync() is single-flight }

Network.addListener('networkStatusChange', (s) => { if (s.connected) void sync(); }); App.addListener('resume', () => void sync());

数据行及其 outbox 条目在同一事务中写入,因此崩溃不会只留下其中一个。

陷阱

  • Network.getStatus().connected === true 并不意味着 API 可达(强制门户、VPN、服务器宕机)。将 fetch 失败视为离线;connected 仅作为重试提示。
  • networkStatusChange 可能连续触发多次;让 sync() 单飞。
  • 每次变更都发送幂等键(outbox 行 id);否则超时后的重试会产生重复记录。
  • 按实体顺序处理 outbox;删除不能超越其创建。
  • 使用响应中的服务器时间戳 / 版本推进拉取游标,绝不用设备时钟。
  • 认证令牌离线会过期:在清空 outbox 前刷新,遇到 401 保留条目而不是丢弃。
  • 不要在 SQLite 行中存储大 blob;用 @capacitor/filesystem 保存文件,并在数据库中保留路径。
  • iOS 后台执行是机会性的。后台任务有帮助但不能保证同步时机;恢复时前台同步是可靠路径。

验证

  • 设备开启飞行模式:创建、编辑、删除记录;杀死并重启应用;数据仍然存在。
  • 重新打开网络:outbox 排空到 0,服务器一次性反映所有变更(无重复)。
  • 冲突测试:两台设备离线编辑同一记录,再同时连接;结果符合所选策略。
  • 弱网:Android 模拟器网络限速 / iOS Network Link Conditioner(设置 -> 开发者);重试退避,无数据丢失。
  • 原生检查: grep -rn "serviceWorker.register" src/ | grep -v "isNativePlatform" # 应为空或受 Web 守卫 /usr/libexec/PlistBuddy -c "Print :NSAppTransportSecurity:NSAllowsLocalNetworking" ios/App/App/Info.plist grep -n "networkSecurityConfig|usesCleartextTraffic" android/app/src/main/AndroidManifest.xml
  • 检查数据库:在调试屏暴露 SELECT COUNT(*) FROM outbox,或拉取文件(adb exec-out run-as com.example.app ls databases/)。

错误处理

错误 / 症状 | 原因 | 修复 "CapgoCapacitorFastSql" plugin is not implemented on android / Plugin not implemented | Service Worker 或 WKAppBoundDomains 阻止注入,或缺少 npx cap sync | 从原生构建中移除 SW / 设置 ios.limitsNavigationsToAppBoundDomains;运行 npx cap sync iOS:The resource could not be loaded because the App Transport Security policy requires the use of a secure connection | Fast SQL localhost 传输被 ATS 阻止 | 添加 NSAllowsLocalNetworking Android:CLEARTEXT communication to localhost not permitted by network security policy | 到 localhost 的明文被阻止 | 添加 network_security_config.xml localhost 例外 SQLITE_CONSTRAINT: UNIQUE constraint failed during pull | 服务器行 id 本地已存在 | 使用 INSERT ... ON CONFLICT(id) DO UPDATE database is locked | 并行写入 / 长事务 | 串行化写入;保持事务短小;单飞同步 iOS 存储压力后数据丢失 | 存储在 localStorage / IndexedDB | 迁移到 SQLite 重连后服务器记录重复 | 重试 POST 未带幂等键 | 将 outbox id 作为 Idempotency-Key 发送;服务器去重

资源

  • Fast SQL 插件:https://capgo.app/docs/plugins/fast-sql/
  • Capacitor 存储指南:https://capacitorjs.com/docs/guides/storage
  • Network API:https://capacitorjs.com/docs/apis/network
  • 后台任务插件:https://capgo.app/docs/plugins/background-task/
  • iOS 故障排查(WKAppBoundDomains):https://capacitorjs.com/docs/ios/troubleshooting