十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

在 Next.js 中集成 Electric 同步引擎:路由代理、认证与写入模式的实战指南

在 Next.js 中集成 Electric 同步引擎:路由代理、认证与写入模式的实战指南 在 Next.js 中集成 Electric 同步引擎路由代理、认证与写入模式的实战指南【免费下载链接】electricThe agent platform built on sync.项目地址: https://gitcode.com/GitHub_Trending/el/electricElectric 是一个基于 Postgres 逻辑复制的同步引擎而 Next.js 是一个全栈 React 框架。本文以仓库文档 website/docs/sync/integrations/next.md 为核心骨架讲解如何将两者组合在客户端组件中使用 Electric 的 React Hooks 消费 Shape 数据通过 Next.js Route Handler 代理 Shape 请求以保证密钥不暴露并将写入路径交给自己的 API。读完本文你将掌握在 Next.js 应用里落地实时数据同步的完整方案从useShape绑定数据、到服务端代理与授权、再到四种写入模式的选择。Electric 与 Next.js 的集成定位Next.js 基于 React 构建。Electric 对 React 有一等公民的支持维护了 electric-sql/react 包提供useShape、preloadShape、getShapeStream、getShape等 Hooks 与辅助函数用于把 Shape 数据绑定到组件上见 React 集成文档。因此在 Next.js 中集成 Electric 与集成任何其他 npm / React 库没有本质区别——你既可以在纯 React 前端使用它也可以充分利用 Next.js 的服务端能力Route Handler、中间件、SSR来加固数据访问。仓库中的示例现状需要注意的是本仓库当前没有维护examples/nextjs示例应用。根据 next.md 的说明之前的示例于 2026 年 4 月 23 日从main分支移除原因是其落后于受支持的 Next.js 与 React 技术栈。这一点在 Next.js 示例页 中也有同步说明。不过集成 Next.js 所需的关键环节并不依赖那个被移除的示例而是由当前仓库中仍在维护的组件构成客户端React Hooks 包 与 TypeScript 客户端用于在客户端组件中订阅 Shape。服务端认证指南 提供的 Route Handler / 代理实现保证密钥留在服务端。写入路径写入模式指南 提供的四种写入模式。数据模型Shape 指南 提供的 Shape 定义表、where、columns、queryable columns。历史参考仓库中保留的 Next.js demo 页面 可作为历史部署参考但当前实现应以本文引用的各指南为准。第一步在客户端组件中使用 React Hooks 订阅 Shape安装依赖在 Next.js 项目中安装 React Hooks 包它依赖 TypeScript 客户端electric-sql/client后者会作为依赖被引入npm i electric-sql/react若需要直接使用ShapeStream/Shape底层原语也可单独安装npm i electric-sql/client用useShape绑定数据在客户端组件use client中useShape会把一个物化后的 Shape 绑定到状态变量上并在数据更新时自动触发重渲染。推荐的生产模式是把请求指向你自己的 API 端点由服务端代理到 Electricuse client import { useShape } from electric-sql/react const MyComponent () { const { isLoading, data } useShape{ title: string }({ url: /api/items, // 你的 Next.js Route Handler服务端再代理到 Electric }) if (isLoading) { return divLoading .../div } return ( div {data.map((item) ( div key{item.title}{item.title}/div ))} /div ) }在开发环境或可信环境也可以直接连接 Electric 服务并通过params传递 Shape 定义表名、where、columns 等 Postgres 特定参数use client import { useShape } from electric-sql/react const MyFilteredComponent () { const { isLoading, data } useShape{ id: number; title: string }({ url: http://localhost:3000/v1/shape, params: { table: items, where: status active, columns: [id, title], }, }) if (isLoading) { return divLoading .../div } return ( div {data.map((item) ( div key{item.id}{item.title}/div ))} /div ) }⚠️ 直接连接会暴露数据库结构仅限开发环境。生产应用必须走服务端代理。useShape接受与ShapeStream相同的 options返回UseShapeResult字段类型说明dataT[]组成物化 Shape 的行数组shapeShapeT该useShape使用的 Shape 实例isLoadingboolean初始拉取期间为true之后为falselastSyncedAtnumber?最近一次同步的 Unix 时间isLoading时为undefinedisErrorboolean是否出错errorShapeT[error]错误对象其他 Hooks 与辅助函数除了useShapereact-hooks.tsx 还导出了几个适合在路由加载阶段或模块级使用的函数其内部实现基于全局缓存可避免同一 Shape 重复建流、重复物化preloadShape在路由加载函数中提前确保 Shape 数据就绪后再渲染。从源码看react-hooks.tsx#L17-L24它内部先getShapeStream再getShape并await shape.rows直到数据完全加载。非常适合 Next.js 的generateStaticParams、Server Component 预取或客户端路由加载阶段// 生产模式走 API 代理 export const clientLoader async () { return await preloadShape({ url: /api/items, }) } // ⚠️ 开发模式直接连接 export const devLoader async () { return await preloadShape({ url: http://localhost:3000/v1/shape, params: { table: items, }, }) }getShapeStreamTreact-hooks.tsx#L47基于全局缓存 get-or-create 一个ShapeStream避免对同一 Shape 日志建立多个流。缓存键是sortedOptionsHash(options)——对所有 options 按键排序后序列化因此不同组件只要传入相同配置就会复用同一流。getShapeTreact-hooks.tsx#L71基于全局缓存 get-or-create 一个物化Shape避免对同一流多次物化。// ✅ 生产模式 const itemsStream getShapeStreamItem({ url: /api/items, }) const itemsShape getShapeItem(itemsStream)组件卸载时中止订阅如果希望在组件卸载或路由离开时中止 Shape 对实时更新的订阅可以使用AbortController。注意如果有多个组件共享同一个流getShapeStream的缓存会让这种情况很容易发生中止会停止所有订阅者因此请谨慎使用function MyComponent() { const [controller, _] useState(new AbortController()) const { data } useShape({ url: /api/items, signal: controller.signal, }) useEffect(() { return () { // 实时更新现在被禁用 controller.abort() } }, []) // ... }第二步通过 Route Handler 代理 Shape 请求为什么要代理Electric 的同步 API 是纯 HTTPGET /v1/shape携带 Shape 定义?tableitems等即可订阅数据。这意味着你可以像保护任何 Web 资源一样保护 Shape——在请求到达 Electric 之前先经过你的 Next.js Route Handler或中间件、边缘函数完成鉴权与授权。代理模式的核心原则是服务端控制 Shape 定义table、queryable_columns、主where以及 API 密钥secret都必须在服务端设置绝不能交给客户端客户端只能传递offset、handle、live、live_sse、replica、log等协议参数以及 POST body 中的子集查询参数。这样即使客户端恶意传参也无法扩大数据访问范围详见 认证指南 的“Parameters your proxy must control”一节。完整的 Route Handler 实现下面是一个可直接放入app/api/items/route.ts的完整实现它综合了 认证指南 的代理示例// app/api/items/route.ts import { ELECTRIC_PROTOCOL_QUERY_PARAMS } from electric-sql/client export async function GET(request: Request) { const url new URL(request.url) // 构造上游 Electric 地址 const originUrl new URL(http://localhost:3000/v1/shape) // 只透传 Electric 协议参数offset、handle、live 等 url.searchParams.forEach((value, key) { if (ELECTRIC_PROTOCOL_QUERY_PARAMS.includes(key)) { originUrl.searchParams.set(key, value) } }) // 表名由服务端设置不接受客户端参数 originUrl.searchParams.set(table, items) // // 认证与授权 // const user await loadUser(request.headers.get(authorization)) // 用户不存在则返回 401 if (!user) { return new Response(user not found, { status: 401 }) } // 非管理员只能访问自己组织的数据。 // 使用参数化查询防止 SQL 注入 if (!user.roles.includes(admin)) { originUrl.searchParams.set(where, org_id $1) originUrl.searchParams.set(params[1], user.org_id) } const response await fetch(originUrl) // fetch 会解压 body但不会移除 content-encoding 与 content-length // 头这会导致浏览器解码失败。 // 参见 https://github.com/whatwg/fetch/issues/1729 const headers new Headers(response.headers) headers.delete(content-encoding) headers.delete(content-length) return new Response(response.body, { status: response.status, statusText: response.statusText, headers, }) }关键点ELECTRIC_PROTOCOL_QUERY_PARAMS来自electric-sql/client是客户端需要透传的协议参数白名单offset、handle、live等代理只放行这些参数。表名与授权where全部服务端设定非管理员用org_id $1params[1]做参数化过滤防止 SQL 注入。返回前必须删除content-encoding与content-length头fetch 已解压 body保留这两个头会破坏浏览器端解码。类型安全的 where 子句生成当授权规则复杂时手拼 SQL 字符串容易出错。可以用 Drizzle 或 Kysely 在编译期生成类型安全的 where 子句。由于 Electric 的 HTTP API 通过独立查询参数params[1]value、params[2]value传递参数值来安全替换$1、$2占位符代理只需提取 where 片段并把参数逐个写入即可。Drizzle用column.namesql.identifier()生成不带表前缀的列名因为 Electric 期望不带表前缀的列名import { QueryBuilder } from drizzle-orm/pg-core import { sql } from drizzle-orm import { users } from ./schema // 你的 Drizzle schema 定义 export async function GET(request: Request) { // ... 前置代码 ... const user await loadUser(request.headers.get(authorization)) if (!user || user.roles.includes(admin)) { // 管理员看到全部 } else { // 类型安全的 where 表达式引用不存在的列会在编译期报错 const whereExpr sql${sql.identifier(users.org_id.name)} ${user.org_id} // 无需数据库连接即可编译为 SQL 片段 const qb new QueryBuilder() const { sql: query, params } qb .select() .from(users) .where(whereExpr) .toSQL() // 提取 WHERE 子句片段 const fragment query.replace(/^SELECT .* FROM .* WHERE\s/i, ) originUrl.searchParams.set(where, fragment) // 参数逐个写入params[1]value, params[2]value ... params.forEach((value, index) { originUrl.searchParams.set(params[${index 1}], String(value)) }) } // ... fetch 并返回响应 ... }Kysely编译后需要去掉表前缀import { db } from ./db // 带生成类型的 Kysely 实例 export async function GET(request: Request) { // ... 前置代码 ... if (!user.roles.includes(admin)) { // 引用无效列会在编译期报错 const query db .selectFrom(users) .selectAll() .where(org_id, , user.org_id) .where(status, , active) const { sql: query, parameters } query.compile() let fragment query.replace(/^SELECT .* FROM .* WHERE\s/i, ) fragment fragment.replace(/\b\w\./g, ) // 去掉表前缀 originUrl.searchParams.set(where, fragment) parameters.forEach((value, index) { originUrl.searchParams.set(params[${index 1}], String(value)) }) } }两种方式都提供编译期校验引用无效列、错误类型、不兼容操作符都会报错、SQL 注入防护、重命名安全与 IDE 自动补全。支持 POST 子集查询当 where 子句变得很大复杂的 ACL 子查询、大量参数或WHERE id ANY($1)包含数百个 ID时GET 请求会因 URL 过长而返回HTTP 414 Request-URI Too Long。Electric 支持用 POST 把子集参数放在 JSON body 中。POST body 支持的参数参数类型说明wherestring过滤子集的 WHERE 子句paramsobject参数形如{1: value1, 2: value2}对应$1、$2占位符limitinteger最大返回行数需要order_byoffsetinteger分页跳过的行数需要order_byorder_bystringORDER BY 子句使用 limit/offset 时必填{ where: \organization_id\ $1 AND (\owner_user_id\ $2 OR ...), params: {1: org_123, 2: user_456}, order_by: created_at DESC, limit: 100 }安全模型Electric 始终用AND组合主 Shape whereURL 中与子集 wherePOST body 中即WHERE {main_shape_where} AND ({subset_where})。因此子集查询只能收窄结果、永远不能扩大访问范围即使客户端发送where: 11主 where 依然生效。子集 where 还会校验语法、禁止子查询并在设置queryable_columns时限制只能引用这些列。代理需要同时支持 GET 与 POST并始终在服务端设置table、queryable_columns、主where与secretimport { ELECTRIC_PROTOCOL_QUERY_PARAMS } from electric-sql/client export async function handler(request: Request) { const url new URL(request.url) const method request.method const originUrl new URL(http://localhost:3000/v1/shape) // 透传协议参数offset、handle、live 等 url.searchParams.forEach((value, key) { if (ELECTRIC_PROTOCOL_QUERY_PARAMS.includes(key)) { originUrl.searchParams.set(key, value) } }) // 认证 const user await loadUser(request.headers.get(authorization)) if (!user) { return new Response(unauthorized, { status: 401 }) } // 服务端设置 Shape 定义这是你的授权层 originUrl.searchParams.set(table, items) originUrl.searchParams.set(queryable_columns, id,title,created_at) originUrl.searchParams.set(columns, id,title) originUrl.searchParams.set(where, organization_id ${user.org_id}) originUrl.searchParams.set(secret, process.env.ELECTRIC_SECRET) // 转发请求到 Electric let response: Response if (method POST) { // POST透传客户端 body子集参数只能收窄结果 const clientBody await request.text() response await fetch(originUrl, { method: POST, headers: { Content-Type: application/json }, body: clientBody, }) } else { // GET简单代理 response await fetch(originUrl) } const headers new Headers(response.headers) headers.delete(content-encoding) headers.delete(content-length) return new Response(response.body, { status: response.status, statusText: response.statusText, headers, }) }客户端侧只需配置subsetMethod: POSTimport { ShapeStream } from electric-sql/client const stream new ShapeStream({ url: /api/shapes/items, // 你的代理端点Next.js Route Handler headers: { Authorization: Bearer ${token}, }, subsetMethod: POST, // 子集请求走 POST })⚠️前瞻性提醒根据 认证指南 的说明Electric 2.0 中 GET 子集快照请求将被弃用仅支持 POST。建议现在就让代理同时支持 GET/POST并让客户端切换到subsetMethod: POST为升级做好准备。第三步让密钥只存在于服务端——认证与安全令牌怎么带到代理客户端通过headers选项携带Authorization头代理Route Handler据此解析用户并做授权。TypeScript 客户端还支持函数式选项便于动态刷新令牌const stream new ShapeStream({ url: http://localhost:3000/v1/shape, headers: { // 每次请求都会重新求值 Authorization: async () Bearer ${await getAccessToken()}, }, })这个模式适用于需要周期性刷新令牌、基于会话的认证、从安全存储取令牌或需要自动处理令牌轮换的场景。处理 401/403 与错误重试代理返回 401认证失败或 403无权限时客户端可用onError回调刷新凭证并重试。返回值的语义返回{ headers }或{ params }用新值重试返回{}用相同配置重试适合瞬时错误返回 void 则停止流。5xx 服务端错误会自动按指数退避重试。const stream new ShapeStream({ url: /api/shapes/items, headers: { Authorization: Bearer ${currentToken}, }, onError: async (error) { if (error instanceof FetchError error.status 401) { // 令牌过期——刷新并重试 const newToken await refreshAuthToken() return { headers: { Authorization: Bearer ${newToken}, }, } } // 其他错误停止同步 }, })会话失效与缓存隔离Vary 头如果 CDN 或浏览器缓存了 Shape 响应用户登出后仍可能读到旧数据。解决方法是让响应携带Vary头把认证上下文纳入缓存键Vary: Authorization或基于 Cookie 的认证Vary: Cookie若同时支持多种认证方式可合并Vary: Authorization, Cookie。这样不同用户的请求会被分别缓存登出后失去凭证的用户无法命中已认证的缓存响应。同时登出时应做整页刷新以清空内存中的已同步 Shape 数据async function handleLogout() { await clearAuthToken() // 整页刷新清空内存中的全部同步数据 window.location.reload() }Gatekeeper 模式另一种选择除代理模式外认证指南 还描述了 Gatekeeper 模式客户端向你的 API 中的 gatekeeper 端点POST /gatekeeper/:table提交凭证与 Shape 定义gatekeeper 授权后签发包含 Shape 声明的 shape-scoped JWT此后客户端通过授权代理访问 Electric代理验证 JWT 并核对令牌中的 Shape 声明与请求参数完全一致才放行。该模式把主鉴权逻辑收敛到 API 中只执行一次适合在边缘函数中做轻量校验也适用于 CDN 场景。仓库中的 gatekeeper-auth 示例 提供了 APIElixir/Phoenix、Caddy 反向代理、边缘函数三种代理实现。第四步写入路径——四种模式的选择Electric 只做读路径同步数据从 Postgres 同步进本地应用它不提供也不规定写路径方案。在 Next.js 中读路径交给 Electric代理后的 Shape写路径则由你自由选择。写入模式指南 给出了四种由简到繁的模式均有 write-patterns 示例 对应的源码1. 在线写入Online writes最直接把同步读与常规 REST API 调用写组合。适合只读应用、偶尔写入或必须在线才能编辑的应用——例如实时仪表盘、数据分析可视化、在云端生成 embedding 的 AI 应用、支付类系统。优点实现简单可复用现有 API。缺点写路径上有网络延迟UI 要等服务器响应离线不可用。2. 乐观状态Optimistic state在模式 1 之上用 React 内置的useOptimistic钩子示例源码在等待服务器响应时立即显示“乐观”状态写入成功后会经 Electric 自动同步回应用此时丢弃乐观状态即可。优点把网络移出写路径读写都支持离线实现简单。缺点示例中的乐观状态是组件作用域的、不持久化——其他组件可能显示不一致信息卸载组件或刷新页面会丢失乐观状态。3. 共享持久化乐观状态Shared persistent optimistic state在模式 2 基础上把乐观状态放进共享、持久化的本地存储示例用 valtio localStorage示例源码。所有组件都能看到并响应乐观状态matchWrite合并逻辑支持把本地乐观状态 rebase 到其他用户的并发更新上回滚入口拥有写上下文、可回滚单条写入。优点写更抗中断、组件一致不可变同步状态与可变本地状态分离便于实现回滚。缺点读路径上合并数据会让本地读取稍慢写入仍走 API。4. 通过数据库同步Through the database sync把共享持久化乐观状态延伸到本地嵌入式数据库示例用 PGlite示例源码。应用代码直接读写本地数据库变更在后台自动同步。具体做法数据同步进不可变的todos_synced表乐观状态持久化在影子表todos_local用todos视图在读取时合并写路径用INSTEAD OF触发器把对视图的写入重定向到本地表并记录changes日志再用NOTIFY驱动同步工具把变更发往服务器。优点纯本地优先体验组件只与本地数据库交互读写接口统一。缺点嵌入式数据库是较重依赖影子表与触发器让客户端 schema 复杂化后台同步使回滚上下文难以重建。选择建议顺序即复杂度在线写入最简单乐观状态适合想要快速响应的管理类应用与移动应用共享持久化状态在复杂度与体验间取得良好平衡通过数据库同步则适合纯本地优先的协作/创作类软件。若需要处理并发合并与回滚指南还给出了进阶讨论合并逻辑、回滚策略并指出实践中冲突非常罕见、可用简单的暴力策略应对大部分场景。关于 SSR 的现状Next.js 支持 SSR而 Electric 团队目前正在探索用 SSR 使用 Electric 的模式仓库中标注为实验性。目标是在服务器渲染与客户端组件无缝进入实时同步之间取得平衡。因此当前文档给出的核心建议是客户端组件内使用标准 Electric React 客户端模式SSR 相关的框架集成仍在演进中仓库以 HelpWanted issue #1596 公开征集改进。生产项目应优先采用本文第二步的路由代理方案并保持客户端组件边界清晰。参考资料关联文档Next.js 集成指南React 集成指南认证指南写入模式指南Shape 指南TypeScript 客户端文档React Hooks 源码Next.js 示例页历史部署参考write-patterns 示例gatekeeper-auth 示例【免费下载链接】electricThe agent platform built on sync.项目地址: https://gitcode.com/GitHub_Trending/el/electric创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表