news 2026/9/15 19:20:04

ElectricSQL TodoMVC 示例应用完全指南:从本地启动到读写分离架构剖析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ElectricSQL TodoMVC 示例应用完全指南:从本地启动到读写分离架构剖析

ElectricSQL TodoMVC 示例应用完全指南:从本地启动到读写分离架构剖析

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

本指南以 examples/todo-app/README.md 为核心,完整讲解 Electric 仓库中经典 TodoMVC 示例应用的启动步骤、工程结构与实现原理。你将掌握如何在 pnpm monorepo 中安装构建、通过 Docker Compose 一键拉起 Postgres 与 Electric 后端、执行数据库迁移,并理解示例如何用useShape实现响应式数据订阅、用 Express 服务端完成写入的"读-写分离"架构。

示例概览:一个经典 TodoMVC 应用

examples/todo-app是一个使用 ElectricSQL 开发的经典 TodoMVC 应用,属于 Electric 开源仓库("The agent platform built on sync")中的官方示例。从 package.json 可以看到它的技术栈定位:"Somewhat opinionated starter for ElectricSQL with Vite, and React Router",即一个面向 ElectricSQL 的、带有鲜明技术选型倾向的起步模板,核心依赖包括:

  • @electric-sql/client@electric-sql/react(均为workspace:*链接到仓库内源码):分别提供底层同步客户端与 React 的useShape响应式 Hook;
  • Vite + React 18 + React Router 6:前端工程与路由骨架;
  • Radix UI Themes + Fontsource:UI 组件与字体;
  • Express +pg+ zod + body-parser + cors:Node.js 写代理服务,负责接收前端写请求并直连 Postgres;
  • sst(Serverless Stack):云部署编排(详见 sst.config.ts)。

整个示例演示了 ElectricSQL 的典型"读-写分离"用法:读路径由前端通过 Electric Shape 订阅实时同步,写路径则由后端 API 直连数据库完成,数据库变更再通过 WAL 同步回所有订阅客户端。

前置条件:作为 pnpm workspace 一部分运行

该示例是 Electric monorepo 的组成部分,必须放在 pnpm-workspace.yaml 定义的工作区上下文中构建和运行,不能单独安装。因为 package.json 中的依赖声明为"@electric-sql/client": "workspace:*""@electric-sql/react": "workspace:*",这类 workspace 协议依赖只有在 monorepo 根目录执行pnpm install时才能被正确解析并软链接到packages/下的本地源码。

运行前需要准备的环境包括:

  • Node.js 与 pnpm:用于工作区安装与构建;
  • Docker + Docker Compose:示例后端服务(Postgres 与 Electric sync service)通过 Docker Compose 启动;
  • 可选的PostgreSQL 客户端工具db:migrate脚本通过pg-migrations(见 devDependencies)应用迁移,无需手工连接数据库。

安装与构建整个工作区

按照 README 的标准流程,首先进入 monorepo 根目录(示例位于examples/todo-app,其上一级两级即为仓库根):

cd ../../

然后安装并构建所有 workspace 包与示例:

pnpm install pnpm run -r build

pnpm install会依据 pnpm-workspace.yaml 解析全部 workspace 包(包括packages/sync-servicepackages/typescript-clientpackages/react-hooks等)并安装依赖;pnpm run -r build则按依赖顺序递归构建所有需要构建的包与示例,确保@electric-sql/client@electric-sql/react等 workspace 依赖的产物(dist)就绪,前端示例才能在后续构建中被正确引用。

构建完成后,回到示例目录:

cd examples/todo-app

启动后端服务:pnpm backend:up

在示例目录执行:

pnpm backend:up

这一命令会串联完成两件事(见 package.json 的 scripts 定义):

"backend:up": "PROJECT_NAME=todo-app-example pnpm -C ../../ run example-backend:up && pnpm db:migrate"
  1. 设置PROJECT_NAME=todo-app-example环境变量后,调用仓库根目录的example-backend:up脚本;
  2. 后端容器就绪后,立即执行db:migrate应用数据库迁移。

根目录脚本与 Docker Compose

仓库根 package.json 中定义了三个相关脚本:

"example-backend:up": "pnpm example-backend:down && pnpm example-backend:just_up", "example-backend:just_up": "dotenv -e .env.dev -- docker compose -f ./.support/docker-compose.yml up -d", "example-backend:down": "dotenv -e .env.dev -- docker compose -f .support/docker-compose.yml down --volumes"

也就是说backend:up实际执行的是down --volumes(停止并删除容器与卷)再up -d(后台启动)。README 特别提醒:该命令总会停止并删除其他示例后端容器挂载的卷,这是刻意为之,用于保证示例每次都以干净的数据库和干净的磁盘状态启动。因此在同一台机器上并行运行多个示例时需留意,backend:up会互相清理。

docker-compose.yml 服务详解

容器定义位于 .support/docker-compose.yml,包含两个服务:

postgres(数据库)

  • 镜像postgres:16-alpine,数据库名electric,用户/密码postgres/password
  • 端口映射54321:5432,将容器内 Postgres 暴露到宿主机 54321 端口(避免与本地常见 5432 冲突);
  • 使用./postgres.conf(见 .support/postgres.conf)作为配置,按 README 与 WAL 相关说明,该配置为 Electric 启用了所需的wal_level=logical等复制设置;
  • 数据目录与/tmp挂载为tmpfs(内存文件系统),这正是"每次启动都是干净数据库"的关键:容器重启即数据清空,配合down --volumes保证磁盘干净。

backend(Electric sync service)

  • 镜像electricsql/electric:canary(canary 为每日构建的开发镜像,对应 packages/sync-service 源码);
  • 通过DATABASE_URL指向同 Compose 网络内的 Postgres:postgresql://postgres:password@postgres:5432/electric?sslmode=disable
  • 设置ELECTRIC_INSECURE: true:配置文件注释明确警告"不适合生产环境,仅应在开发时或已通过其他方式保护 Electric API 时使用";
  • 端口映射3000:3000,即 Electric 的 Shape HTTP 端点默认监听地址;
  • build字段指向../packages/sync-service/,允许在需要时从源码构建镜像而非拉取远程镜像。

数据库结构与迁移

backend:up的第二步是执行:

"db:migrate": "dotenv -e ../../.env.dev -- pnpm exec pg-migrations apply --directory ./db/migrations"

该命令从仓库根 .env.dev 读取DATABASE_URL等环境变量,用@databases/pg-migrations把 db/migrations 目录下的 SQL 按序应用到数据库。示例仅有一个迁移文件 db/migrations/001-create-todos.sql:

CREATE 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 );

表结构与前端 TypeScript 类型一一对应(见 src/routes/index.tsx 中的ToDo类型):id为 UUID 主键(客户端用uuid库生成),title为待办文本,completed为完成状态布尔值,created_at为带时区时间戳用于前端排序。注意迁移文件位于db/migrations目录下,这正是 Electric 官方示例与文档推荐的迁移管理方式:数据库结构变更与同步配置一起版本化。

启动开发服务器:pnpm dev

后端就绪后,在示例目录启动开发服务器:

pnpm dev

对应脚本为:

"dev": "dotenv -e .env -- concurrently \"vite\" \"node server.js\""

它从示例目录的 .env(仓库中未提交,由开发环境提供)读取配置,并用concurrently同时启动两个进程:

  1. Vite 开发服务器:基于 vite.config.ts(仅启用@vitejs/plugin-react-swc),托管 React 前端。应用入口 src/main.tsx 通过 React Router 的createBrowserRouter注册/路由,根路由 src/routes/root.tsx 仅渲染<Outlet />,业务页面对应 src/routes/index.tsx,整体包裹在 Radix UI 的暗色Theme中。
  2. Node.js 写代理服务:server.js 是一个 Express 应用,监听3010端口,提供/todos的 REST 接口。

前端通过环境变量VITE_SERVER_URL定位后端地址(在 src/routes/index.tsx 中形如new URL(\${import.meta.env.VITE_SERVER_URL}/todos`)`),所有数据读写都经由该服务。

应用实现剖析:读订阅与写代理

读路径:useShape 响应式订阅

前端核心是 src/routes/index.tsx 中的useShape

const { data: todos } = useShape<ToDo>({ url: new URL(`${import.meta.env.VITE_SERVER_URL}/todos`).href, }) todos.sort((a, b) => a.created_at - b.created_at)

useShape来自@electric-sql/react(对应 packages/react-hooks 与 packages/typescript-client),它订阅一个 Shape URL,将结果映射为todos数组。当 Postgres 中todos表发生任何写入/更新/删除时,Electric 会通过逻辑复制捕获变更并推送,React 组件自动重渲染——因此列表无需手动刷新即可实时反映所有客户端(包括其他浏览器窗口)的修改。示例在渲染前按created_at升序排序,保证新增待办追加到列表末尾。空列表时显示 "No to-dos to show - add one!" 占位提示。

写路径:Express + pg 直连数据库

与读路径走 Shape 同步不同,写路径统一通过 server.js 的 REST 接口完成:

  • POST/todos:前端生成iduuidv4())与title后提交,服务端先用 zod 的postSchema校验(id必须为 UUID、title为字符串),再执行参数化 INSERT,completed默认falsecreated_at取当前时间;
  • PUT/todos/:id:点击待办卡片时触发,通过putSchema校验可选字段,并借助generateUpdateQuery动态拼接UPDATE "todos" SET ... WHERE id = ...的参数化语句,切换completed状态;
  • DELETE/todos/:id:点击卡片右侧的 "X" 幽灵按钮触发(e.stopPropagation()防止冒泡到卡片的点击切换逻辑),执行DELETE from todos where id = $1

服务端所有 SQL 均使用$1参数化占位符,避免 SQL 注入;数据库写入成功后,Postgres 的 WAL 变更会被 Electric 捕获并广播,所有useShape订阅端随之更新,形成"写库 → 同步 → 多端一致"的闭环。

GET /todos:Shape 代理与鉴权透传

server.js 中最具架构参考价值的是GET /todos:它并不查数据库,而是将请求代理到 Electric 的 Shape 端点

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`)

关键设计有三点:

  1. ELECTRIC_PROTOCOL_QUERY_PARAMS(从@electric-sql/client导入)是客户端握手所需的协议参数白名单,代理只透传白名单内的参数,其余查询参数一律丢弃,避免任意参数被转发;
  2. 表名todos由服务端强制指定,客户端无法自定义要同步的表;
  3. 若配置了ELECTRIC_SOURCE_ID/ELECTRIC_SOURCE_SECRET,会附加source_idsecret查询参数完成对 Electric 的源鉴权。

随后代理把 Electric 返回的 Web Stream 转为 Node 流管道回客户端,并剥离content-encodingcontent-length两个可能破坏解码的响应头;对ERR_STREAM_PREMATURE_CLOSE(客户端提前断开)等错误做了静默容错。该模式让示例可以复用 Electric 的原生同步协议,同时把鉴权、参数校验等逻辑收敛在自家后端。

停止后端服务

开发完成后,在示例目录执行:

pnpm backend:down

对应脚本为:

"backend:down": "PROJECT_NAME=todo-app-example pnpm -C ../../ run example-backend:down"

最终调用根目录的example-backend:downdocker compose ... down --volumes),停止 postgres 与 backend 两个容器并删除卷。由于数据目录本身是 tmpfs,即使不手动清理,重启后数据也会自动归零。

部署形态(参考)

除本地开发外,示例还通过 sst.config.ts 提供云端部署编排,可作为理解生产形态的参考:使用 Neon 提供托管 Postgres 并自动应用./db/migrations迁移(createDatabaseForCloudElectric同时产出sourceId/sourceSecret用于 Electric 鉴权);在共享 ECS 集群上部署 Node 后端容器,通过负载均衡把443/https转发到容器内3010/http,健康检查指向/health;前端以sst.aws.StaticSite构建部署,并把后端地址注入为VITE_SERVER_URL环境变量。整个编排同样体现了"后端持有DATABASE_URLELECTRIC_URL、前端只拿公开 URL"的隔离原则。

小结

通过本文的步骤,你可以完整走通 ElectricSQL 官方 TodoMVC 示例:在 examples/todo-app 中先pnpm install && pnpm run -r build构建 monorepo,再用pnpm backend:up一键拉起(并自动清理)Postgres 与 Electric 后端、应用迁移,随后pnpm dev并行启动 Vite 前端与 Express 写代理。源码层面,src/routes/index.tsx 演示了useShape的声明式订阅,server.js 演示了参数化写入、Shape 代理与协议参数白名单过滤,而 .support/docker-compose.yml 则展示了为 Electric 调优过的本地开发环境模板。这套"读走 Shape、写走 API"的架构,正是将 ElectricSQL 集成进既有服务端工程时的推荐起点。

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

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/15 19:19:52

时延抖动本质与实战治理:从网络卡顿到精准控制

1. 时延抖动不是“网络卡”&#xff0c;而是数据包在时间维度上的“醉汉走路”很多人一听到“网络卡”&#xff0c;第一反应是带宽不够、路由器太旧、WiFi信号弱——这些确实会影响网速&#xff0c;但它们主要拖慢的是平均传输速度。而“时延抖动”&#xff08;Jitter&#xff…

作者头像 李华
网站建设 2026/9/15 19:16:45

高速公路智能事件检测服务器部署调优实战经验

做过高速机电项目的人应该都有印象&#xff0c;路网中心那面电视墙上几十上百路视频&#xff0c;靠人眼盯着根本不现实&#xff0c;尤其是夜间和恶劣天气&#xff0c;画面里一个停下来的小车、一个翻越护栏的行人&#xff0c;可能几秒钟就酿成大事故。大华事件检测智能服务器就…

作者头像 李华
网站建设 2026/9/15 19:15:26

Pytorch实现DenseNet:密集连接、特征复用与CIFAR-10训练实战

简介&#xff1a;基于Pytorch实现DenseNet的完整项目源码&#xff0c;面向希望系统掌握经典卷积神经网络结构与Pytorch工程实现的开发者、研究人员及深度学习初学者。项目围绕DenseNet的核心机制展开&#xff0c;覆盖稠密块、过渡层、增长率与瓶颈层等关键设计&#xff0c;并配…

作者头像 李华