Electric 在线写入模式(Online Writes):用现有 API 结合读路径同步构建本地优先应用
Electric 在线写入模式Online Writes用现有 API 结合读路径同步构建本地优先应用【免费下载链接】electricThe agent platform built on sync.项目地址: https://gitcode.com/GitHub_Trending/el/electric导读本篇文章聚焦于 examples/write-patterns 示例中的第一种写入模式——Online writes在线写入。它是 Electric 官方 Write patterns写入模式指南中最简单的一种读路径Read-path交给 Electric 从 Postgres 同步数据到本地应用写路径Write-path则直接调用你现有的 REST API 写回 Postgres。读完本文你将理解该模式的架构、适用场景与固有局限掌握其完整实现含容错重试的 API 客户端、shape 代理后端与数据库迁移并知道如何在本仓库中一键运行起来验证效果。模式概述读写分离的最简单方案Online writes 模式的应用同时使用两种能力Electric 读路径同步read-path sync通过 Electric 将 Postgres 中的数据增量同步进本地应用应用端对数据变化实时响应在线写入online writes应用内产生的写操作通过普通 HTTP API 直接写回 Postgres。它是 Writes 指南 中介绍的所有写入模式里最简单的入门方案代码量最少、心智负担最低也是理解后续2-optimistic-state、3-shared-persistent、4-through-the-db三个更高级模式的起点。在本仓库中该模式的核心代码位于 examples/write-patterns/patterns/1-online-writes/index.tsx全部四个模式通过 examples/write-patterns/patterns/index.ts 导出作为一个 React 应用内的四个并列组件同时运行方便你横向对比它们在不同网络条件下在线/离线/弱网的行为差异。整个示例的架构说明见 examples/write-patterns/README.md。[!TIP] 其他同类示例 Phoenix LiveView 示例 也实现了同一种模式用 Electric 把数据流式同步进 LiveView 客户端写入则交由常规 Phoenix API 处理。如果你更熟悉 Elixir/Phoenix 技术栈可以直接参考它。工作流程一次 Todo 写入的完整链路以本示例中的 Todo 应用为例Online writes 模式的运行流程可以拆解为两条互不干扰的路径读路径Electric 同步应用挂载后useShape订阅 Postgres 中todos表的 shape任何由本应用或其他客户端产生的数据库变更都会经由 Electric 增量同步到本地并触发 UI 重渲染。写路径HTTP API用户在表单中输入标题并提交应用生成idUUID与created_at时间戳通过POST /todos发送到后端后端直接执行 SQL 插入 Postgres。写入成功后该变更又通过读路径的 Electric 同步回到当前应用形成一个完整闭环。下面是该模式在 examples/write-patterns/patterns/1-online-writes/index.tsx 中的实现要点type Todo { id: string title: string completed: boolean created_at: Date } export default function OnlineWrites() { // 用 Electric 的 useShape 钩子把 Postgres 数据同步进 React 状态 const { isLoading, data } useShapeTodo({ url: TODOS_URL, parser: { timestamptz: (value: string) new Date(value), }, }) const todos data ? data.sort((a, b) a.created_at - b.created_at) : [] // 处理用户输入事件通过后端 API 创建、更新、删除 todo async function createTodo(event: React.FormEvent) { event.preventDefault() const form event.target as HTMLFormElement const formData new FormData(form) const title formData.get(todo) as string const path /todos const data { id: uuidv4(), title: title, created_at: new Date(), } await api.request(path, POST, data) form.reset() } // ... }几个值得注意的实现细节useShape只做读组件数据完全来自 Electric 的 shape 订阅UI 上没有任何手写的setState来管理服务端数据客户端生成主键id由uuid库在本地生成uuidv4()避免与后端交互后再等待数据库分配主键这也是本地优先类应用的标准做法排序在本地完成由于created_at是timestamptz类型通过parser将其解析为Date再按时间戳排序展示写操作走 REST 语义新增用POST /todos勾选完成用PUT /todos/:id删除用DELETE /todos/:id与传统的服务端渲染应用完全一致。弹性客户端写失败后的指数退避重试Online writes 模式在线的前提是网络可达但真实网络总会抖动。本示例的共享 API 客户端 examples/write-patterns/shared/app/client.ts 内置了一套带退避算法的重试机制在写路径遇到网络错误时自动重试让体验不至于一断网就立刻崩溃const maxRetries 32 // 最多重试 32 次 const backoffMultiplier 1.1 // 退避增长系数 const initialDelayMs 1_000 // 初始延迟 1 秒 async function retryFetch(url, options, retryCount) { if (retryCount maxRetries) return const delay retryCount * backoffMultiplier * initialDelayMs return await new Promise((resolve) { setTimeout(async () { resolve(await resilientFetch(url, options, retryCount)) }, delay) }) } async function resilientFetch(url, options, retryCount) { try { return await fetch(url, options) } catch (_err) { return await retryFetch(url, options, retryCount 1) } }这段代码的关键设计意图源码注释中亦有说明只对网络错误重试fetch抛异常才进入重试分支而 HTTP 4xx/5xx 状态码不会触发重试。源码注释提示如需对 4xx/5xx 也做容错可以在返回前增加状态码检查延迟从 1 秒缓慢增长到 20 秒delay retryCount * 1.1 * 1000合计约可坚持 3 分钟适合弱网环境下短暂断连后自动恢复API 地址可配置客户端默认请求http://localhost:3001可通过环境变量VITE_SERVER_URL覆盖见 examples/write-patterns/shared/app/config.ts。注意尽管有重试兜底此模式依然没有乐观更新——在写请求成功返回之前UI 不会展示这条新数据。网络缓慢时你仍会看到请求挂起这就是网络在写路径上的直接体现。后端 API写入直连 Postgres读取代理到 Electric后端服务由 Express 实现源码位于 examples/write-patterns/shared/backend/api.js。它同时承担两个职责1. 写入接口直接执行 SQLPOST /todos、PUT /todos/:id、DELETE /todos/:id三个路由直接使用pg连接池向 Postgres 执行 SQL并且用 Zod 对请求体做了严格校验const idSchema z.string().uuid() const createSchema z.object({ id: z.string().uuid(), title: z.string(), created_at: z.string(), write_id: z.string().optional(), }) const updateSchema z.object({ completed: z.boolean(), write_id: z.string().optional(), })写入函数对应的 SQL节选自 api.js-- 创建 INSERT INTO todos (id, title, completed, created_at, write_id) VALUES ($1, $2, false, $3, $4) -- 更新 UPDATE todos SET completed $1, write_id $2 WHERE id $3 -- 删除 DELETE from todos where id $12. 读取接口透传代理到 Electric Shape 端点GET /todos并没有自己查数据库而是作为反向代理把请求转发给 Electric 的/v1/shape端点app.get(/todos, async (req, res) { const ELECTRIC_URL process.env.ELECTRIC_URL || http://localhost:3000 const electricUrl new URL(${ELECTRIC_URL}/v1/shape) // 只透传 Electric 协议参数 Object.keys(req.query).forEach((key) { if (ELECTRIC_PROTOCOL_QUERY_PARAMS.includes(key)) { electricUrl.searchParams.set(key, req.query[key]) } }) // 服务端固定表名 electricUrl.searchParams.set(table, todos) // ... })这里的关键点协议参数白名单通过electric-sql/client导出的ELECTRIC_PROTOCOL_QUERY_PARAMS过滤查询参数只透传 Electric 握手所需参数如offset、live、where等避免任意参数被滥用表名由服务端固定table参数在服务端设置客户端无法篡改要同步哪张表凭据注入如果配置了ELECTRIC_SOURCE_ID/ELECTRIC_SOURCE_SECRET环境变量会自动附加source_id与secret参数便于接入受保护的 shape 端点流式转发将 Electric 返回的 Web Stream 转换为 Node.js 流后pipeline到响应支持长连接实时推送并妥善处理了ERR_STREAM_PREMATURE_CLOSE客户端提前断开等边界情况。数据库结构对应的数据库迁移文件为 examples/write-patterns/shared/migrations/01-create-todos.sqlCREATE TABLE IF NOT EXISTS todos ( id UUID PRIMARY KEY, title TEXT NOT NULL, completed BOOLEAN NOT NULL, created_at TIMESTAMP WITH TIME ZONE NOT NULL );第二个迁移 02-add-write-id.sql 为表增加了可选字段write_id UUID。虽然本文的 Online writes 模式用不到它但后续更高级的模式如 shared persistent需要按每次写入的唯一 ID 而非行id去匹配 Electric 复制流从而在并发修改同一行时只清除自己那次写入的本地乐观状态不影响他人的变更。适用场景与收益Benefits为什么值得用实现极其简单不需要本地数据库、不需要冲突解决逻辑前后端代码量与普通 Web 应用无异直接复用现有 API如果你的服务端已经有一套成熟的 REST/GraphQL 接口含鉴权、校验、审计不需要为本地优先改造它们读体验快且可离线读读路径由 Electric 增量同步本地数据近乎实时刷新且在离线时仍可浏览已同步的数据。典型的适用用例根据 1-online-writes/README.md这类场景特别适合 Online writes实时仪表盘、数据分析与数据可视化以读为主偶尔操作如筛选、收藏对写入延迟不敏感云端生成 embeddings 的 AI 应用写入本身就需要在云端完成模型推理离线写入没有意义写入必须联网集成的系统例如在线支付交易本就需要与支付网关在线交互本地离线写入反而没有价值。Drawbacks必须接受的代价网络始终在写路径上写请求要跨越网络往返体验表现为慢、卡顿、加载转圈loading spinners弱网下尤为明显离线时无法写入交互型应用如果不做任何本地乐观处理断网时写操作将一直挂起重试直到超过maxRetries放弃UI 不会即时反馈若需要离线交互体验必须升级到下一个模式——乐观写入与本地乐观状态Optimistic state它用 React 内置的useOptimistic钩子让写入立即呈现在本地并在此之上继续演进为 共享持久化乐观状态 与 通过数据库同步。如何运行本示例是 pnpm monorepopackage.json的一部分运行步骤如下详见 examples/write-patterns/README.md#how-to-run1. 安装依赖并构建 monorepo 各包在仓库根目录执行pnpm install pnpm run -r build2. 启动后端容器Postgres Electric在 examples/write-patterns 目录下pnpm backend:up3. 启动开发服务器pnpm dev4. 结束后拆除后端容器pnpm backend:down运行后页面上会同时渲染四个写入模式组件。你可以在 DevTools 中切换网络为离线或慢速 3G直观对比Online writes 组件在离线时写入无法即时反映而相邻的 Optimistic state 组件会立即显示本地状态并在恢复联网后自动收敛。小结Online writes 是 Electric 写入模式体系中最基础、也最容易被低估的一环它用读走 Electric、写走 API的朴素拆分让团队无需引入任何新的写入基础设施即可享受同步带来的实时读取体验特别适合写操作天然依赖在线的业务支付、云端 AI、监控大盘。当你的应用开始追求离线可写、即时反馈时再沿着 Optimistic state → Shared persistent → Through the DB 的路径逐步升级即可——这也正是本仓库 write-patterns 示例希望你逐步体会的设计演进曲线。【免费下载链接】electricThe agent platform built on sync.项目地址: https://gitcode.com/GitHub_Trending/el/electric创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考