在 React 中使用 urql 与 Nhost SDK 集成 GraphQL:从认证交换器到类型安全代码生成
2026/9/16 14:14:03 网站建设 项目流程

在 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 SDK

Nhost 是一个开源的 Firebase 替代方案(The Open Source Firebase Alternative with GraphQL),其 GraphQL 层基于 Hasura。React 侧接入 GraphQL 有多种选择,本指南采用的组合是:

  • urql:轻量、可扩展的 GraphQL 客户端,通过 exchange(交换器)管道处理缓存、认证、请求发送等横切关注点;
  • @urql/exchange-auth:专门处理"为每个请求附加令牌、识别认证错误、刷新令牌、失败登出"的认证交换器;
  • @nhost/nhost-js:Nhost 官方 JavaScript SDK,负责管理用户会话(access token / refresh token)与认证流程;
  • GraphQL CodeGen + typed-document-node:从 GraphQL schema 与操作文档生成强类型代码,让查询与变更天然具备端到端类型安全。

从示例的 package.json 可以看到依赖版本组合:urql@^5.0.1@urql/exchange-auth@^3.0.0graphql@^16.11.0@graphql-typed-document-node/core@^3.2.0,而@nhost/nhost-jsworkspace:*形式直接引用本仓库源码,说明这是跟随仓库主线开发的示例。除此之外还使用了 React 19、React Router 8 与 Vite 8 搭建应用骨架。

第一步:安装依赖

在项目根目录执行以下任意一种安装命令(npm / yarn / pnpm 均可,示例仓库本身使用 pnpm):

npm 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.ts):

import 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: "Record<string, 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/v1nhost dev启动的本地 GraphQL 端点,通过x-hasura-admin-secret: nhost-admin-secret(本地默认值)以管理员身份拉取完整 schema;
  • scalars 映射:将 Hasura 特有标量映射为前端可用类型——uuidtimestamptz映射为stringjsonb映射为Record<string, any>bigint映射为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.tsschema.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 开发环境)。会话初始化与同步分为三层:

  1. 初次加载useEffect中调用nhost.getUserSession()读取持久化会话,写入user/session/isAuthenticated状态,并关闭isLoading
  2. 跨标签页同步:通过nhost.sessionStorage.onChange(...)订阅会话变更。底层存储发生更新时(例如另一个标签页完成登录/登出),回调会对比refreshTokenIdlastRefreshTokenIdRef,仅在令牌确实变化时刷新 React 状态,避免无谓重渲染;
  3. 页面焦点一致性:监听visibilitychangewindow 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中的ninjaTurtlescomments表(两个表通过外键关联,comments可反向查询ninjaTurtle):

query 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会包含GetNinjaTurtlesWithCommentsDocumentAddCommentDocument两个TypedDocumentNode常量(类型、变量、结果类型三者绑定)。在组件中直接消费它们即可获得端到端类型安全(完整示例见 Home.tsx):

import { useMutation, useQuery } from "urql"; import { AddCommentDocument, GetNinjaTurtlesWithCommentsDocument, } from "../lib/graphql/__generated__/graphql"; // 查询:返回 data / fetching / error 三态 const [{ data, fetching: loading, error }] = useQuery({ query: GetNinjaTurtlesWithCommentsDocument, }); // 变更:调用返回 Promise,result.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); } };

体验细节:

  • useQueryfetching表示请求进行中,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 管道中是一个有状态的认证环节,集中负责四件事:

  1. 附加令牌:通过addAuthToOperation为每个出站操作注入Authorization头;
  2. 识别认证错误:通过didAuthError从 GraphQL 错误中甄别令牌过期(JWTExpired);
  3. 自动刷新:通过refreshAuth调用nhost.refreshSession换取新令牌,并让 urql 重放失败操作;
  4. 失败登出:刷新失败时调用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_SUBDOMAINVITE_NHOST_REGIONVITE_NHOST_GRAPHQL_URL三个环境变量覆盖默认值,并把codegen.ts中的 schema 端点与 admin secret 替换为实际项目配置。

【免费下载链接】nhostThe Open Source Firebase Alternative with GraphQL.项目地址: https://gitcode.com/GitHub_Trending/nh/nhost

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询