深入理解 PowerSync 与本地优先 (Local-First) 架构
PowerSync 是一种用于构建“本地优先”应用的同步引擎。它的核心思想是:**在客户端本地维护一个 SQLite 数据库副本,所有的读写操作都在本地瞬间完成,引擎在后台静默处理与云端 Postgres 的双向同步。**
#status / growing
#type / concept
#resource / power-sync
[!info] related notes PowerSync
深入理解 PowerSync 与本地优先 (Local-First) 架构
1. 为什么选择 PowerSync?(适用场景与边界)
PowerSync 是一种用于构建“本地优先”应用的同步引擎。它的核心思想是:在客户端本地维护一个 SQLite 数据库副本,所有的读写操作都在本地瞬间完成,引擎在后台静默处理与云端 Postgres 的双向同步。
🎯 绝佳适用场景 (The Sweet Spot)
非常契合类似 Notion、Obsidian、Linear、个人任务管理 (TODO) 等生产力工具:
- 零延迟体验 (Zero-latency UI): 本地 SQLite 查询耗时极短(约 1ms),UI 瞬间反馈,没有网络 Loading 圈。
- 绝对离线可用 (Offline-first): 在地铁、飞机上断网时依然可以新建任务、编辑笔记。
- 单用户 / 小规模协作: 用户自己的数据量通常在几十 MB 以内,完全可以全量拉取到本地设备。极少发生严重的并发写入冲突。
🚫 不适用场景 (局限性)
- 海量全局数据聚合: 如淘宝商品库、推特全站热榜(客户端存不下)。
- 强一致性交易系统: 如银行转账、秒杀扣库存(必须经过服务器权威校验,不能用本地乐观写入)。
- 高频实时排序/大规模实时竞价: 如大型网游排行榜(同步开销过大)。
2. 核心范式转移:Store 还要存实体数据吗?
在传统的 Client-Server 架构中,前端通常使用 Axios 请求数据,然后存入 Pinia/Redux 的 Store 中(如 state.goals = […]),组件从 Store 读取数据渲染。
引入 PowerSync 后,这种模式必须抛弃。
- 单一事实来源 (Single Source of Truth): 本地的 SQLite 就是唯一的权威数据源。如果 Store 再存一份数组,就会产生“缓存的缓存”,导致状态同步困难和极大的代码复杂度。
- 天然的响应式查询 (Reactive Queries): 使用
@powersync/vue提供的useQuery,SQL 查询结果自动就是一个 Vue Ref。当本地执行 INSERT,或者后台从云端拉取了新数据,底层 SQLite 变化会自动触发 Vue 组件的重渲染。 - Store 的正确归宿: Store 不应再负责持有业务数据,而是专注于管理纯粹的视图/UI 状态。
- ✅ 保留在 Store 的: 侧边栏折叠状态、当前选中的过滤标签(Filter)、分页页码、弹窗草稿。
- ❌ 从 Store 剔除的:
goals: []等实体数组,以及相关的fetch/add/update/removeActions。
3. 账号同步与生命周期 (Notion 模式解析)
带账号系统的本地优先应用,其运行生命周期通常遵循以下模式:
- 阶段一:首次登录(必须联网)
- 通过 HTTP 请求完成账号/密码或 OAuth 认证。
- 获取
accessToken和refreshToken并安全持久化(存入系统 Keychain 或localStorage)。 - 建立 PowerSync 连接,进行初次全量同步 (Initial Sync),将该用户的所有云端数据拉入本地 SQLite。
- 阶段二:日常使用(完全离线优先)
- 冷启动极速渲染: 再次打开 App 时,即使没网,直接读取本地 SQLite 瞬间渲染出完整界面。
- 离线写入: 用户新建任务,直接写入本地 SQLite,UI 瞬间更新。写操作被推入本地“同步队列 (Sync Queue)”。
- 后台静默重连: 恢复网络后,SDK 自动在后台校验 Token(过期自动刷新),将积压的本地操作按顺序打包上传至
/powersync/crud,并下载云端的新变更。
- 阶段三:退出登录 (安全清理)
- 销毁本地 Token。
- 必须调用
powerSyncDb.disconnectAndClear()硬核删除或截断本地 SQLite 数据库文件,防止下一个登录同一台设备的用户窃取数据。
4. 桌面端 (Electron) 的架构演进与最佳实践
在 Electron 等多进程环境中,PowerSync 的部署方式是架构设计的重中之重。
❌ 反面教材:双端独立双擎架构(现状)
- 渲染进程 (Renderer): 跑一套
@powersync/web(存入 OPFS),UI 响应式极佳,但因为没权限发网络请求,需通过 IPC 委托主进程代发 CRUD 请求。 - 主进程 (Main): 为了后台定时任务,又跑了一套
@powersync/node(存入本地文件系统),独立监听云端同步。 - 痛点: 数据存两份、同步走两次、容易出现两端数据不一致,极度臃肿。
✅ 最佳实践:主进程单一事实来源 (Single Source of Truth)
彻底废弃渲染进程的数据库引擎,由主进程独揽大权:
- 数据层 (Main Process):
- 仅在主进程运行
@powersync/node(使用高性能的better-sqlite3),负责与云端的全盘同步。 - 暴露简单的 IPC 接口供渲染进程执行 SQL 查询和写入。
- 核心事件: 监听 PowerSync 的底层表变更。一旦有表(如
goals)数据改变,通过 IPC 向渲染进程广播一个无状态的简单事件:db:changed。
- 仅在主进程运行
- UI 层 (Renderer Process):
- 剥离所有本地数据库相关的 NPM 包。
- 封装一个
useIpcQuery(sql)钩子:组件挂载时通过 IPC 拉取一次初始数据。 - 失效与重新拉取 (Invalidate and Refetch): 监听主进程的
db:changed事件。收到事件后,不关心具体改了什么,直接重新通过 IPC 发送 SQL 拉取最新结果,覆盖 Vue Ref,依靠 Vue 的 Virtual DOM 高效更新 UI。
5. 前端代码的演进路线
从传统的 HTTP 请求模式迁移到 PowerSync,通常有两条路线:
- 路线 A(平滑过渡期):充当适配层 实现一个
PowerSyncAdapter去继承原有的IApiClient接口。组件里依然调用service.getGoals(),底层拦截为执行本地 SQL。 缺点:失去了原生的响应式能力,数据更新依然需要手动触发 fetch。 - 路线 B(终极形态):拥抱响应式 SQL 废弃旧的 HTTP Client Service。在 Vue Composables(如
useGoal.ts)中直接引入useQuery,让前端组件成为连接 SQL 查询和 UI 渲染的直通管道。
总结:PowerSync 不是简单地把数据库搬到前端,它代表了一种“以本地数据库为单一事实来源,通过响应式查询驱动视图,靠后台静默保障多端一致”的全新软件架构范式。