深入理解 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/remove Actions。

3. 账号同步与生命周期 (Notion 模式解析)

带账号系统的本地优先应用,其运行生命周期通常遵循以下模式:

  1. 阶段一:首次登录(必须联网)
    • 通过 HTTP 请求完成账号/密码或 OAuth 认证。
    • 获取 accessToken 和 refreshToken 并安全持久化(存入系统 Keychain 或 localStorage)。
    • 建立 PowerSync 连接,进行初次全量同步 (Initial Sync),将该用户的所有云端数据拉入本地 SQLite。
  2. 阶段二:日常使用(完全离线优先)
    • 冷启动极速渲染: 再次打开 App 时,即使没网,直接读取本地 SQLite 瞬间渲染出完整界面。
    • 离线写入: 用户新建任务,直接写入本地 SQLite,UI 瞬间更新。写操作被推入本地“同步队列 (Sync Queue)”。
    • 后台静默重连: 恢复网络后,SDK 自动在后台校验 Token(过期自动刷新),将积压的本地操作按顺序打包上传至 /powersync/crud,并下载云端的新变更。
  3. 阶段三:退出登录 (安全清理)
    • 销毁本地 Token。
    • 必须调用 powerSyncDb.disconnectAndClear() 硬核删除或截断本地 SQLite 数据库文件,防止下一个登录同一台设备的用户窃取数据。

4. 桌面端 (Electron) 的架构演进与最佳实践

在 Electron 等多进程环境中,PowerSync 的部署方式是架构设计的重中之重。

❌ 反面教材:双端独立双擎架构(现状)

  • 渲染进程 (Renderer): 跑一套 @powersync/web(存入 OPFS),UI 响应式极佳,但因为没权限发网络请求,需通过 IPC 委托主进程代发 CRUD 请求。
  • 主进程 (Main): 为了后台定时任务,又跑了一套 @powersync/node(存入本地文件系统),独立监听云端同步。
  • 痛点: 数据存两份、同步走两次、容易出现两端数据不一致,极度臃肿。

✅ 最佳实践:主进程单一事实来源 (Single Source of Truth)

彻底废弃渲染进程的数据库引擎,由主进程独揽大权:

  1. 数据层 (Main Process):
    • 仅在主进程运行 @powersync/node(使用高性能的 better-sqlite3),负责与云端的全盘同步。
    • 暴露简单的 IPC 接口供渲染进程执行 SQL 查询和写入。
    • 核心事件: 监听 PowerSync 的底层表变更。一旦有表(如 goals)数据改变,通过 IPC 向渲染进程广播一个无状态的简单事件:db:changed
  2. 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 不是简单地把数据库搬到前端,它代表了一种“以本地数据库为单一事实来源,通过响应式查询驱动视图,靠后台静默保障多端一致”的全新软件架构范式。

创建于 2026/3/6 更新于 2026/5/27