ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Electric Todo App:基于 useShape 与 Shape API 代理的经典 TodoMVC 示例解析

Electric Todo App:基于 useShape 与 Shape API 代理的经典 TodoMVC 示例解析 Electric Todo App基于 useShape 与 Shape API 代理的经典 TodoMVC 示例解析【免费下载链接】electricThe agent platform built on sync.项目地址: https://gitcode.com/GitHub_Trending/el/electric官方演示文档 website/sync/demos/todo-app.md 介绍了 Electric 仓库中的一个经典TodoMVC 示例应用前端用 React 的useShapeHook 实时订阅 Postgres 表写操作走 Express 后端直连数据库读路径则通过后端代理到 Electric 的 Shape API。读完本篇你可以完整掌握“Electric 同步读 REST 直写”这一同步架构的落地方式并在本地按仓库步骤跑起这个示例。示例定位与整体架构这是 ElectricSQL monorepo 中的一个示例工程位于 examples/todo-app其package.json的描述为Somewhat opinionated starter for ElectricSQL with Vite, and React Router。官方文档页还标注了演示站点与源码位置见 website/sync/demos/todo-app.md 的 frontmatter。从源码结构看整条数据链路如下React 前端 (useShape) │ GET /todos ──────────► Express 后端 (server.js, :3010) │ │ 仅转发 ELECTRIC_PROTOCOL_QUERY_PARAMS │ ◄── SSE 形状流 ◄── 代理 ◄──┘ │ POST/PUT/DELETE /todos │ Electric Shape API (:3000) └──► Postgres (todos 表)读路径前端不直接访问 Electric而是请求后端的/todos后端把请求代理到 Electric 的/v1/shape端点并把表名todos固定在服务端写路径POST/PUT/DELETE 请求由 Express 直接执行 SQL变更由 Electric 的复制机制捕获后推送到所有形状流。数据模型todos 表表结构定义在迁移文件 001-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 );四个字段与前端类型一一对应主键id使用 UUID由前端生成completed与created_at均为NOT NULL保证形状流中的行数据完整。迁移通过db:migrate脚本应用见后文运行部分部署场景下则由 sst.config.ts 调用createDatabaseForCloudElectric并指定migrationsDirectory: ./db/migrations执行。前端核心useShape 订阅表数据官方文档指出的“主要 Electric 代码”位于 src/routes/index.tsx。完整的同步入口只有几行index.tsx#L24-L28export default function Index() { const { data: todos } useShapeToDo({ url: new URL(${import.meta.env.VITE_SERVER_URL}/todos).href, }) todos.sort((a, b) a.created_at - b.created_at)对应的数据模型是type ToDo { id: string title: string completed: boolean created_at: number }这里的关键点useShape来自electric-sql/react对应 monorepo 中的 packages/react-hooks 包。它把url作为形状端点发起长连接自动完成初始快照拉取、断线重连与增量更新data中的todos数组会随 Postgres 中todos表的变更实时变化VITE_SERVER_URL指向 Express 后端本地开发为:3010部署后为后端服务域名前端始终只暴露后端的/todos路径UI 层使用 Radix UI 组件Card、Checkbox等渲染列表。三种写操作都是普通fetch不经过 Electric切换完成状态PUTindex.tsx#L32-L46const onTodoClicked useCallback(async (todo: ToDo) { await fetch( new URL(${import.meta.env.VITE_SERVER_URL}/todos/${todo.id}).href, { method: PUT, headers: { Content-Type: application/json }, body: JSON.stringify({ completed: !todo.completed }), } ) }, [])删除DELETEindex.tsx#L48-L56const onTodoDeleted useCallback(async (todo: ToDo) { await fetch( new URL(${import.meta.env.VITE_SERVER_URL}/todos/${todo.id}).href, { method: DELETE } ) }, [])新建POSTindex.tsx#L100-L121注意 id 由前端用uuidv4()生成const id uuidv4() const formElem event.target as HTMLFormElement const formData Object.fromEntries(new FormData(formElem)) formElem.reset() const res await fetch( new URL(${import.meta.env.VITE_SERVER_URL}/todos).href, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ id, title: formData.todo }), } )点击卡片切换完成态、点X删除、底部表单新建——这些交互产生的任何一次数据库变更都会经由 Electric 推送给本页面以及其他所有订阅该表的客户端实现多方实时同步。后端Express 代理 直写 SQL完整实现见 server.js服务固定监听3010端口。GET /todos只转发协议参数的 Shape 代理这是示例中最有教学价值的一段server.js#L32-L86// GET /todos - proxy to Electric for syncing todos app.get(/todos, async (req, res) { const ELECTRIC_URL process.env.ELECTRIC_URL || http://localhost:3000 const electricUrl new URL(${ELECTRIC_URL}/v1/shape) // Only pass through Electric protocol parameters Object.keys(req.query).forEach((key) { if (ELECTRIC_PROTOCOL_QUERY_PARAMS.includes(key)) { electricUrl.searchParams.set(key, req.query[key]) } }) // Set the table server-side electricUrl.searchParams.set(table, todos) // Add source credentials if available if (process.env.ELECTRIC_SOURCE_ID) { electricUrl.searchParams.set(source_id, process.env.ELECTRIC_SOURCE_ID) } if (process.env.ELECTRIC_SOURCE_SECRET) { electricUrl.searchParams.set(secret, process.env.ELECTRIC_SOURCE_SECRET) } try { const response await fetch(electricUrl) // Remove problematic headers that could break decoding const headers {} response.headers.forEach((value, key) { if ( key.toLowerCase() ! content-encoding key.toLowerCase() ! content-length ) { headers[key] value } }) res.writeHead(response.status, response.statusText, headers) // Convert Web Streams to Node.js stream and pipe const nodeStream Readable.fromWeb(response.body) await pipeline(nodeStream, res) } catch (error) { // Ignore premature close errors - these happen when clients disconnect early if (error.code ERR_STREAM_PREMATURE_CLOSE) { return } ... } })其中ELECTRIC_PROTOCOL_QUERY_PARAMS从electric-sql/client导入定义在 packages/typescript-client/src/constants.ts#L37 并由包入口重新导出。它的用途是让代理只透传客户端协议必需的查询参数如续订游标等而丢弃其余参数——源码中的注释明确解释了动机如果原样转发所有参数客户端就能通过table、where、columns控制形状端点从而越权访问任意 Postgres 表可参考 skills 文档中的代理鉴权说明。该代理还体现了三层安全/健壮性设计表名服务端固定electricUrl.searchParams.set(table, todos)覆盖一切客户端尝试前端永远只能同步这张表凭据注入source_id/secret来自环境变量部署时由 SST 注入前端无从接触流式转发细节剥除content-encoding与content-长度头因为 Node 侧已解压保留会破坏客户端解码用Readable.fromWebpipeline把 SSE 流逐字节泵到响应并对客户端提前断开产生的ERR_STREAM_PREMATURE_CLOSE静默处理。写端点Zod 校验 参数化 SQLPOST/PUT/DELETE 三个端点都先做 schema 校验再执行 SQLconst idSchema z.string().uuid() const postSchema z.object({ id: z.string().uuid(), title: z.string(), }) const putSchema z.object({ title: z.string().optional(), completed: z.boolean().optional(), })POST /todosserver.js#L88-L108校验失败返回 400 与 zod 错误信息通过则insert into todos (id, title, completed, created_at) VALUES($1, $2, false, $3)completed固定为falsecreated_at取服务端当前时间PUT /todos/:idserver.js#L110-L125动态 UPDATE 由generateUpdateQueryserver.js#L201-L213生成把putSchema中出现的任意可选字段拼成SET col $n子句并全部参数化DELETE /todos/:idserver.js#L127-L137DELETE from todos where id $1另有GET /health返回 200供部署环境做健康检查。本地运行monorepo Docker Compose示例是 pnpm workspace 的一部分运行步骤与 examples/todo-app/README.md 一致在 monorepo 根目录安装并构建所有工作区包pnpm install pnpm run -r build进入examples/todo-app用 Docker Compose 拉起示例后端pnpm backend:up该脚本见 examples/todo-app/package.json实际执行PROJECT_NAMEtodo-app-example pnpm -C ../../ run example-backend:up pnpm db:migrate。根目录的example-backend:up会先down --volumes再启动README 特别提示这会停掉并删除其他示例容器挂载的卷确保示例总是从干净数据库启动。组合文件为 .support/docker-compose.yml两个服务分别是postgres:16-alpine映射54321:5432数据库electricelectricsql/electric:canary构建上下文为packages/sync-service/ELECTRIC_INSECURE: true映射3000:3000。README 与 compose 注释都注明 insecure 模式仅适用于开发环境。随后的db:migrate通过pg-migrationsdatabases/pg-migrations应用db/migrations下的迁移。启动开发服务器pnpm dev即dotenv -e .env -- concurrently vite node server.jsVite 负责前端热更新Express 后端同时起在3010端口。用完停掉后端服务pnpm backend:down部署形态Dockerfile 与 SST 配置示例附带两种部署配置可作为生产参考Dockerfile基于node:lts-alpine多阶段构建把 monorepo 中依赖的packages/typescript-client、packages/react-hooks与examples/todo-app一并拷贝、pnpm install --frozen-lockfile后全量构建最终镜像EXPOSE 3010、ENTRYPOINT [node, server.js]sst.config.ts通过 SST 把同一镜像部署为 EKS 服务负载均衡器转发443/https - 3010/http健康检查路径/health并注入DATABASE_URL、ELECTRIC_URL、ELECTRIC_SOURCE_ID、ELECTRIC_SOURCE_SECRET四个环境变量静态站点构建时把VITE_SERVER_URL设置为后端服务 URL从而让useShape的读路径与写 REST 路径都指向同一个代理后端。小结这个示例以极小的体量串起了 Electric 同步方案的完整闭环useShape一行代码完成实时订阅todos表结构定义形状边界Express 代理用ELECTRIC_PROTOCOL_QUERY_PARAMS白名单 服务端固定表名实现“前端拿不到 Electric 地址与凭据”的安全隔离而写路径则保持最朴素的 REST 参数化 SQL。若要进一步深入建议阅读 packages/typescript-client 中形状客户端的SPEC.md与 packages/react-hooks 的 Hook 实现理解data背后的连接管理与重连策略。【免费下载链接】electricThe agent platform built on sync.项目地址: https://gitcode.com/GitHub_Trending/el/electric创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表