关于 capacitor-offline-first
面向 Capacitor 应用开发者的离线优先开发技能,涵盖本地存储与同步设计。重点包括 SQLite 数据、事务性 outbox、冲突处理及平台相关问题。
主要功能
- 01基于 SQLite 的本地记录
- 02事务性同步 outbox
- 03冲突与删除策略
- 04Capacitor 平台问题排查
适用场景
- 01设计离线记录存储
- 02避免同步写入重复
- 03排查 iOS 数据丢失
- 04确保原生插件可用
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ cap-go/ capgo-skills --skill capacitor-offline-first安装后在 Claude Code 中输入/capacitor-offline-first确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
metadata.jsonreferences/fast-sql-setup.mdsync-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()),这样离线创建记录无需往返服务器 |
| 冲突策略 | 询问用户 / 产品负责人:按记录采用最后写入优先、按字段合并,或以服务器为准并由用户审核 |
| 删除 | 软删除(墓碑标记),直到服务器确认 |
工作流
- 检查:当前存储方式(
localStorage、@capacitor/preferences、其他 SQLite 插件)、API 形式(是否支持updated_since/ 版本字段 / 幂等键?)、认证刷新流程,以及现有的 service worker 注册。 - 报告:在进行大范围修改前,向用户说明计划(架构、发件箱、冲突策略)。对于大型应用,将每个实体列为一项 TODO。
- 安装:运行
npm install @capgo/capacitor-fast-sql @capacitor/network && npx cap sync,然后按照 references/fast-sql-setup.md 进行平台设置(必需:iOS ATS 本地网络、Android localhost 明文流量)。 - 架构 + 发件箱:references/sync-engine.md。
- 同步触发条件:应用启动、
networkStatusChange-> 已连接、Appresume、每次本地写入后(防抖),以及可选的后台任务。 - UI:显示待处理数量 / 上次同步时间;绝不因网络而阻塞写入。
- 验证(见下文)。
仅在涉及某个主题时加载对应参考资料。
最简结构
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时在前台同步才是可靠方式。
验证
- 设备开启飞行模式:创建、编辑、删除记录;强制关闭并重新启动应用;数据仍然存在。
- 重新开启网络:待发送队列清空至 0,服务器反映所有更改且没有重复项。
- 冲突测试:两台设备在离线状态下编辑同一条记录,然后都重新连接;结果符合所选策略。
- 网络不稳定:使用 Android 模拟器网络限速 / iOS Network Link Conditioner(设置 -> 开发者);重试间隔逐渐延长,且不会丢失数据。
- 原生检查:
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
- 检查数据库:在调试界面中显示
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 | ATS 阻止 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 发送;服务器进行去重 |
资源
- 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
评论
支持 Markdown