ARTICLE DETAIL

资讯详情

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

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

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到自定义dataMapper与buildVariables适配任意 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 方法create、createMany、getOne、getList、getMany、update、updateMany、deleteOne、deleteMany、customcreateLiveProvider(wsClient)—— 实时提供者工厂基于graphql-ws的 WebSocket 客户端通过 GraphQL subscription 驱动 Refine 的实时特性选项Options系统—— 每个 data provider 方法都可被深度定制见下文“Options”一节。使用前需要明确三点约定文档的 Good to know 部分数据提供者期望你传入一个urql/core的Client实例你负责在meta中提供gqlQuery和gqlMutation可以用urql/core导出的gql编写 GraphQL 操作更系统的数据获取指南参见仓库的 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/core与graphql-ws是 peerDependencies需要由使用方显式安装。2.2 创建客户端并挂载数据提供者文档给出了最小可运行示例用Client创建 GraphQL 客户端传入 API URL 与fetchExchange再交给createDataProviderimport 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/core的fetchExchange负责把操作通过 HTTPfetch发出这是生产环境最常用的交换器如果你的客户端需要缓存、持久化等能力可以按 urql 的机制自行扩展exchanges数组。三、Options让响应结构与你的 API 对齐3.1 为什么需要 OptionsGraphQL 的查询字段由调用方决定因此同一个资源在不同 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 方法getList、updateMany等每个方法内部又可以覆盖若干“构建片段”。文档强调所有字段都是可选的传入的字段会被深度合并进默认选项因此你只需覆盖想要定制的方法其余方法回落到默认实现。这一行为在 packages/graphql/src/dataProvider/index.ts 中可以看到createDataProvider用deepmerge(defaultOptions, baseOptions)合并配置。底层ActionMethod的形态packages/graphql/src/dataProvider/options.ts为type ActionMethod { dataMapper: ( response: OperationResultany, 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: OperationResultany, params: GetListParams) { // resource: blogPosts const operationName all${camelcase(resource, {pascal: true})} // operationName: allBlogPosts return response.data?.[operationName].nodes; }, } })这样无需任何 swizzle 操作getList就能正确读取allBlogPosts.nodes作为列表数据其余方法getOne、create等继续使用默认行为。3.4 两个特殊方法convertMutationToQuery与getTotalCount除了通用的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 }), }, };从这份默认实现可以总结出几个对排错极有帮助的规律命名约定列表默认按资源名的复数形式response.data[params.resource].nodes/.totalCount读取单项操作按“操作动词 PascalCase 单数资源名”读取如createOnePost、updateOnePost、deleteOnePost这是典型的 nestjs-query 风格命名gqlVariables的合并getOne、getList、getMany、update、updateMany、deleteOne、deleteMany、custom的默认buildVariables都会把params.meta?.gqlVariables并入变量对象这是向操作注入额外参数如自定义字段的统一通道create/update的数据源优先使用params.variables回退到meta.gqlVariables。3.6 排序、过滤与分页的转换实现getList默认依赖三个工具函数见 packages/graphql/src/utils/getListHelpers.ts理解它们有助于你确认前端行为与后端期望是否一致buildSorters把 Refine 的CrudSort[]映射为{ field, direction }order统一转为大写ASC/DESCbuildPagination分页模式为off时返回{ limit: 2147483647 }等效“不分页”否则计算{ limit: pageSize, offset: (currentPage - 1) * pageSize }默认pageSize为 10、currentPage为 1buildFilters把 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.ts从graphql-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.gqlMutationcreate/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 次逻辑类错误立即暴露减少无谓请求。五、RealtimeGraphQL 订阅驱动的实时数据5.1 创建 live providerrefinedev/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中必须提供resource与subscriptionTypemeta必须存在否则抛出明确错误。随后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 需要鉴权文档推荐通过 urqlClient的fetchOptions在请求头中注入令牌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, were using localStorage to access the token. * You can use your own authentication logic here. * In real world applications, youll 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 路由 createDataProvidercreateLiveProvider资源为blogPosts与categoriesAPI 为https://api.nestjs-query.refine.dev/graphqlWebSocket 为wss://api.nestjs-query.refine.dev/graphql是验证本文所述全部概念的推荐起点。8.2 测试覆盖该包的测试位于 packages/graphql/test为每个 data provider 方法create、createMany、getList、getOne、getMany、update、updateMany、deleteOne、deleteMany、custom都提供了.spec.ts与对应的.mock.ts外加 options.spec.ts 验证选项合并逻辑。阅读这些测试可以确认默认dataMapper的字段名约定、排序/过滤/分页的转换结果以及自定义 Options 覆盖后的行为——它们是你在自定义数据提供者时最可靠的“行为契约”参考。结语在 Refine v5 中接入 GraphQL 后端的完整路径可以概括为四步用urql/core创建客户端并交给createDataProvider、在meta中显式提供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),仅供参考
返回列表