mcpField
C
capacitor-offline-first

capacitor-offline-first

github.com/cap-go开发731.1k 次安装2 周前更新
代码仓库

关于 capacitor-offline-first

面向 Capacitor 应用开发者的离线优先开发技能,涵盖本地存储与同步设计。重点包括 SQLite 数据、事务性 outbox、冲突处理及平台相关问题。

主要功能

  1. 01基于 SQLite 的本地记录
  2. 02事务性同步 outbox
  3. 03冲突与删除策略
  4. 04Capacitor 平台问题排查

适用场景

  1. 01设计离线记录存储
  2. 02避免同步写入重复
  3. 03排查 iOS 数据丢失
  4. 04确保原生插件可用

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/cap-go/capgo-skills --skill capacitor-offline-first

安装后在 Claude Code 中输入/capacitor-offline-first确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

metadata.json
references/
fast-sql-setup.md
sync-engine.md

详细介绍

离线优先的 Capacitor 应用

适用场景

满足以下情况时使用:

  • 应用必须在离线时读取和写入数据,并在之后同步
  • 为用户数据选择本地存储(Preferences、SQLite 还是 IndexedDB)
  • 同步期间写入被重复、丢失或覆盖
  • iOS 在存储空间不足后缓存数据消失
  • 添加了 service worker,导致原生插件停止工作(Plugin not implemented)

以下情况不要使用:

  • 替换 @capacitor-community/sqlite / Ionic 安全存储: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。仅在 PWA 构建中保留 SW。
ID由客户端生成 UUID(crypto.randomUUID()),这样离线创建记录无需往返服务器
冲突策略询问用户 / 产品负责人:按记录采用最后写入优先、按字段合并,或以服务器为准并由用户审核
删除软删除(墓碑标记),直到服务器确认

工作流

  1. 检查:当前存储方式(localStorage、@capacitor/preferences、其他 SQLite 插件)、API 形式(是否支持 updated_since / 版本字段 / 幂等键?)、认证刷新流程,以及现有的 service worker 注册。
  2. 报告:在进行大范围修改前,向用户说明计划(架构、发件箱、冲突策略)。对于大型应用,将每个实体列为一项 TODO。
  3. 安装:运行 npm install @capgo/capacitor-fast-sql @capacitor/network && npx cap sync,然后按照 references/fast-sql-setup.md 进行平台设置(必需:iOS ATS 本地网络、Android localhost 明文流量)。
  4. 架构 + 发件箱:references/sync-engine.md。
  5. 同步触发条件:应用启动、networkStatusChange -> 已连接、App resume、每次本地写入后(防抖),以及可选的后台任务。
  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());

数据行和对应的发件箱条目在同一事务中写入,因此即使发生崩溃,也不会只留下其中一项。

常见陷阱

  • Network.getStatus().connected === true 不代表 API 可访问(强制门户、VPN、服务器宕机等)。将 fetch 失败视为离线;仅将 connected 作为重试提示。
  • networkStatusChange 可能连续触发多次;确保 sync() 同一时间仅运行一次。
  • 每次变更都发送一个幂等键(发件箱行 ID);否则超时后的重试会导致记录重复。
  • 按实体顺序处理发件箱;删除操作不能先于创建操作执行。
  • 使用响应中的服务器时间戳 / 版本推进拉取游标,绝不要使用设备时钟。
  • 离线期间认证令牌可能过期:清空发件箱前先刷新令牌,遇到 401 时保留条目,不要丢弃。
  • 不要在 SQLite 行中存储大型二进制数据;使用 @capacitor/filesystem 保存文件,并在数据库中保留路径。
  • iOS 后台执行具有不确定性。后台任务有帮助,但无法保证同步时机;resume 时在前台同步才是可靠方式。

验证

  1. 设备开启飞行模式:创建、编辑、删除记录;强制关闭并重新启动应用;数据仍然存在。
  2. 重新开启网络:待发送队列清空至 0,服务器反映所有更改且没有重复项。
  3. 冲突测试:两台设备在离线状态下编辑同一条记录,然后都重新连接;结果符合所选策略。
  4. 网络不稳定:使用 Android 模拟器网络限速 / iOS Network Link Conditioner(设置 -> 开发者);重试间隔逐渐延长,且不会丢失数据。
  5. 原生检查:
grep -rn "serviceWorker.register" src/ | grep -v "isNativePlatform"   # should be empty or web-guarded
/usr/libexec/PlistBuddy -c "Print :NSAppTransportSecurity:NSAllowsLocalNetworking" ios/App/App/Info.plist
grep -n "networkSecurityConfig\|usesCleartextTraffic" android/app/src/main/AndroidManifest.xml
  1. 检查数据库:在调试界面中显示 SELECT COUNT(*) FROM outbox,或拉取文件(adb exec-out run-as com.example.app ls databases/)。

错误处理

错误 / 现象原因修复方法
"CapgoCapacitorFastSql" plugin is not implemented on android / Plugin not implementedService 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 connectionATS 阻止 Fast SQL 的 localhost 传输添加 NSAllowsLocalNetworking
Android:CLEARTEXT communication to localhost not permitted by network security policy网络安全策略阻止向 localhost 发送明文通信添加 network_security_config.xml 中的 localhost 例外
拉取时出现 SQLITE_CONSTRAINT: UNIQUE constraint failed服务器行 ID 已存在于本地使用 INSERT ... ON CONFLICT(id) DO UPDATE
database is locked并行写入 / 事务耗时过长串行执行写入;缩短事务;同步采用 single-flight 模式
iOS 存储空间不足后数据丢失数据存储在 localStorage / IndexedDB 中迁移到 SQLite
重新连接后服务器记录重复重试 POST 时未使用幂等键将待发送队列 ID 作为 Idempotency-Key 发送;服务器进行去重

资源

评论

支持 Markdown