Electric + PGlite:用可嵌入的 WASM Postgres 构建带同步能力的 Local-First 应用
2026/9/16 13:04:30 网站建设 项目流程

Electric + PGlite:用可嵌入的 WASM Postgres 构建带同步能力的 Local-First 应用

【免费下载链接】electricThe agent platform built on sync.项目地址: https://gitcode.com/GitHub_Trending/el/electric

PGlite 是 Electric 生态中"嵌入式 Postgres"这一原语的实现:它是一个编译为 WASM 的轻量级 Postgres,打包成 TypeScript 库后可以直接在浏览器、Node.js、Bun 和 Deno 中运行一个完整的数据库,无需安装任何额外依赖(gzip 后小于 3MB)。配合 Electric 的 Postgres Sync,你还能把云端 Postgres 中的数据以 Shape 流的方式同步进这个本地数据库,并通过 live query 获得内置的响应式能力。读完本文,你将掌握 PGlite 的创建与持久化配置、扩展加载方式,以及如何把 Postgres Sync 的数据订阅进 PGlite 表并构建"写穿透数据库"的完整 Local-First 应用——仓库内的 linearlite 示例 就是这套方案的完整落地。

什么是 PGlite:不是虚拟机,而是 WASM 编译的 Postgres

PGlite 是一个轻量级的 WASM Postgres 构建,打包成 TypeScript 库,支持浏览器、Node.js、Bun 和 Deno 四个运行环境。它最大的特点是没有使用 Linux 虚拟机——与早期"浏览器里的 Postgres"项目(通常靠 QEMU 之类方案跑一个完整 Linux 用户态)不同,PGlite 是将 Postgres 直接以单用户模式编译进 WASM 的,因此体积小、启动快、依赖为零。

最基本的用法就是创建实例并执行 SQL:

import { PGlite } from '@electric-sql/pglite' const db = new PGlite() await db.query("select 'Hello world' as message;") // -> { rows: [ { message: "Hello world" } ] }

持久化与存储后端

PGlite 可以按不同场景选择存储后端:

  • 内存数据库:不传持久化配置时作为临时(ephemeral)的 in-memory 数据库使用,适合测试与沙箱场景;
  • 文件系统(Node/Bun):将dataDir指向磁盘目录即可持久化;
  • IndexedDB(浏览器):使用idb://前缀的 URI,数据落在浏览器的 IndexedDB 中,刷新页面不丢失。

仓库中 linearlite 示例的 worker 展示了浏览器场景的典型创建方式,并额外传入了relaxedDurability: true放宽持久化约束以提升大批量写入吞吐:

import { worker } from '@electric-sql/pglite/worker' import { PGlite } from '@electric-sql/pglite' import { migrate } from './migrations' worker({ async init() { const pg = await PGlite.create({ dataDir: `idb://linearlite2`, relaxedDurability: true, }) // Migrate the database to the latest schema await migrate(pg) return pg }, })

注意这里 PGlite 运行在 Web Worker 中(通过worker入口注册),避免数据库操作阻塞主线程 UI——这是大数据集场景的推荐做法。

可扩展:动态加载 Postgres 扩展

PGlite 支持动态加载 Postgres 扩展(extension loading),包括 pgvector 等常用扩展。扩展通过PGlite.createextensions配置项注入,下文同步示例中的liveelectricSync()就是通过该机制挂载的。

同步进 PGlite:把云端 Postgres 变成本地实时数据库

PGlite 最核心的杀手级特性是reactive:内置 sync 与 live query 两个原语。其中 sync 原语由@electric-sql/pglite-sync提供,它让 PGlite 能直接订阅 Electric 的 Postgres Sync 服务——即连接你的云端 Postgres、消费逻辑复制流、并将数据扇出为 Shape 的那个 Elixir 读路径同步引擎。

官方文档页面给出的完整示例(源自 website/src/partials/sync-into-pglite.tsx)把云端的一个itemsShape 同步进本地items表:

import { PGlite } from '@electric-sql/pglite' import { live } from '@electric-sql/pglite/live' import { electricSync } from '@electric-sql/pglite-sync' import { useLiveQuery } from '@electric-sql/pglite-react' // Create a persistent local PGlite database const pg = await PGlite.create({ dataDir: `idb://my-database`, extensions: { electric: electricSync(), live, }, }) // Setup the local database schema await pg.exec(` CREATE TABLE IF NOT EXISTS items ( id SERIAL PRIMARY KEY, ); `) // Establish a persistent shape subscription await pg.electric.syncShapeToTable({ shape: { url: `${BASE_URL}/v1/shape` }, table: `items`, primaryKey: [`id`], }) // Bind data to your components using live queries // against the local embedded database const Component = () => { const items = useLiveQuery(`SELECT * FROM items;`) return <pre>{JSON.stringify(items)}</pre> }

逐段拆解这个例子的关键配置:

  1. PGlite.create({ dataDir, extensions })dataDir: 'idb://my-database'指定浏览器 IndexedDB 持久化;extensions中挂载两个扩展——electricSync()提供pg.electric.syncShapeToTable等 API,live提供pg.live.query实时查询能力。
  2. 本地 schema 先行:同步前先用pg.exec建好目标表。本地表结构决定同步数据的落点。
  3. syncShapeToTable:建立一条持久的 Shape 订阅。参数中shape.url指向 Electric 服务的/v1/shapeHTTP 端点;table指定数据写入的本地表;primaryKey声明主键列,用于正确应用 upsert/删除。
  4. useLiveQuery:来自@electric-sql/pglite-react,把 SQL 查询绑定为 React 响应式数据源——底层 PGlite 数据一变,组件自动重渲染,无需任何手动订阅逻辑。

生产级订阅配置:linearlite 中的完整参数

上面的极简示例覆盖了 API 骨架,而 linearlite 示例 的 src/sync.ts 则展示了处理 10 万条数据、约 150MB 数据集时用到的全部生产参数。以issue表的订阅为例(原文第 65–83 行):

const issuesSync = await pg.sync.syncShapeToTable({ shape: { url: issueUrl.toString(), // ${ELECTRIC_URL}/v1/shape,可携带 source_id 等参数 params: { table: `issue`, source_id: ELECTRIC_SOURCE_ID, }, }, table: `issue`, primaryKey: [`id`], shapeKey: `issues`, // 订阅标识,用于断线恢复时续订同一 Shape commitGranularity: `up-to-date`, // 初始同步提交粒度:追平到最新即提交 useCopy: true, // 使用 COPY 协议批量灌入,显著加快初始同步 onInitialSync: async () => { issueShapeInitialSyncDone = true await pg.exec(`ALTER TABLE issue ENABLE TRIGGER ALL`) doPostInitialSync() }, })

相比最小示例多了几个实战要点:

  • params:将source_idsecret等查询参数附加到 Shape 请求上,用于区分数据源与鉴权;
  • shapeKey:为订阅命名,页面刷新后能以同一键恢复进度而不是从头重拉;
  • commitGranularity: 'up-to-date':控制初始同步时的提交时机;
  • useCopy: true:初始同步走 COPY 批量写入路径,是大数据集下快初始同步的关键;
  • onInitialSync回调:初始同步完成后的钩子,linearlite 在这里启用先前被禁用的触发器并创建索引(见下文"性能考量");
  • subscribe:返回的 sync 对象支持subscribe(onProgress, onError)回调,示例用它来更新"Inserting issues..."这类同步进度 UI,并通过 localStorage + StorageEvent 在多标签页间广播同步状态。

订阅建立后,pg.electric扩展会把 Shape 流持续应用到本地表;同时pg.live.query(live 扩展)可以持续监视本地表的任何变化——linearlite 正是用它驱动反向的写入同步(下一节)。

写入路径:通过本地数据库"写穿透"服务器

PGlite 只负责读路径同步,写入可以走你现有的后端。仓库中的 linearlite 演示了四种写入模式中的write through the database模式(另见 Writes 指南):本地变更先写入本地 PGlite,再由客户端把未同步的行发送到写入服务器,服务端落库后,变更又经由 Electric 同步流回到本地表,完成闭环。

local-first 架构下的两种常见合并策略(摘自 examples/linearlite/README.md):

  1. Merge on write(写时合并):本地只有一张数据表,本地变更直接应用到该表,服务端同步到达时把待合并变更与新数据合并。读路径性能好,适合大数据集——linearlite 采用的是这一种;
  2. Merge on read(读时合并):每张服务端表对应两张本地表——一张纯服务端镜像(从不做本地变更),一张 "delta" 表存本地变更;读取时按 id join,也可用视图 +instead of触发器封装。

linearlite 的本地表在业务列之外额外维护一组写入路径状态列(定义在 db/migrations-client/01-create_tables.sql 的迁移中):

作用
deleted软删除标记,行已被本地删除但未同步
new标记该行为本地新插入
modified_columns被本地修改过的列名数组
sent_to_server变更是否已发给写入服务器
synced行是否已与服务端完全一致(纯镜像)
backupJSONB 备份列,存被修改列的旧值;可用revert_local_changes(table_name, row_id)函数回退到服务端状态

这些列由一组触发器维护,而行为分叉的开关是一个由 PGlite 同步插件设置的配置变量:同步进行期间electric.syncing = true,否则为false。据此触发器执行不同的合并语义:

  • 同步期间(electric.syncing = true
    • Insert:行已存在则转按 Update 处理;清空modified_columns,置new = falsesent_to_server = falsesynced = true(纯镜像);
    • Update:若行是纯镜像或服务端变更更新(sent_to_server = trueNEW.modified >= OLD.modified),则全量应用并清空备份与标志位;若行存在本地变更,则只更新不在modified_columns中的列,把被覆盖的旧值存入backup——这就是无锁的列级合并;
    • Delete:同步期间执行真实删除(软删除只用于本地写路径)。
  • 本地写入期间(electric.syncing = false
    • Insert:把所有业务列加入modified_columns,置new = truesent_to_server = false
    • Update:对每个新增的变更列追加进modified_columns并把原值存入backup,置sent_to_server = false
    • Delete:本地新行(new = true)直接真删;已同步行则置deleted = true软删,保留行等待上行同步。

上行同步:用 live query 监视未同步行

反向链路实现在 src/sync.ts 的startWritePath中:

  1. 用 live query 持续统计issuecomment两张表中synced = false的行数;
  2. 一旦有未同步行,用Mutex串行化后执行doSyncToServer
  3. 在一个事务里SELECT ... WHERE synced = false AND sent_to_server = false取出全部变更,组装成ChangeSetPOST到写入服务器的/apply-changes端点;
  4. 服务端成功后,在另一个事务中把对应行标记为sent_to_server = true——更新条件带modified = $2校验,防止标记窗口内行又被改动;该事务里还执行SET LOCAL electric.bypass_triggers = true跳过触发器开销;
  5. 服务端落库后,Electric 同步流把变更回放到本地表,触发器在到达时把行标记为synced = true,闭环完成。

写入服务器本身很简单:server.ts 用 Hono 实现,把操作应用到 Postgres 后返回 200;示例同时提供 Supabase Edge Function 版本(见 supabase/functions)。README 也指出它可在此基础上扩展鉴权与权限校验。

大数据集下的性能考量

linearlite 的 README 总结了三个为"快初始同步 + 即时本地响应"服务的关键优化,对任何大表同步都有参考价值:

  • 初始迁移中的合并触发器在初始同步完成前保持禁用ALTER TABLE ... ENABLE TRIGGER ALLonInitialSync回调中执行),避免灌入 10 万行期间触发器空转;
  • 索引创建同样推迟到初始同步完成后postInitialSync),不让索引构建拖慢数据灌入;
  • 全文检索索引(tsvector/tsquery)推迟到用户首次打开搜索功能时才构建,把"到达可用状态"的时间压到最短。

运行 linearlite 示例

该示例是 Electric monorepo 的一部分,作为 pnpm workspace 成员构建运行。完整命令序列(摘自 examples/linearlite/README.md):

# 1. 在 monorepo 根目录安装并构建全部工作区包 cd (monorepo 根目录) pnpm install pnpm run -r build # 2. 启动后端服务(Postgres + Electric 同步服务,Docker Compose) cd examples/linearlite pnpm backend:up # 3. 启动写入路径服务器 pnpm run write-server # 4. 启动前端 dev server pnpm dev # 5. 结束后清理后端 pnpm backend:down

其中backend:up会先停止并删除其他示例挂载的卷,确保每次从干净的数据库开始;pnpm backend:up同时执行服务端迁移与数据装载(100,000 条 issue 加评论,约 150MB)。示例依赖的关键包版本见 examples/linearlite/package.json:@electric-sql/pglite@electric-sql/pglite-react@electric-sql/pglite-repl均为^0.2.17@electric-sql/pglite-sync^0.2.20

适用边界与延伸阅读

  • PGlite 当前在 Electric 官网产品页上标记为beta,API 与参数(如syncShapeToTable的选项集合)可能随版本演进,具体以你所锁定版本的行为为准;
  • 运行 Postgres Sync 需要一个可达的 Electric 服务实例(DATABASE_URL指向 Postgres、/v1/shape端点对客户端开放),linearlite 示例通过 backend/docker-compose.yml 一键拉起;
  • Shape 定义语法(wherecolumns投影、渐进加载)见 Shapes 指南,同步服务的部署与组件关系见 Postgres Sync 页面;
  • 更完整的 API 参考、示例与 in-browser REPL 以 PGlite 官方站点(pglite.dev)为准;本仓库内可参考的实现入口是 linearlite 示例 与 website/src/partials/sync-into-pglite.tsx。

总结:PGlite 把"一个真正的 Postgres"装进了浏览器和 JS 运行时,而@electric-sql/pglite-sync让它在 Electric 的读路径同步架构中成为一等公民——本地表既是实时缓存,又是写入缓冲,electric.syncing触发器机制则把双向合并的复杂性收敛到了 SQL 层。这套"读同步进 PGlite + live query 驱动写上行"的组合,正是构建离线可用、响应式、可承受大数据集的 Local-First 应用的完整配方。

【免费下载链接】electricThe agent platform built on sync.项目地址: https://gitcode.com/GitHub_Trending/el/electric

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询