跳转到内容

38. Offline-First App 离线优先应用

V1 — 1个用户:Service Worker离线缓存

Section titled “V1 — 1个用户:Service Worker离线缓存”

你想做一个离线也能用的笔记页面,地铁上没网也能查看之前的内容。

  • Service Worker拦截网络请求
  • Cache API缓存静态资源
  • 离线时返回缓存内容
用HTML+JS+Service Worker实现一个离线可访问的笔记页面:
功能:
1. 主页面:显示笔记列表+编辑器
2. Service Worker:
- install事件:预缓存index.html, style.css, app.js
- fetch事件:网络优先(network-first)策略
- 网络失败 → 返回缓存版本
- 缓存成功响应供下次离线用
3. 笔记数据存localStorage
4. 离线指示器:页面顶部显示"在线"/"离线"状态
5. navigator.onLine监听网络状态变化
6. 新建/编辑/删除笔记(全离线操作)
文件结构:
- index.html(主页面)
- sw.js(Service Worker)
- app.js(应用逻辑)
- style.css(样式)
技术:HTML+CSS+JavaScript, Service Worker, Cache API
  • Service Worker注册成功
  • 首次访问后资源被缓存
  • 断网后页面仍可加载
  • 离线状态显示”离线”
  • 离线时可新建/编辑笔记
  • 恢复网络后状态更新为”在线”
  • Service Worker生命周期 → Module: PWA基础
  • 缓存策略(network-first)→ Module: 离线存储
  • 网络状态监听 → Module: 浏览器API

V2 — 10个用户:IndexedDB本地数据库

Section titled “V2 — 10个用户:IndexedDB本地数据库”

笔记越来越多,localStorage不够用了,需要完整的本地数据库。

  • IndexedDB存储结构化数据
  • 完整的CRUD操作
  • 联网时标记待同步数据
在V1基础上用IndexedDB替代localStorage:
IndexedDB:
1. 数据库:NotesDB
2. 对象仓库:notes
- id(自增主键)
- title, content, created_at, updated_at
- sync_status: "synced" / "pending" / "conflict"
3. 索引:按updated_at排序,按sync_status筛选
CRUD操作:
1. 创建笔记 → sync_status = "pending"
2. 编辑笔记 → 更新内容 + sync_status = "pending"
3. 删除笔记 → 标记deleted=true + sync_status = "pending"(软删除)
4. 查询:按更新时间倒序,支持标题搜索
同步标记:
1. 每条记录有sync_status字段
2. 所有本地操作标记为"pending"
3. 同步队列:列出所有pending记录
4. 界面显示待同步数量角标
5. 联网时显示"同步"按钮(V3再实现实际同步)
前端改进:
1. 笔记列表(标题+摘要+时间)
2. Markdown编辑器
3. 每条笔记旁显示同步状态图标
4. 离线/在线指示器
技术:HTML+CSS+JavaScript, IndexedDB, Service Worker
  • IndexedDB正确创建数据库和仓库
  • CRUD操作正常(全离线)
  • sync_status正确标记
  • 搜索功能正常
  • 软删除不显示但数据仍在
  • 待同步数量准确
  • IndexedDB API → Module: 浏览器存储
  • 对象仓库和索引设计 → Module: 数据建模
  • 同步状态标记 → Module: 离线优先架构
  • 软删除模式 → Module: 数据管理

V3 — 100个用户:冲突解决(Last-Write-Wins)

Section titled “V3 — 100个用户:冲突解决(Last-Write-Wins)”

多个用户需要同步笔记到服务器,但离线编辑可能产生冲突。

  • Go后端同步API
  • 时间戳+版本号管理
  • Last-Write-Wins冲突策略
在V2基础上实现服务端同步:
Go后端:
1. PostgreSQL表:
- notes(id, user_id, title, content, version, updated_at, deleted)
- version:每次更新+1
2. 同步API:
- POST /api/sync — 客户端上传变更,服务端返回服务端变更
- 请求体:{changes: [{id, title, content, version, updated_at, deleted}]}
- 响应体:{server_changes: [...], conflicts: [...]}
同步流程:
1. 客户端收集所有pending记录
2. 发送到服务端
3. 服务端对每条记录:
- 如果服务端无此记录 → 直接创建
- 如果客户端version == 服务端version → 接受更新,version+1
- 如果客户端version < 服务端version → 冲突
4. 冲突解决(Last-Write-Wins):
- 比较updated_at,较新的覆盖较旧的
- 被覆盖的版本记录到conflict_history
5. 返回服务端有但客户端没有的变更
客户端同步:
1. 联网时自动触发同步(也可手动)
2. 收到server_changes → 更新本地IndexedDB
3. 收到conflicts → 标记为已解决(LWW自动处理)
4. 同步完成 → 所有pending改为synced
技术:Go+Gin, PostgreSQL, IndexedDB, Service Worker
  • 客户端变更正确上传到服务端
  • 服务端变更正确同步到客户端
  • 版本号匹配时正常更新
  • 版本冲突时Last-Write-Wins生效
  • 冲突历史有记录
  • 同步后pending状态清除
  • 删除同步正确
  • 同步协议设计 → Module: 分布式数据
  • 版本号冲突检测 → Module: 乐观并发
  • Last-Write-Wins策略 → Module: 冲突解决
  • 离线同步架构 → Module: 离线优先

V4 — 1000个用户:增量同步与冲突UI

Section titled “V4 — 1000个用户:增量同步与冲突UI”

笔记量大了全量同步太慢,需要只同步变化的部分,冲突让用户自己决定。

  • 增量同步(只传变更)
  • 同步队列管理
  • 冲突UI让用户手动选择
在V3基础上实现增量同步和冲突UI:
增量同步:
1. 客户端记录last_sync_at时间戳
2. 同步时只发送last_sync_at之后变更的记录
3. 服务端也只返回该时间之后的变更
4. 减少传输数据量(从全量到增量)
同步队列:
1. 客户端维护操作队列(IndexedDB的sync_queue仓库)
2. 每次操作记录:{operation, record_id, timestamp, data}
3. 同步时按队列顺序提交
4. 队列处理完毕后清空
5. 同步失败时队列保留,下次重试
冲突UI:
1. 冲突检测:客户端version != 服务端version 且 都有修改
2. 不再自动LWW,弹出冲突解决对话框
3. 对话框显示:
- 左侧:本地版本(高亮变化部分)
- 右侧:服务端版本(高亮变化部分)
- 选项:保留本地/保留服务端/手动合并
4. 手动合并:编辑器中显示合并后内容,用户确认
5. 解决后标记resolved,继续同步
断点续传:
1. 大文件笔记分块同步
2. 同步中断后从断点继续
3. 进度指示器
技术:Go+Gin, PostgreSQL, IndexedDB, React
  • 增量同步只传输变更数据
  • last_sync_at正确更新
  • 同步队列按顺序处理
  • 冲突对话框正确显示双方内容
  • 保留本地/服务端选择生效
  • 手动合并功能正常
  • 同步失败后队列保留重试
  • 增量同步(delta sync)→ Module: 同步策略
  • 操作队列设计 → Module: 消息队列
  • 冲突解决UI设计 → Module: 用户体验
  • diff算法基础 → Module: 算法

V5 — 1万个用户:CRDT无冲突合并

Section titled “V5 — 1万个用户:CRDT无冲突合并”

用户在多设备编辑,冲突太频繁,需要自动合并不丢数据。

  • CRDT数据类型实现
  • 自动合并无冲突
  • 多设备同步
在V4基础上实现CRDT自动合并:
CRDT基础类型:
1. G-Counter(只增计数器):
- 每个节点维护自己的计数
- 合并:取每个节点的最大值
- 用途:点赞数、浏览数
2. LWW-Register(Last-Writer-Wins寄存器):
- 值+时间戳
- 合并:取时间戳更大的值
- 用途:笔记标题、标签
3. OR-Set(Observed-Remove集合):
- 添加时附带唯一标签
- 删除时删除特定标签
- 合并:求并集后去除已删除标签
- 用途:笔记中的标签集合
文本CRDT(简化版):
1. 每个字符有唯一ID(lamport时间戳+节点ID)
2. 插入:在两个字符ID之间插入新字符
3. 删除:标记字符为删除(墓碑)
4. 合并:按字符ID排序,自动无冲突
多设备同步:
1. 每个设备有唯一device_id
2. 操作日志(operation log)记录所有CRDT操作
3. 同步时交换操作日志
4. 本地应用远端操作 → 自动合并
5. 最终一致性保证
技术:Go+Gin, PostgreSQL, IndexedDB, CRDT库
  • G-Counter在多设备递增后合并正确
  • LWW-Register取最新值
  • OR-Set添加删除合并无冲突
  • 文本编辑多设备同时修改后合并正确
  • 操作日志同步完整
  • 无需用户手动解决冲突
  • 最终一致(所有设备收敛到相同状态)
  • CRDT原理与类型 → Module: 分布式数据结构
  • Lamport时间戳 → Module: 逻辑时钟
  • 最终一致性 → Module: CAP理论
  • 无冲突合并 → Module: 协作编辑

多人协作编辑文档,即使离线也能编辑,上线后自动合并所有人的修改。

  • 多人离线编辑+自动合并
  • P2P直接同步选项
  • 离线数据分析
在V5基础上实现协作离线编辑:
多人协作:
1. 文档共享:邀请其他用户协作编辑
2. 在线时:WebSocket实时同步(字符级)
3. 离线时:本地CRDT操作积累
4. 上线后:交换积累的操作,CRDT自动合并
5. 协作光标:显示其他在线用户的光标位置
P2P同步:
1. WebRTC数据通道直连
2. 局域网发现(mDNS)
3. P2P优先,服务器兜底
4. 适用于:同一办公室多人协作,网络不稳定但局域网可用
5. 同步协议:交换Merkle树比较差异
离线分析:
1. 本地统计:编辑字数、活跃时间、笔记数量
2. IndexedDB存统计数据
3. 联网时上报(可选)
4. 隐私优先:分析在本地完成,不强制上报
存储优化:
1. CRDT操作日志压缩(合并连续操作)
2. 定期快照(snapshot),清理旧操作日志
3. IndexedDB存储配额管理
4. 超过限额时提示用户清理或同步
技术:Go微服务, PostgreSQL, WebRTC, CRDT, Merkle树, IndexedDB
  • 多人在线实时协作正常
  • 离线编辑后上线自动合并
  • 合并结果所有用户一致
  • WebRTC P2P连接建立
  • P2P同步数据正确
  • 离线分析统计准确
  • 操作日志压缩减少存储
  • 快照+清理正常工作
  • 离线协作架构 → Module: 协作系统
  • WebRTC P2P通信 → Module: P2P网络
  • Merkle树数据比对 → Module: 数据结构
  • 存储配额管理 → Module: 客户端优化