Nhost JS SDK GraphQL 客户端实战:从基础查询到类型安全与错误处理
【免费下载链接】nhostThe Open Source Firebase Alternative with GraphQL.项目地址: https://gitcode.com/GitHub_Trending/nh/nhost
Nhost 是一个开源的 Firebase 替代方案(The Open Source Firebase Alternative with GraphQL),其 JavaScript SDK(@nhost/nhost-js)内置了完整的 GraphQL 客户端模块,用于与 Nhost 的 Hasura GraphQL 服务交互。本文以 graphql.md 为核心骨架,系统讲解如何通过nhost.graphql.request执行查询与变更、如何利用 TypeScript 泛型与 GraphQL Document Node 实现端到端类型安全,以及 SDK 的错误处理模型;同时结合仓库源码(graphql/client.ts、fetch/fetch.ts)与测试用例,深入剖析其底层实现原理。读完本文,你将掌握 Nhost GraphQL 客户端的全部核心用法,并能直接在自己的项目中落地实践。
模块概览:GraphQL 客户端在 Nhost SDK 中的位置
@nhost/nhost-js是 Nhost 的官方 JavaScript SDK,负责与 Nhost 后端的各项服务通信。从 nhost.ts 可以看出,SDK 内部将服务拆分为四个独立的客户端:
- auth:认证服务(注册、登录、会话管理)
- storage:文件存储服务
- graphql:Hasura GraphQL 服务(本文主角)
- functions:Serverless 函数服务
这四者统一由createClient(客户端场景)或createServerClient(服务端场景)组装进一个NhostClient实例,其中 GraphQL 客户端以nhost.graphql形式暴露。graphql模块是整个 SDK 中“与业务数据打交道”的核心:你的表数据、视图、权限校验都通过它走 GraphQL 完成。
该模块的入口文件 graphql/index.ts 中明确说明:“这是与 Nhost GraphQL 服务交互的主模块。通常通过主 Nhost 客户端(createClient)使用该模块,但如果你有特定场景,也可以直接使用它。”
直接使用 GraphQL 客户端(独立导入)
模块通过 package.json 中的子路径导出(exports字段)暴露,因此除了通过nhost.graphql使用外,还可以单独引入:
import { createClient } from "@nhost/nhost-js/graphql";如果你需要绕过完整的 Nhost 客户端(例如只需对任意 GraphQL 端点发请求,而不需要认证、存储等功能),可以直接调用模块导出的工厂函数createAPIClient(url, chainFunctions)创建独立客户端,详见后文“深入源码”一节。
基本用法:从 Nhost 客户端发起首次 GraphQL 请求
最常见的用法是创建完整的 Nhost 客户端,然后通过nhost.graphql.request发送查询。request方法提供了完整的 TypeScript 类型支持,可配合泛型标注响应类型,也可使用 GraphQL Document Node 与第三方工具链(如 Apollo Client、GraphQL Code Generator)集成。
最简单的查询如下:
import { createClient } from "@nhost/nhost-js"; const nhost = createClient({ subdomain, region, }); const resp = await nhost.graphql.request({ query: `query GetMovies { movies { id title director genre } }`, });subdomain与region用于构造 GraphQL 服务的基础 URL。查看 nhost.ts 的实现可知,SDK 内部通过generateServiceUrl('graphql', subdomain, region, graphqlUrl)拼接出形如https://<subdomain>.graphql.<region>.nhost.run/v1/graphql的端点;如果你有自定义端点,也可以直接在createClient中传入graphqlUrl(完整 URL,会覆盖 subdomain/region 的组合)。
request返回的响应对象结构为{ body, status, headers }(对应 fetch/fetch.ts 中定义的FetchResponse<T>接口),其中body是标准的GraphQLResponse:包含可选的data与可选的errors。因此查询结果通过resp.body.data?.movies访问。
查询与变更(Query / Mutation)
request方法同时支持查询和变更操作。查询用于获取数据、不应修改服务端数据;变更(mutation)则用于增删改。两者共用同一个request调用,只需把 GraphQL 操作字符串换成 mutation 即可,例如仓库测试 graphql.test.ts 中的变更示例:
const resp = await nhost.graphql.request<UpdateUsersDisplayNameResponse>({ query: `mutation UpdateUsersDisplayName($id: uuid!, $displayName: String!) { updateUser(pk_columns: {id: $id}, _set: {displayName: $displayName}) { id displayName } }`, variables: { id: userID, displayName: 'My New Display Name', }, operationName: 'UpdateUsersDisplayName', });注意这里同时使用了三个字段:query(操作字符串)、variables(参数)、operationName(可选的操作名,当请求字符串中包含多个操作时用于指定执行哪一个)。
使用变量(Variables)
你可以在查询和变更中通过variables选项传递动态参数,使操作更灵活、可复用:
import { createClient } from "@nhost/nhost-js"; const nhost = createClient({ subdomain, region, }); const resp = await nhost.graphql.request({ query: `query GetMovies($genre: String!) { movies(where: {genre: {_eq: $genre}}) { id title director genre } }`, variables: { genre: "Sci-Fi", }, }); console.log(resp.body.data?.movies); // [ // { // id: '3d67a6d0-bfb5-444a-9152-aea543ebd171', // title: 'The Matrix', // director: 'Lana Wachowski, Lilly Wachowski', // genre: 'Sci-Fi' // }, // { // id: '90f374db-16c1-4db5-ba55-643bf38953d3', // title: 'Inception', // director: 'Christopher Nolan', // genre: 'Sci-Fi' // }, // ]变量的类型默认是GraphQLVariables,即Record<string, unknown>(键值对形式)。借助变量,你可以把用户输入、筛选条件等动态值安全地注入到查询中,而无需拼接字符串。
使用字符串查询 + TypeScript 泛型(类型安全的第一步)
你可以通过 TypeScript 泛型为响应数据声明类型,让resp.body.data获得完整的类型推导:
import { createClient } from "@nhost/nhost-js"; const nhost = createClient({ subdomain, region, }); // 这是可选的,但能让你获得类型化的响应 // Apollo Client 或 The Guild 的 GraphQL Code Generator 等工具 // 可以为你生成这些类型与文档节点。 interface Movies { movies: { id: string; title: string; director: string; genre: string; }[]; } const resp = await nhost.graphql.request<Movies>({ query: `query GetMovies { movies { id title director genre } }`, });request<TResponseData, TVariables>的第一个泛型参数TResponseData(默认unknown)控制GraphQLResponse<TResponseData>['data']的类型;第二个泛型参数TVariables(默认GraphQLVariables)控制variables的类型。这样,查询结果与变量都处于类型系统的保护之下。
使用 GraphQL Document Node(第三方工具链集成)
为了与 Apollo Client、The Guild 的 GraphQL Code Generator 等第三方库更好地集成,你可以使用gql模板标签创建 GraphQL Document Node,然后直接传给request:
import { createClient } from "@nhost/nhost-js"; import gql from "graphql-tag"; const nhost = createClient({ subdomain, region, }); interface Movies { movies: { id: string; title: string; director: string; genre: string; }[]; } const getMoviesQuery = gql` query GetMovies($genre: String!) { movies(where: { genre: { _eq: $genre } }) { id title director genre } } `; const resp = await nhost.graphql.request<Movies>(getMoviesQuery, { genre: "Sci-Fi", }); console.log(resp.body.data?.movies);注意此处的调用签名与字符串形式不同:第二个参数直接是variables(而非RequestInit),第三个可选参数才是额外的 fetch 选项。仓库测试 graphql.test.ts 验证了这种调用方式(含变量与不含变量两种形态)。
使用 Document Node 可以带来以下收益:
- 更好的 IDE 支持:语法高亮与校验
- 代码生成工具集成:GraphQL Code Generator 可自动生成类型与文档节点
- 与 Apollo Client 等 GraphQL 库的兼容性:文档节点可直接在这些生态中复用
从源码看,request的重载实现位于 graphql/client.ts:它会判断传入的对象是否带有kind属性(Document Node 的标志),若是则通过extractQueryFromDocument提取查询字符串、从第一个定义中取出操作名作为operationName,再走与字符串形式相同的executeOperation执行路径。
底层实现:extractQueryFromDocument 与片段去重
当使用 Document Node 时,SDK 并非直接序列化 AST,而是借助loc偏移量从原始源码切片重建查询字符串(见 graphql/client.ts)。其行为如下:
- 若文档没有
loc,返回空字符串; - 若定义节点缺少
loc,则回退返回原始源码文本; - 若定义节点均含
loc偏移(典型如 Codegen 输出),则按定义逐段切片并拼接,同时对重复的 Fragment 定义按名称去重(best-effort)。
这一点在接口文档中也有说明:“当文档的定义节点包含loc偏移时,重复的片段定义会以尽力而为的方式去重。”对应的专项测试 graphql-fragment-dedup.test.ts 覆盖了多重嵌套片段去重(如三个片段共同引用同一个Picture片段时只保留一份)、AST 中本身存在重复片段、以及无loc时回退等边界场景。这意味着你可以放心地把 Codegen 生成的大型查询文档交给request,不必担心重复片段导致 Hasura 校验失败。
错误处理(Error Handling)
SDK 的行为是:当 GraphQL 操作返回的响应带有长度大于 0 的errors属性时,抛出异常。异常类型为FetchError<GraphQLResponse>,其中携带包含错误的响应体。
捕获 FetchError 并检查错误体
import { createClient } from "@nhost/nhost-js"; import { FetchError } from "@nhost/nhost-js/fetch"; import type { GraphQLResponse } from "@nhost/nhost-js/graphql"; const nhost = createClient({ subdomain, region, }); try { await nhost.graphql.request({ query: ` query GetRestrictedObject { restrictedObject { restrictedField } } `, }); expect(true).toBe(false); // 不应执行到这里 } catch (error) { if (!(error instanceof FetchError)) { throw error; // 不是 FetchError 则重新抛出 } const resp = error as FetchError<GraphQLResponse>; console.log("Error:", JSON.stringify(resp.body, null, 2)); // Error: { // "body": { // "errors": [ // { // "message": "field 'restrictedObject' not found in type: 'query_root'", // "extensions": { // "path": "$.selectionSet.restrictedObject", // "code": "validation-failed" // } // } // ] // }, // "status": 200, // "headers": {} // } // error handling... }直接记录错误消息
FetchError继承自标准Error类型,因此如果你只想记录错误消息,可以直接使用error.message:
import { createClient } from "@nhost/nhost-js"; import { FetchError } from "@nhost/nhost-js/fetch"; import type { GraphQLResponse } from "@nhost/nhost-js/graphql"; const nhost = createClient({ subdomain, region, }); try { await nhost.graphql.request({ query: ` query GetRestrictedObject { restrictedObject { restrictedField } } `, }); expect(true).toBe(false); // 不应执行到这里 } catch (error) { if (!(error instanceof Error)) { throw error; // 重新抛出非 Error 类型 } console.log("Error:", error.message); // Error: field 'restrictedObject' not found in type: 'query_root' }错误模型源码解析
FetchError定义在 fetch/fetch.ts,它扩展了原生Error,额外携带body(原始响应体)、status(HTTP 状态码)、headers(响应头)三个属性。构造函数通过extractMessage(body)自动从常见错误格式中提取人类可读的消息——包括纯字符串、{ message }、{ error }、{ error: { message } }以及{ errors: [{ message }] }数组(见 fetch/fetch.ts),因此error.message会直接呈现第一条 GraphQL 错误信息。
抛出时机在 graphql/client.ts 的executeOperation中:解析响应 JSON 后,只要data.errors存在就抛出FetchError。注意 GraphQL 错误通常伴随 HTTP 200 返回(如上例中status: 200),因此不能只依赖 HTTP 状态码判断成功与否,必须捕获异常或检查errors字段。仓库测试 graphql.test.ts 对该行为有完整验证(无效查询、权限不足/字段不存在两类场景)。
接口与类型参考(Interfaces & Types)
Client 接口
GraphQL 客户端接口,提供执行查询与变更的方法:
属性
url: string—— GraphQL 端点 URL。
方法
pushChainFunction(chainFunction: ChainFunction): void—— 向 fetch 链添加一个中间件函数(参数chainFunction类型为ChainFunction,详见 fetch 模块)。request()—— 执行 GraphQL 操作,有两种调用签名:
签名一:字符串请求对象
request<TResponseData, TVariables>( request: GraphQLRequest<TVariables>, options?: RequestInit, ): Promise<FetchResponse<GraphQLResponse<TResponseData>>>;执行 GraphQL 查询操作(查询用于获取数据,不应修改服务端数据)。类型参数:TResponseData默认unknown;TVariables默认GraphQLVariables。参数request为包含查询与可选变量的请求对象,options?为额外的 fetch 选项。返回携带 GraphQL 响应与元数据的 Promise。
签名二:TypedDocumentNode
request<TResponseData, TVariables>( document: TypedDocumentNode<TResponseData, TVariables>, variables?: TVariables, options?: RequestInit, ): Promise<FetchResponse<GraphQLResponse<TResponseData>>>;使用类型化文档节点执行 GraphQL 操作。当文档的定义节点包含loc偏移时,重复的片段定义会以尽力而为的方式去重。参数document为携带查询与类型信息的TypedDocumentNode,variables?为操作变量,options?为额外 fetch 选项。
GraphQLError
表示服务端返回的 GraphQL 错误:
| 属性 | 类型 | 说明 |
|---|---|---|
message | string | 错误消息 |
locations? | { column: number; line: number }[] | 错误在 GraphQL 文档中发生的位置 |
path? | string[] | 错误发生的查询路径 |
extensions? | { path: string; code: string } | 特定于 GraphQL 实现(Hasura)的附加错误信息 |
GraphQLRequest
用于查询与变更的 GraphQL 请求对象(泛型TVariables默认GraphQLVariables):
| 属性 | 类型 | 说明 |
|---|---|---|
query | string | GraphQL 查询或变更字符串 |
variables? | TVariables | 参数化查询的可选变量 |
operationName? | string | 可选的要执行的操作名 |
GraphQLResponse
符合 GraphQL 规范的标准响应格式(泛型TResponseData默认unknown):
| 属性 | 类型 | 说明 |
|---|---|---|
data? | TResponseData | 成功执行返回的数据 |
errors? | GraphQLError[] | 执行失败或部分失败时的错误数组 |
GraphQLVariables 类型别名
type GraphQLVariables = Record<string, unknown>;GraphQL 操作的变量对象,即变量名与变量值的键值对。
createAPIClient() 工厂函数
function createAPIClient(url: string, chainFunctions?: ChainFunction[]): Client;创建一个用于与 GraphQL 端点交互的 API 客户端。该客户端提供执行查询与变更的方法,并支持通过中间件函数处理认证、错误处理等横切关注点。
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
url | string | undefined | GraphQL 端点的基础 URL |
chainFunctions | ChainFunction[] | [] | fetch 链的中间件函数数组 |
返回Client—— 带查询与变更方法的 GraphQL 客户端。该工厂函数从 graphql/index.ts 导出,是独立使用 GraphQL 模块(import { createClient } from "@nhost/nhost-js/graphql")时实际创建客户端的底层入口。
深入源码:request 的执行链路与中间件机制
请求执行链路
从 graphql/client.ts 可以看到createAPIClient的实现要点:
- 初始化:用
createEnhancedFetch(chainFunctions)构建增强版 fetch; - 头部处理:
executeOperation中先合并传入的options.headers,若未显式设置Content-Type则自动补为application/json(测试 client.test.ts 验证了自定义Authorization头与 JSON Content-Type 可共存); - 发送请求:以
POST方法、JSON.stringify(request)作为请求体调用增强 fetch; - 解析响应:读取响应文本并
JSON.parse为GraphQLResponse,封装为{ body, status, headers }; - 错误抛出:若
data.errors存在,抛出FetchError。
Fetch 链与中间件
fetch/fetch.ts 定义了中间件模型:ChainFunction = (next: FetchFunction) => FetchFunction,即每个中间件接收“链中的下一个 fetch”并返回包装后的新 fetch;createEnhancedFetch通过reduceRight将中间件按数组顺序依次包裹在原生fetch之外,因此每个中间件既能拦截请求(调用next之前)也能拦截响应(调用next之后)。
Nhost 客户端的认证能力正是构建在这一机制之上。createClient会默认注入withClientSideSessionMiddleware(见 nhost.ts),它由多个中间件组成,其中 middlewareAttachAccessToken.ts 会从会话存储中读取 access token,自动为请求添加Authorization: Bearer <token>头(若请求已带 Authorization 头则跳过)。这意味着:只要用户已登录,nhost.graphql.request发出的请求就会自动携带认证信息,Hasura 据此完成基于角色的行级/字段级权限控制——你无需手动管理 token。
此外,Client接口暴露的pushChainFunction允许你在运行时动态追加中间件(如自定义日志、重试、请求改写),追加后 SDK 会重新构建 fetch 链(graphql/client.ts)。
服务端场景(Server-side)补充说明
除浏览器端外,SDK 还提供createServerClient(见 nhost.ts),适用于 Next.js/Remix 的 Server Component、API Route、中间件等场景。与客户端版本的区别在于:
- 必须显式提供
storage实现(SDK 无法在服务端自动检测存储); - 禁用自动会话刷新中间件(避免服务端并发请求下的竞态问题);
- 仍然会附加 Authorization 令牌并从响应更新会话存储。
nhost.graphql的用法在两种客户端下完全一致,区别仅在于会话如何注入。这为 SSR 应用中使用类型安全的 GraphQL 查询提供了完整支持。
总结与最佳实践
- 日常查询:通过
nhost.graphql.request({ query, variables, operationName })即可完成查询与变更;结果在resp.body.data,错误以异常形式抛出。 - 类型安全:优先使用 TypeScript 泛型
request<Movies>;更进一步,用gql或 GraphQL Code Generator 生成TypedDocumentNode后传给request,可同时获得响应类型、变量类型与 IDE 校验。 - 错误处理:捕获
FetchError<GraphQLResponse>,从error.body.errors读取结构化错误(message/path/extensions.code),或直接用error.message记录日志;不要用 HTTP 状态码判断 GraphQL 成功与否。 - 认证自动注入:借助默认的中间件链,登录状态下的请求自动携带 Bearer Token,无需手工拼接。
- 独立使用:若只需 GraphQL 能力,可通过
@nhost/nhost-js/graphql子路径导入并用createAPIClient创建独立客户端。
相关代码与测试可继续阅读:graphql/client.ts、graphql/index.ts、fetch/fetch.ts、nhost.ts、graphql.test.ts、graphql-fragment-dedup.test.ts。
【免费下载链接】nhostThe Open Source Firebase Alternative with GraphQL.项目地址: https://gitcode.com/GitHub_Trending/nh/nhost
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考