news 2026/9/11 18:26:01

Refine v5 GraphQL 数据提供者集成指南:从客户端搭建到实时订阅与错误治理

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Refine v5 GraphQL 数据提供者集成指南:从客户端搭建到实时订阅与错误治理

Refine v5 GraphQL 数据提供者集成指南:从客户端搭建到实时订阅与错误治理

【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine

导读

本文以 Refine 仓库中的 GraphQL 集成文档为主体,系统讲解@refinedev/graphql数据提供者的完整接入方式:从安装依赖、创建 urql 客户端、通过meta传递gqlQuery/gqlMutation,到自定义dataMapperbuildVariables适配任意 GraphQL 响应结构,再到利用createLiveProvider实现基于订阅的实时数据流,以及错误分类、重试策略与鉴权集成。读完本文,你将能够在一个 Refine v5 应用中完整落地 GraphQL 后端(本文示例为 nestjs-query 风格的 API),并具备应对响应结构差异、网络抖动和认证需求的实战能力。

一、概览:@refinedev/graphql是什么

@refinedev/graphql是 Refine 官方提供的 GraphQL 数据提供者包,它的设计目标正如其文档所述:在不牺牲 GraphQL 特性(嵌套查询、字段选择、订阅、类型系统)的前提下,获得 Refine 全部的数据能力(CRUD、分页、排序、过滤、实时订阅等)。

这个包由三部分构成,对应 packages/graphql/src/index.ts 的导出结构:

  • createDataProvider(client)—— 数据提供者工厂,基于 @urql/core 的Client实例实现 Refine 的 10 个 data provider 方法(createcreateManygetOnegetListgetManyupdateupdateManydeleteOnedeleteManycustom);
  • createLiveProvider(wsClient)—— 实时提供者工厂,基于graphql-ws的 WebSocket 客户端,通过 GraphQL subscription 驱动 Refine 的实时特性;
  • 选项(Options)系统—— 每个 data provider 方法都可被深度定制(见下文“Options”一节)。

使用前需要明确三点约定(文档的 Good to know 部分):

  1. 数据提供者期望你传入一个@urql/coreClient实例
  2. 负责在meta中提供gqlQuerygqlMutation,可以用@urql/core导出的gql编写 GraphQL 操作;
  3. 更系统的数据获取指南参见仓库的 Data Fetching 指南。

二、安装与基础接入

2.1 安装依赖

按文档给出的安装命令,需要同时安装三个包:

npm install @refinedev/graphql @urql/core graphql-ws # 或使用 pnpm pnpm add @refinedev/graphql @urql/core graphql-ws

三者角色不同:@refinedev/graphql提供数据提供者与实时提供者;@urql/core是执行查询/变更的 GraphQL 客户端;graphql-ws则是基于 WebSocket 的订阅传输层。从 packages/graphql/package.json 可以看到,运行时依赖还包括camelcase(资源名转驼峰)、pluralize(单复数转换)、graphql-tag(解析操作文档)、deepmerge(合并默认选项)与lodash/set(构造嵌套过滤条件);@refinedev/coregraphql-ws是 peerDependencies,需要由使用方显式安装。

2.2 创建客户端并挂载数据提供者

文档给出了最小可运行示例:用Client创建 GraphQL 客户端,传入 API URL 与fetchExchange,再交给createDataProvider

import Refine from "@refinedev/core"; import { Client, fetchExchange } from "@urql/core"; import createDataProvider from "@refinedev/graphql"; export const API_URL = "https://api.nestjs-query.refine.dev/graphql"; const gqlClient = new Client({ url: API_URL, exchanges: [fetchExchange] }); const dataProvider = createDataProvider(gqlClient); const App = () => <Refine dataProvider={dataProvider}>{/* ... */}</Refine>;

仓库中的完整示例 examples/data-provider-graphql/src/App.tsx 在此基础上还同时配置了liveProvider、antd 主题、路由与资源定义,并暴露了client以便在页面中复用。@urql/corefetchExchange负责把操作通过 HTTPfetch发出,这是生产环境最常用的交换器;如果你的客户端需要缓存、持久化等能力,可以按 urql 的机制自行扩展exchanges数组。

三、Options:让响应结构与你的 API 对齐

3.1 为什么需要 Options

GraphQL 的查询字段由调用方决定,因此同一个资源在不同 API 中的返回结构千差万别。例如文档中的示例查询:

query PostList($where: JSON, $sort: String) { allBlogPosts(where: $where, sort: $sort) { nodes { id title content category { id } } } }

这里列表字段叫allBlogPosts,而 Refine 的资源名是blogPosts。默认的getList实现会直接读取response.data?.[params.resource].nodes(见 packages/graphql/src/dataProvider/options.ts 中getList.dataMapper),遇到这种命名差异就必须自行改造。过去这要求“swizzle”整个数据提供者,而现在只需传第二个参数。

3.2 第二个参数:按方法覆盖的配置对象

createDataProvider的第二个参数是一个对象,每个字段对应一个 data provider 方法getListupdateMany等),每个方法内部又可以覆盖若干“构建片段”。文档强调:

所有字段都是可选的,传入的字段会被深度合并进默认选项,因此你只需覆盖想要定制的方法,其余方法回落到默认实现。

这一行为在 packages/graphql/src/dataProvider/index.ts 中可以看到:createDataProviderdeepmerge(defaultOptions, baseOptions)合并配置。底层ActionMethod的形态(packages/graphql/src/dataProvider/options.ts)为:

type ActionMethod = { dataMapper: ( response: OperationResult<any>, params: GetOneParams | GetListParams | etc, ) => {} | []; buildVariables: (params: CreateParams | UpdateParams | etc) => {}; };
  • dataMapper:从 urql 的OperationResult响应中提取 Refine 需要的数据;
  • buildVariables:把 Refine 的参数(params)转换成 GraphQL 操作所需的变量对象。

3.3 实战:自定义getList.dataMapper

文档给出了一个非常典型的场景——将响应中的allBlogPosts字段提取出来:

import dataProvider, { GraphQLClient, defaultGetDataFn, } from "@refinedev/graphql"; import camelCase from "camelcase"; const client = /** client init **/ const dataProvider = createDataProvider(client, { getList: { dataMapper: (response: OperationResult<any>, params: GetListParams) => { // resource: blogPosts const operationName = `all${camelcase(resource, {pascal: true})}` // operationName: allBlogPosts return response.data?.[operationName].nodes; }, } })

这样无需任何 swizzle 操作,getList就能正确读取allBlogPosts.nodes作为列表数据,其余方法(getOnecreate等)继续使用默认行为。

3.4 两个特殊方法:convertMutationToQuerygetTotalCount

除了通用的dataMapper/buildVariables,文档特别指出了两个增强点:

  • getOne.convertMutationToQuerygetOne除了被useOne使用,还会被useForm消费。编辑场景下useForm可能只提供了gqlMutation而没有gqlQuery,此时需要把变更操作“转换”成查询,才能回填初始数据。默认实现位于 packages/graphql/src/dataProvider/options.ts:它通过getOperationFields(见 packages/graphql/src/utils/graphql.ts)从原操作中提取字段选择集,再按query GetPost($id: ID!) { post(id: $id) { ...fields } }的形态生成查询;
  • getList.getTotalCount:用于从列表查询响应中提取总数(默认读取totalCount字段),该值会作为 Refine 列表页total供分页组件使用。

3.5 默认选项全览

为了让你清楚每个方法默认做了什么,下面给出 packages/graphql/src/dataProvider/options.ts 中defaultOptions的完整结构(方法名与可覆盖的构建片段):

export const defaultOptions = { create: { dataMapper: (response, params) => response.data?.[`createOne${PascalSingular(resource)}`], buildVariables: (params) => ({ input: { [singular(resource)]: params.variables ?? params?.meta?.gqlVariables } }), }, createMany: { dataMapper: (response, params) => response.data?.[`createMany${Pascal(resource)}`], buildVariables: (params) => ({ input: { [camelcase(resource)]: params.variables ?? params?.meta?.gqlVariables } }), }, getOne: { dataMapper: (response, params) => response.data?.[camelcase(singular(resource))], buildVariables: (params) => ({ id: params.id, ...params.meta?.gqlVariables }), convertMutationToQuery: (params) => { /* 见 3.4 节 */ }, }, getList: { dataMapper: (response, params) => response.data?.[params.resource].nodes, getTotalCount: (response, params) => response.data?.[params.resource].totalCount, buildVariables: (params) => ({ sorting: buildSorters(params.sorters), filter: buildFilters(params.filters), paging: buildPagination(params.pagination), ...params.meta?.variables, ...params.meta?.gqlVariables, }), }, getMany: { buildFilter: (params) => ({ filter: { id: { in: params.ids } } }), dataMapper: (response, params) => response.data?.[camelcase(resource)].nodes, }, update: { dataMapper: (response, params) => response.data?.[`updateOne${PascalSingular(resource)}`], buildVariables: (params) => ({ input: { id: params.id, update: params.variables, ...params.meta?.gqlVariables } }), }, updateMany: { dataMapper: (_response, params) => params.ids.map((id) => ({ id })), buildVariables: (params) => ({ input: { filter: { id: { in: ids } }, update: variables, ...params.meta?.gqlVariables } }), }, deleteOne: { dataMapper: (response, params) => response.data?.[`deleteOne${PascalSingular(resource)}`], buildVariables: (params) => ({ input: { id: params.id, ...params?.meta?.gqlVariables } }), }, deleteMany: { dataMapper: (_response, params) => params.ids.map((id) => ({ id })), buildVariables: (params) => ({ input: { filter: { id: { in: ids } }, ...params.meta?.gqlVariables } }), }, custom: { dataMapper: (response, params) => response.data ?? response.error?.message, buildVariables: (params) => ({ ...params.payload, ...params?.meta?.variables, ...params?.meta?.gqlVariables }), }, };

从这份默认实现可以总结出几个对排错极有帮助的规律:

  1. 命名约定:列表默认按资源名的复数形式(response.data[params.resource].nodes/.totalCount)读取;单项操作按“操作动词 + PascalCase 单数资源名”读取(如createOnePostupdateOnePostdeleteOnePost),这是典型的 nestjs-query 风格命名;
  2. gqlVariables的合并getOnegetListgetManyupdateupdateManydeleteOnedeleteManycustom的默认buildVariables都会把params.meta?.gqlVariables并入变量对象,这是向操作注入额外参数(如自定义字段)的统一通道;
  3. create/update的数据源:优先使用params.variables,回退到meta.gqlVariables

3.6 排序、过滤与分页的转换实现

getList默认依赖三个工具函数(见 packages/graphql/src/utils/getListHelpers.ts),理解它们有助于你确认前端行为与后端期望是否一致:

  • buildSorters:把 Refine 的CrudSort[]映射为{ field, direction }order统一转为大写(ASC/DESC);
  • buildPagination:分页模式为off时返回{ limit: 2147483647 }(等效“不分页”);否则计算{ limit: pageSize, offset: (currentPage - 1) * pageSize },默认pageSize为 10、currentPage为 1;
  • buildFilters:把 Refine 的CrudFilter[]转换为嵌套过滤对象。其中操作符映射值得注意——eq/ne/lt/gt/lte/gte/in/nin直接映射为eq/neq/lt/gt/lte/gte/in/notIncontains/startswith/endswith等字符串操作符映射为iLike/notILike(不区分大小写)或like/notLike(区分大小写),并用%包裹或拼接值;null/nnull映射为is: null/isNot: nullbetween/nbetween要求长度为 2 的数组,否则抛出明确错误;and/or条件会被递归构建。

这些转换逻辑在每个方法的测试用例中都有对应验证,例如 packages/graphql/test/getList/getList.spec.ts 覆盖了排序、过滤、分页组合场景,可作为你自定义选项时的行为参照。

四、Queries 与 Mutations:通过meta传递操作

4.1 编写操作与挂载 meta

数据提供者本身并不知道你要查询哪些字段——这由你在meta中显式声明。文档推荐的最佳实践是把查询/变更与使用它的组件放在一起

import gql from "graphql-tag"; const POSTS_LIST_QUERY = gql` query PostList($where: JSON, $sort: String) { posts(where: $where, sort: $sort) { id title content category { id } } } `; const POST_CREATE_MUTATION = gql` mutation createPost($input: createPostInput!) { createPost(input: $input) { id title content category { id } } } `;

注意:示例中queries.tsgraphql-tag导入gql,而文档概览部分提到@urql/core也导出了gql,二者都能把模板字符串解析为 GraphQLDocumentNode,可按项目依赖任选其一。

然后在 hook 的meta中引用:

import { useList } from "@refinedev/core"; import { POSTS_LIST_QUERY } from "./queries"; export const PostListPage () => { const { result } = useList({ resource: "posts", // highlight-next-line meta: { gqlQuery: POSTS_LIST_QUERY }, }); const data = result.data; return ( <div> {/* ... */} </div> ); }
import { useForm } from "@refinedev/core"; import { POST_CREATE_MUTATION } from "./queries"; export const PostCreatePage () => { const { formProps } = useForm({ resource: "posts", // highlight-next-line meta: { gqlMutation: POST_CREATE_MUTATION }, }); return ( <div> {/* ... */} </div> ); }

4.2 底层如何选择操作

从 packages/graphql/src/dataProvider/index.ts 的实现可以看出各方法的操作选择策略:

  • getList/getMany:只接受meta.gqlQuery,缺失时抛出[Code] Operation is required.
  • updateMany/deleteOne/deleteMany:只接受meta.gqlMutation
  • create/createMany/getOne/update/customgqlMutation ?? gqlQuery取其一;其中getOne在拿到 mutation 时还会走convertMutationToQuery转换为查询(见 3.4 节)。

这解释了为何useForm编辑页只需给gqlMutation也能回填数据:getOne会先把它转成查询执行。

4.3 错误处理:三类错误前缀

GraphQL 场景下错误来自两个层面:网络错误与 API 返回的 GraphQL 错误。urql 用CombinedError统一承载这两类错误,而 Refine GraphQL 数据提供者会识别它们并加上带前缀的错误信息,方便调用方区分处理。文档归纳为三类:

  • [Code]代码错误:必需的参数(如 Query 或 Mutation)未提供;
  • [Network]网络错误:任何阻止网络请求发生的错误;
  • [GraphQL]GraphQL 错误:API 返回的errors数组中的错误,被拼接为字符串。

这些错误会通过 Notification provider(如果配置了)或 Refine 的错误处理器呈现给用户。这个分类在 packages/graphql/src/dataProvider/index.ts 的errorHandler中可见:有networkError时拼[Network] ...,有graphQLErrors时拼[GraphQL] ...,代码缺失时抛出的则是[Code] Operation is required.;此外getApiUrl未实现,也会抛出[Code] Not implemented on refine-graphql data provider.

4.4 管理重试:按错误类型精确控制

Refine 底层使用 TanStack Query,默认会对失败的 API 调用重试 3 次再向用户展示错误。但[Code][GraphQL]类错误重试毫无意义(重试只会重复同样失败),只有[Network]错误值得重试。文档给出的方案是通过数据获取 hook 的queryOptions传入自定义retry函数:

useList({ meta: { gqlQuery: GET_LIST_QUERY, }, // highlight-start queryOptions: { retry(failureCount, error) { // failureCount provides the number of times the request has failed // error is the error thrown from the GraphQL provider return error?.message.includes("[Network]") && failureCount <= 3; }, }, // highlight-end });

这样只有网络错误会触发重试(最多 3 次),逻辑类错误立即暴露,减少无谓请求。

五、Realtime:GraphQL 订阅驱动的实时数据

5.1 创建 live provider

@refinedev/graphql还导出createLiveProvider,它接收一个graphql-ws创建的 WebSocket 客户端,并在内部生成订阅操作:

import Refine from "@refinedev/core"; import { createLiveProvider } from "@refinedev/graphql"; import createClient from "graphql-ws"; const WSS_URL = "wss://api.nestjs-query.refine.dev/graphql"; const wsClient = createClient({ url: WSS_URL }); const liveProvider = createLiveProvider(wsClient); const App = () => ( <Refine // highlight-next-line liveProvider={liveProvider} options={{ liveMode: "auto" }} > {/* ... */} </Refine> );

liveMode设为auto后,订阅由 Refine 自动管理:列表页挂载时订阅创建/更新/删除事件,数据变化时自动刷新。

5.2 底层订阅生成逻辑

从 packages/graphql/src/liveProvider/index.ts 可以看到subscribe的实现:params中必须提供resourcesubscriptionTypemeta必须存在,否则抛出明确错误。随后:

  • subscriptionType === "useList"时,对同一资源建立created / updated / deleted 三个订阅
  • subscriptionType === "useOne"时,只建立updated订阅(单项只关心更新)。

实际订阅文本由 packages/graphql/src/liveProvider/helpers.ts 中的生成器拼装,例如:

subscription CreatedPost($input: CreatePostSubscriptionFilterInput) { createdPost(input: $input) { # 复用 gqlQuery / gqlMutation 中的字段选择集 } } subscription UpdatedPost($input: UpdateOnePostSubscriptionFilterInput) { updatedOnePost(input: $input) { # ... } } subscription DeletedPost($input: DeleteOnePostSubscriptionFilterInput) { deletedOnePost(input: $input) { id } }

生成的订阅操作名/字段名遵循Created/Updated/Deleted + PascalCase 单数资源名的 nestjs-query 约定;字段选择集通过getOperationFields从你提供的meta.gqlQuery(或gqlMutation)中复用,因此实时推送的字段与你列表页查询的字段天然一致。过滤条件则由buildFilters转换后放入input.filter,且会排除包含.的嵌套字段。

仓库示例 examples/data-provider-graphql/src/App.tsx 中liveProvider={createLiveProvider(createClient({ url: WS_URL }))}的用法即上述流程的完整落地。

六、Authentication:携带令牌的两种方式

6.1 最简单的方式:fetchOptions

如果 API 需要鉴权,文档推荐通过 urqlClientfetchOptions在请求头中注入令牌:

import createDataProvider from "@refinedev/graphql"; import { Client, fetchExchange } from "urql"; export const client = new Client({ url: API_URL, exchanges: [fetchExchange], fetchOptions: () => { return { headers: { /** * For demo purposes, we're using `localStorage` to access the token. * You can use your own authentication logic here. * In real world applications, you'll need to handle it in sync with your `authProvider`. */ // highlight-next-line Authorization: `Bearer ${localStorage.getItem("token")}`, }, }; }, }); /** * Create the data provider with the custom client. */ const dataProvider = graphqlDataProvider(client);

这里把fetchOptions写成函数,保证每次请求都读取最新令牌;文档同时提醒,生产环境中令牌的读写应与你的authProvider保持同步(例如登录/登出时统一更新)。

6.2 进阶方式:urqlauthExchange

如果鉴权需求更复杂(如令牌刷新、并发请求排队重放),可以使用 urql 生态的authExchange进行流程化的鉴权管理。它允许你在请求发出前检查并附加令牌、在收到 401 时触发刷新流程并重放请求,属于比手写fetchOptions更完善的方案,适合对接 OAuth 等动态令牌场景。

七、与 Inferencer 的配合

@refinedev/inferencer可以根据数据提供者的响应自动生成视图代码与预览。由于 GraphQL 数据提供者依赖meta字段(gqlQuery/gqlMutation),使用 Inferencer 时需先提供这些meta值,Inferencer 再据此推断响应字段、生成代码并渲染预览。更完整的说明参见 Inferencer 文档 中关于 GraphQL 后端与meta值的章节。

八、配套示例与测试

8.1 可运行的完整示例

文档末尾给出的示例即仓库中的 examples/data-provider-graphql 项目。它演示了完整链路:antd 主题 + react-router 路由 +createDataProvider+createLiveProvider,资源为blogPostscategories,API 为https://api.nestjs-query.refine.dev/graphql(WebSocket 为wss://api.nestjs-query.refine.dev/graphql),是验证本文所述全部概念的推荐起点。

8.2 测试覆盖

该包的测试位于 packages/graphql/test,为每个 data provider 方法(createcreateManygetListgetOnegetManyupdateupdateManydeleteOnedeleteManycustom)都提供了.spec.ts与对应的.mock.ts,外加 options.spec.ts 验证选项合并逻辑。阅读这些测试可以确认:默认dataMapper的字段名约定、排序/过滤/分页的转换结果,以及自定义 Options 覆盖后的行为——它们是你在自定义数据提供者时最可靠的“行为契约”参考。

结语

在 Refine v5 中接入 GraphQL 后端的完整路径可以概括为四步:@urql/core创建客户端并交给createDataProvidermeta中显式提供gqlQuery/gqlMutation用第二个参数按方法定制dataMapper/buildVariables以适配响应结构按需叠加createLiveProvider、错误重试与鉴权配置。把握好本文梳理的默认命名约定、三类错误前缀与实时订阅生成规则,你就能把任意风格的 GraphQL API 平稳地嵌入 Refine 的应用框架中,兼顾类型安全、实时性与可维护性。

【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine

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

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

Spark ML在TB级销售预测中的实践与优化

1. 项目背景与目标去年接手公司销售预测任务时&#xff0c;我面临一个典型的数据科学难题&#xff1a;如何利用历史销售数据构建可靠的预测模型。传统Excel表格和简单线性回归已经无法满足业务需求&#xff0c;我们需要处理的是TB级的交易记录、数百个SKU以及复杂的季节性因素。…

作者头像 李华
网站建设 2026/9/11 18:25:09

永磁同步电机离线辨识原理与仿真建模实践

简介&#xff1a;这是一份面向电机控制工程师与电气专业学生的永磁同步电机离线辨识仿真模型资源。离线辨识是获取电阻、电感等关键参数、支撑矢量控制与直接转矩控制策略的重要手段&#xff0c;资源提供了从模型搭建、参数估计到结果验证的完整工具链。压缩包共5个文件&#x…

作者头像 李华
网站建设 2026/9/11 18:24:55

电销外包按通话分钟计费还是按线索计费

可核验行业规范及2026年市场实测数据重要合规提示&#xff1a;电销外包业务需要遵守通信管理相关法规&#xff0c;严禁骚扰呼叫&#xff0c;服务商必须具备对应电信业务经营许可资质&#xff0c;企业合作前务必核验服务商资质。本评测为第三方客观评测&#xff0c;评测对象为【…

作者头像 李华
网站建设 2026/9/11 18:21:12

分布式光伏储能系统双层优化设计与工程实践

1. 项目背景与核心挑战分布式光伏储能系统作为新型电力系统的重要组成部分&#xff0c;正面临配置优化与运行策略协同设计的难题。我在参与某工业园区微电网项目时&#xff0c;深刻体会到传统单层优化模型难以兼顾投资经济性与运行可靠性的痛点。当光伏渗透率超过30%时&#xf…

作者头像 李华