
在 React 中使用 urql 与 Nhost SDK 集成 GraphQL从认证交换器到类型安全代码生成【免费下载链接】nhostThe Open Source Firebase Alternative with GraphQL.项目地址: https://gitcode.com/GitHub_Trending/nh/nhost本文以 examples/guides/react-urql 示例项目为主线系统讲解如何将 urql 与 Nhost SDK 深度集成到 React 应用中从依赖安装、GraphQL CodeGen 配置到基于urql/exchange-auth的 JWT 自动注入与无感刷新再到组件内查询与变更的完整落地。读完本文你将能够独立搭建一套类型安全 自动认证 文档缓存的 React GraphQL 应用并理解preferGetMethod: false等关键配置背后的 Hasura 兼容性原理。整体架构为什么选择 urql Nhost SDKNhost 是一个开源的 Firebase 替代方案The Open Source Firebase Alternative with GraphQL其 GraphQL 层基于 Hasura。React 侧接入 GraphQL 有多种选择本指南采用的组合是urql轻量、可扩展的 GraphQL 客户端通过 exchange交换器管道处理缓存、认证、请求发送等横切关注点urql/exchange-auth专门处理为每个请求附加令牌、识别认证错误、刷新令牌、失败登出的认证交换器nhost/nhost-jsNhost 官方 JavaScript SDK负责管理用户会话access token / refresh token与认证流程GraphQL CodeGen typed-document-node从 GraphQL schema 与操作文档生成强类型代码让查询与变更天然具备端到端类型安全。从示例的 package.json 可以看到依赖版本组合urql^5.0.1、urql/exchange-auth^3.0.0、graphql^16.11.0、graphql-typed-document-node/core^3.2.0而nhost/nhost-js以workspace:*形式直接引用本仓库源码说明这是跟随仓库主线开发的示例。除此之外还使用了 React 19、React Router 8 与 Vite 8 搭建应用骨架。第一步安装依赖在项目根目录执行以下任意一种安装命令npm / yarn / pnpm 均可示例仓库本身使用 pnpmnpm install urql urql/exchange-auth nhost/nhost-js graphql graphql-typed-document-node/core # 或 yarn add urql urql/exchange-auth nhost/nhost-js graphql graphql-typed-document-node/core # 或 pnpm add urql urql/exchange-auth nhost/nhost-js graphql graphql-typed-document-node/core其中graphql-typed-document-node/core提供TypedDocumentNode类型定义是 urql 消费 CodeGen 生成文档节点的类型桥梁graphql是 GraphQL 语言的核心运行时urql 与 CodeGen 均依赖它。第二步安装 GraphQL CodeGen 开发依赖类型安全的前提是从 schema 生成类型因此需要安装以下开发依赖npm install -D graphql-codegen/cli graphql-codegen/typescript graphql-codegen/typescript-operations graphql-codegen/typed-document-node graphql-codegen/schema-ast # 或 yarn add -D graphql-codegen/cli graphql-codegen/typescript graphql-codegen/typescript-operations graphql-codegen/typed-document-node graphql-codegen/schema-ast # 或 pnpm add -D graphql-codegen/cli graphql-codegen/typescript graphql-codegen/typescript-operations graphql-codegen/typed-document-node graphql-codegen/schema-ast五个插件的分工插件作用typescript从 schema 生成基础 TypeScript 类型对象、输入类型、枚举等typescript-operations根据.graphql操作文档生成对应的结果类型与变量类型typed-document-node将操作与类型绑定为TypedDocumentNode供 urql 直接使用schema-ast将远端 schema 导出为本地schema.graphql文件cli提供graphql-codegen命令行入口第三步配置 GraphQL CodeGen在项目根目录创建codegen.ts示例中的完整文件见 codegen.tsimport type { CodegenConfig } from graphql-codegen/cli; const config: CodegenConfig { schema: [ { https://local.graphql.local.nhost.run/v1: { headers: { x-hasura-admin-secret: nhost-admin-secret, }, }, }, ], documents: [src/**/*.ts], ignoreNoDocuments: true, generates: { ./src/lib/graphql/__generated__/graphql.ts: { documents: [src/lib/graphql/**/*.graphql], plugins: [typescript, typescript-operations, typed-document-node], config: { scalars: { UUID: string, uuid: string, timestamptz: string, jsonb: Recordstring, any, bigint: number, bytea: Buffer, citext: string, }, useTypeImports: true, }, }, ./schema.graphql: { plugins: [schema-ast], config: { includeDirectives: true, }, }, }, }; export default config;关键配置解读schema 指向本地 Nhost 开发环境https://local.graphql.local.nhost.run/v1是nhost dev启动的本地 GraphQL 端点通过x-hasura-admin-secret: nhost-admin-secret本地默认值以管理员身份拉取完整 schemascalars 映射将 Hasura 特有标量映射为前端可用类型——uuid、timestamptz映射为stringjsonb映射为Recordstring, anybigint映射为numberbytea映射为Buffercitext映射为string。这一步能显著减少手写类型转换useTypeImports: true生成代码使用import type便于 tree-shaking 与隔离两个输出目标类型文件输出到src/lib/graphql/__generated__/graphql.ts同时把远端 schema 原样导出到根目录schema.graphql便于离线查看结构。在package.json中注册生成脚本{ scripts: { generate: graphql-codegen --config codegen.ts } }示例仓库还提供了 codegen-wrapper.sh 包装脚本先执行pnpm graphql-codegen --config codegen.ts再用biome check --write对生成的graphql.ts与schema.graphql做统一格式化保证产物与仓库代码风格一致对应package.json中的generate: bash codegen-wrapper.sh。第四步创建 AuthProvider —— 管理 Nhost 会话状态认证状态是整个集成的根基。示例中的 AuthProvider.tsx 是一个 React Context 组件对外暴露interface AuthContextType { user: StoredSession[user] | null; // 当前登录用户 session: StoredSession | null; // 完整会话含令牌与用户信息 isAuthenticated: boolean; // 是否已认证 isLoading: boolean; // 会话是否仍在初始化 nhost: NhostClient; // Nhost 客户端实例 }核心实现要点// 初始化 Nhost 客户端本地开发默认 region/subdomain 均为 local const nhost useMemo( () createClient({ region: import.meta.env.VITE_NHOST_REGION || local, subdomain: import.meta.env.VITE_NHOST_SUBDOMAIN || local, }), [], );初始化时通过import.meta.env.VITE_NHOST_REGION/VITE_NHOST_SUBDOMAIN读取 Vite 环境变量缺省回退到local对应本地 Nhost 开发环境。会话初始化与同步分为三层初次加载useEffect中调用nhost.getUserSession()读取持久化会话写入user/session/isAuthenticated状态并关闭isLoading跨标签页同步通过nhost.sessionStorage.onChange(...)订阅会话变更。底层存储发生更新时例如另一个标签页完成登录/登出回调会对比refreshTokenId与lastRefreshTokenIdRef仅在令牌确实变化时刷新 React 状态避免无谓重渲染页面焦点一致性监听visibilitychange与window focus事件页面重新可见时再次调用reloadSession校准会话保证从后台切回时状态不过期。useAuthHook 在组件树之外调用时会抛出useAuth must be used within an AuthProvider错误强制约束使用边界。第五步创建 UrqlProvider —— 接入认证交换器认证与 GraphQL 客户端的结合点在 UrqlProvider.tsx。核心是 urql 的 exchange 管道cacheExchange → authExchange → fetchExchange。const client: Client createClient({ url: import.meta.env.VITE_NHOST_GRAPHQL_URL || https://local.graphql.local.nhost.run/v1, // Force POST requests (Hasura interprets GET requests as persisted queries) preferGetMethod: false, exchanges: [ cacheExchange, authExchange(async (utils) { return { addAuthToOperation(operation) { const session nhost.getUserSession(); if (!session?.accessToken) { return operation; } return utils.appendHeaders(operation, { Authorization: Bearer ${session.accessToken}, }); }, didAuthError(error) { return error.graphQLErrors.some((e) e.message.includes(JWTExpired), ); }, async refreshAuth() { const currentSession nhost.getUserSession(); if (!currentSession?.refreshToken) { return; } try { await nhost.refreshSession(60); } catch (e: unknown) { console.error( Error refreshing session:, e instanceof Error ? e : Unknown error, ); await nhost.auth.signOut({ refreshToken: currentSession.refreshToken, }); } }, }; }), fetchExchange, ], });authExchange的三个回调共同构成完整的认证生命周期addAuthToOperation每次请求发送前执行。从nhost.getUserSession()读取当前会话若存在accessToken则通过utils.appendHeaders注入Authorization: Bearer token头未登录时不附加保持匿名请求可用didAuthError响应返回后判断是否为认证错误。这里以 GraphQL 错误信息是否包含JWTExpired为判据——当令牌过期时返回true触发 urql 重新执行认证流程refreshAuth检测到令牌过期后调用。利用nhost.refreshSession(60)请求新的会话参数60表示刷新后希望 access token 的剩余有效期秒数若刷新失败则调用nhost.auth.signOut({ refreshToken })主动登出避免应用停留在无效会话状态。这一机制保证用户登录后所有查询/变更自动携带合法 JWT令牌过期时应用无感刷新刷新失败则安全登出——无需在业务组件中手工处理任何令牌逻辑。第六步组装应用 Provider 树在 main.tsx 中按外层 Auth、内层 urql的顺序包裹应用const Root () ( React.StrictMode AuthProvider UrqlProvider App / /UrqlProvider /AuthProvider /React.StrictMode );顺序有讲究UrqlProvider内部通过useAuth()消费 Nhost 客户端因此必须位于AuthProvider之内。示例的 App.tsx 使用 React Router 8 组织路由并用 ProtectedRoute.tsx 保护受信页面——isLoading时显示加载态未认证时重定向到/signin认证通过后渲染子路由Outlet。登录与注册页面直接调用 Nhost SDK 的认证 API见 SignIn.tsx 与 SignUp.tsx// 登录支持 MFA 分支处理 const response await nhost.auth.signInEmailPassword({ email, password }); if (response.body?.mfa) { navigate(/signin/mfa?ticket${response.body.mfa.ticket}); return; } if (response.body?.session) { navigate(/home); } // 注册options 中携带 displayName注册后自动登录或发送验证邮件 const response await nhost.auth.signUpEmailPassword({ email, password, options: { displayName }, }); if (response.body) { navigate(/home); // 自动登录成功 } else { navigate(/verify); // 需要邮箱验证 }第七步定义 GraphQL 操作文档在src/lib/graphql/queries.graphql见 queries.graphql中定义查询与变更。示例以忍者神龟及其评论为数据模型对应schema.graphql中的ninjaTurtles与comments表两个表通过外键关联comments可反向查询ninjaTurtlequery GetNinjaTurtlesWithComments { ninjaTurtles { id name description createdAt updatedAt comments { id comment createdAt user { id displayName email } } } } mutation AddComment($ninjaTurtleId: uuid!, $comment: String!) { insertComment(object: { ninjaTurtleId: $ninjaTurtleId, comment: $comment }) { id comment createdAt ninjaTurtleId } }该查询同时演示了 Hasura 的嵌套关系查询ninjaTurtles.comments是一对多关系comments.user则关联 Nhost 内置的auth.users表即 Nhost Auth 的默认用户表因此可以一次性取到评论作者信息。第八步生成类型并应用于组件运行生成脚本npm run generate # 或 yarn generate # 或 pnpm generate执行后src/lib/graphql/__generated__/graphql.ts会包含GetNinjaTurtlesWithCommentsDocument与AddCommentDocument两个TypedDocumentNode常量类型、变量、结果类型三者绑定。在组件中直接消费它们即可获得端到端类型安全完整示例见 Home.tsximport { useMutation, useQuery } from urql; import { AddCommentDocument, GetNinjaTurtlesWithCommentsDocument, } from ../lib/graphql/__generated__/graphql; // 查询返回 data / fetching / error 三态 const [{ data, fetching: loading, error }] useQuery({ query: GetNinjaTurtlesWithCommentsDocument, }); // 变更调用返回 Promiseresult.error 为空即成功 const [, addComment] useMutation(AddCommentDocument); const handleAddComment async (turtleId: string) { if (!commentText.trim()) return; const result await addComment({ ninjaTurtleId: turtleId, comment: commentText, }); if (!result.error) { setCommentText(); setActiveCommentId(null); } };体验细节useQuery的fetching表示请求进行中error为 GraphQL 网络/服务端错误示例分别渲染加载态与错误态提交评论成功后才清空输入框、关闭评论编辑区失败则保留用户输入避免误丢内容评论作者优先显示displayName缺失时回退到email再回退为 Anonymous。关键配置注意事项Hasura 兼容性为什么必须preferGetMethod: false这是本示例中最容易被忽视、却直接影响可用性的配置urql v5 默认对查询使用GET 请求便于浏览器 HTTP 缓存复用而 Hasura 会把 GET 请求解释为持久化查询persisted query尝试导致普通查询无法按预期执行设置preferGetMethod: false强制所有操作查询与变更走POST 请求从而与 Hasura 的语义完全兼容。代码注释中明确标注了这一点// Force POST requests (Hasura interprets GET requests as persisted queries)。认证交换器的职责边界urql/exchange-auth在 urql 的 exchange 管道中是一个有状态的认证环节集中负责四件事附加令牌通过addAuthToOperation为每个出站操作注入Authorization头识别认证错误通过didAuthError从 GraphQL 错误中甄别令牌过期JWTExpired自动刷新通过refreshAuth调用nhost.refreshSession换取新令牌并让 urql 重放失败操作失败登出刷新失败时调用nhost.auth.signOut保证客户端状态与服务端会话一致。这套闭环让登录后无感续期成为默认行为业务组件完全不必感知令牌细节。关键特性总结端到端类型安全CodeGen 从远端 schema 生成类型与TypedDocumentNode查询变量、返回结果与组件代码强绑定重构字段时编译期即可发现错误自动令牌管理认证交换器统一注入 JWT、检测过期并自动刷新无需手工处理Authorization头跨标签页会话同步AuthProvider订阅sessionStorage.onChange与页面焦点事件多标签页登录状态保持一致内置文档缓存cacheExchange提供 urql 的文档级缓存同一查询在组件间共享结果并支持响应式失效Hasura 就绪preferGetMethod: false强制 POST规避 Hasura 对 GET 请求的持久化查询解释。运行示例进入 examples/guides/react-urql 目录先启动本地 Nhost 环境nhost dev默认 GraphQL 端点为https://local.graphql.local.nhost.run/v1然后执行pnpm install pnpm generate pnpm dev即可在本地运行演示应用。若连接远程项目通过VITE_NHOST_SUBDOMAIN、VITE_NHOST_REGION、VITE_NHOST_GRAPHQL_URL三个环境变量覆盖默认值并把codegen.ts中的 schema 端点与 admin secret 替换为实际项目配置。【免费下载链接】nhostThe Open Source Firebase Alternative with GraphQL.项目地址: https://gitcode.com/GitHub_Trending/nh/nhost创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考