☰
urql 持久化查询(APQ)与文件上传实战:从 Automatic Persisted Queries 到 GraphQL Multipart
2026/9/25 2:17:14 网站建设 项目流程
  • 前端

【免费下载链接】urql

The highly customizable and versatile GraphQL client with which you add on features like normalized caching as you grow.

项目地址:https://gitcode.com/gh_mirrors/ur/urql
点击查看免费下载

本文以urql文档《Persistence & Uploads》为骨架,系统讲解两大进阶能力:基于@urql/exchange-persisted的(自动)持久化查询(Automatic Persisted Queries)如何通过 SHA256 哈希与extensions.persistedQuery实现 CDN 友好缓存与按需注册;以及@urql/core@4原生内置的 GraphQL Multipart 文件上传如何通过FormData序列化自动生效。读完本文,你将掌握persistedExchange的全部配置项与自定义哈希方案,理解 GET/POST 切换与重试降级机制,并能直接在项目中使用File/Blob变量完成文件上传。

概览:为什么要持久化查询与文件上传?

GraphQL 请求默认以 POST 发送,而绝大多数 CDN 与 HTTP 缓存不会缓存 POST 请求,因此每次查询都直达源站。持久化查询(Persisted Queries)通过以哈希代替完整查询文本,让请求体变小、可被 CDN 与 API 层缓存;文件上传则让客户端能够通过 GraphQL API 直接提交二进制文件,而不必绕道对象存储或独立上传接口。

在urql中,这两件事的落地方式完全不同:

  • 持久化查询需要引入@urql/exchange-persisted包,并按照特定顺序插入exchanges数组;
  • 文件上传从@urql/core@4起原生支持,零安装、零配置,只要variables中出现File或Blob实例,请求会自动切换为multipart/form-data。

注:早期文件上传依赖@urql/multipart-fetch-exchange包,该包现已弃用,功能合入@urql/core@4(见 fetchSource.ts 头部注释)。

Automatic Persisted Queries 的工作原理

自动持久化查询(APQ)基于非官方的 GraphQL Persisted Queries Spec。其核心流程如下:

  1. 客户端哈希:客户端把 GraphQL 查询文本转换为 SHA256 哈希,发送哈希而不是完整查询;
  2. 服务端识别:若服务端此前见过该查询,直接按哈希处理请求,流程与常规请求无异;
  3. Miss 重试注册:若服务端不认识该哈希,会返回PersistedQueryNotFound错误。此时客户端应改发「完整查询 + 哈希」的组合,服务端据此"登记"这条查询,后续请求即可命中缓存;
  4. GET 化增强缓存:若只发送哈希过的持久化查询(GET 请求),CDN 就能轻而易举地缓存它们——因为默认情况下大多数缓存不会自动缓存 POST 请求。

在urql中,persistedExchange负责这一切:它位于其他 fetch/subscription 交换器之前,通过修改每个操作的extensions对象,为 GraphQL 请求附加持久化查询元数据。

仓库源码印证

从 persistedExchange.ts 的实现可以看到,每个符合条件的操作都会经过getPersistedOperation:

  • 用makeOperation复制操作并标记persistAttempt: true,防止重复哈希处理;
  • 调用哈希函数对stringifyDocument(operation.query)计算结果;
  • 将结果写入operation.extensions.persistedQuery = { version: 1, sha256Hash }(version: 1即规范约定的版本号);
  • 仅当操作是query类型时才改写context.preferGetMethod,推动 GET 化。

服务端返回后,结果处理逻辑 会识别两类错误:

  • PersistedQueryNotFound:视为一次 Miss,在重试操作上标记persistedQuery.miss: true后通过内部retries流重新 forward;
  • PersistedQueryNotSupported:说明服务端根本不支持持久化查询,此时置supportsPersistedQueries = false彻底关闭后续持久化逻辑,并删除persistedQuery扩展后重试,保证功能平滑降级。

如果同一操作出现两次 Miss,开发环境下会打印警告,提示可能是ssrExchange等带缓存的交换器投递了过期错误结果,建议将persistedExchange移动到fetchExchange之前(源码警告信息)。

安装与基础配置

首先安装@urql/exchange-persisted:

yarn add @urql/exchange-persisted # 或 npm install --save @urql/exchange-persisted

然后将persistedExchange加入exchanges数组,位置必须放在与 API 通信的交换器(如fetchExchange、subscriptionExchange)之前:

import { Client, fetchExchange, cacheExchange } from 'urql'; import { persistedExchange } from '@urql/exchange-persisted'; const client = new Client({ url: 'http://localhost:1234/graphql', exchanges: [ cacheExchange, persistedExchange({ preferGetForPersistedQueries: true, }), fetchExchange, ], });

preferGetForPersistedQueries:GET 与 POST 的策略切换

这是最常用的配置项,推荐设为true,让持久化查询走 GET 请求,从而让 CDN 发挥作用。

  • true或'within-url-limit':当拼接后的 URL不超过 2048 字符时使用 GET(这也是源码中的默认值,见 persistedExchange.ts 的'within-url-limit'默认分支);
  • 'force':强制所有持久化查询使用 GET,即使 URL 超过长度限制(此时查询文本可能被截断,需谨慎使用);
  • false或undefined:保持 POST。

源码中,该值最终被写入operation.context.preferGetMethod(persistedExchange.ts),fetchExchange读取后会在 GET 模式下省略请求体中的query字段。subscriptionExchange同样理解这些修改,如果你用订阅通道承载查询也能协同工作。

与其他交换器的协作

persistedExchange本身不发起网络请求,它只负责改写操作与处理错误重试。fetchExchange看到extensions.persistedQuery后,会按需从请求中剔除query。这也是为什么必须把它放在cacheExchange之后、fetchExchange之前——缓存层不应缓存到带有哈希扩展的中间态。

仓库中的 with-apq 示例 演示了在客户端开启持久化查询后,配合useQuery的正常用法,读者可对照vite.config.js与package.json直接运行体验。

自定义哈希:从 Web Crypto 到编译期哈希

persistedExchange默认使用 SHA256 生成哈希。其降级链路在 sha256.ts 中清晰可见:

  1. 浏览器环境优先使用内置Web Crypto API(window.crypto.subtle.digest);
  2. Node.js 环境回退到Node Crypto 模块(crypto.createHash('sha256')),通过间接require/import加载以避免打包副作用;
  3. 两者都不可用时返回空字符串(开发环境打印警告)。

通过generateHash选项可以完全替换这套逻辑:

persistedExchange({ generateHash: (_, document) => document.documentId, });

上面的写法适合配合Webpack 的graphql-persisted-document-loader使用:哈希在编译期就已由 loader 生成并写入document.documentId,运行时只需直接取用generateHash的第二个参数(GraphQLDocumentNode对象)即可,省去运行时哈希开销。

React Native 场景

React Native 中没有 Web Crypto API,因此必须提供自定义的 SHA256 实现。此时利用generateHash的第一个参数——GraphQL 查询字符串:

import sha256 from 'hash.js/lib/hash/sha/256'; persistedExchange({ async generateHash(query) { return sha256().update(query).digest('hex'); }, });

注意:generateHash若返回null或undefined,该操作将不被当作持久化操作处理,即跳过本交换器的逻辑(见 PersistedExchangeOptions 注释)。这可以用于按操作动态决定是否持久化。

非自动模式:enforcePersistedQueries

如果 API 只接受预注册的持久化查询、拒绝任意查询(常见于 API 混淆/加固场景),可以关闭 APQ 的重试逻辑:

persistedExchange({ enforcePersistedQueries: true, });

启用后,交换器会忽略PersistedQueryNotFound与PersistedQueryNotSupported错误,假设所有持久化查询都已注册,直接把哈希请求当作常规 GraphQL 请求处理(源码中enforcePersistedQueries会跳过重试分支)。

作用于 mutation 与 subscription

默认情况下,persistedExchange只处理query操作。若需对变更和订阅启用持久化(常用于 API 混淆场景),可开启:

persistedExchange({ enableForMutation: true, enableForSubscriptions: true, });

源码中的 operationFilter 按此开关决定哪些kind进入持久化流程。注意:preferGetMethod仅对query生效,mutation 即便持久化也仍走 POST。

File Uploads:@urql/core@4 的原生文件上传

GraphQL 服务端常通过 GraphQL Multipart Request Spec 支持文件上传。urql的用法极其简单:

  • 在variables中直接传入File或Blob对象;
  • 在 GraphQL 文档中为对应变量声明标量(通常叫File或Upload);
  • 浏览器中通常通过文件输入控件(<input type="file">)拿到File对象。

无需任何安装与配置——@urql/core@4原生支持。当urql在variables任意位置检测到File/Blob时,会自动把请求切换为multipart/form-data,按规范构建FormData并发送。

仓库源码印证:extractFiles 与 serializeBody

这一能力由 variables.ts 的extractFiles与 fetchOptions.ts 的serializeBody共同实现:

  • 检测:extractFiles递归遍历variables,通过instanceof FileConstructor || instanceof BlobConstructor识别文件对象(variables.ts),并把文件路径记录为variables.xxx.yyy形式的键;数组按path.0、path.1索引展开;
  • 序列化:serializeBody发现files.size > 0后构造FormData:
    • operations:JSON 序列化后的操作(查询文档 + 变量,文件占位保留);
    • map:JSON 化的路径映射{ "0": ["variables.file"] };
    • 0、1、2…:按序追加的二进制文件内容。

这正是 GraphQL Multipart Request Spec 的规范结构。同时makeFetchOptions(fetchOptions.ts)仅在序列化结果是字符串时才设置content-type: application/json,FormData场景交由 fetch 自动生成multipart/form-data; boundary=...头。

自定义 File/Blob 的注意事项

若你使用自定义版本的File和Blob,务必确保它们正确继承原生类,才能被instanceof识别为文件。extractFiles只在File/Blob构造函数可用时运行(FileConstructor缺省回退为NoopConstructor,见 variables.ts)。

完整示例:with-multipart

仓库的 with-multipart 示例 提供了可直接运行的上传流程:

import React, { useState } from 'react'; import { gql, useMutation } from 'urql'; const UPLOAD_FILE = gql` mutation UploadFile($file: Upload!) { uploadFile(file: $file) { filename } } `; const FileUpload = () => { const [selectedFile, setSelectedFile] = useState(); const [result, uploadFile] = useMutation(UPLOAD_FILE); const { data, fetching, error } = result; const handleFileUpload = () => { uploadFile({ file: selectedFile }); }; const handleFileChange = event => { setSelectedFile(event.target.files[0]); }; return ( <div> {fetching && <p>Loading...</p>} {error && <p>Oh no... {error.message}</p>} {data && data.uploadFile ? ( <p>File uploaded to {data.uploadFile.filename}</p> ) : ( <div> <input type="file" onChange={handleFileChange} /> <button onClick={handleFileUpload}>Upload!</button> </div> )} </div> ); };

要点:event.target.files[0]取到浏览器File对象后,直接放入useMutation的变量{ file: selectedFile }即可,其余全部由@urql/core@4处理。

配置项速查表

配置项类型默认值说明
preferGetForPersistedQueriesboolean \| 'within-url-limit' \| 'force''within-url-limit'持久化查询是否使用 GET;'force'无视 URL 长度强制 GET
generateHash(query: string, document) => Promise<string \| null \| undefined>内置 SHA256自定义哈希函数;返回空值则跳过持久化
enforcePersistedQueriesbooleanfalse启用非自动模式,忽略 APQ 错误、禁用重试
enableForMutationbooleanfalse对 mutation 操作启用持久化
enableForSubscriptionsbooleanfalse对 subscription 操作启用持久化

常见问题与排错

  • GET URL 超长:'within-url-limit'模式下超过 2048 字符会退回 POST;业务查询特别大时建议用'force'前先评估 URL 长度与网关限制。
  • 两次 Miss 警告:开发控制台出现 "two misses for the same operation" 时,优先检查ssrExchange/缓存交换器是否投递了过期错误结果,并把persistedExchange移到它们之后(fetchExchange之前)。
  • React Native 下哈希为空:Web Crypto 不可用,必须通过generateHash提供实现(如hash.js),否则操作不会进入持久化流程。
  • 上传不生效:确认服务端实现了 Multipart Request Spec,且变量确实是原生File/Blob实例——自定义的伪文件类不会被instanceof识别。

延伸阅读

  • 持久化查询交换器文档 与 核心实现、哈希实现 及完整 测试用例
  • 文件上传底层:fetchSource.ts(multipart/mixed 响应解析)、fetchOptions.ts(serializeBody)、variables.ts(extractFiles)
  • 可直接运行的仓库示例:with-apq 与 with-multipart
  • 更进阶的缓存主题参见 graphcache 文档,上传与持久化的组合可在实际项目中与 retryExchange 等交换器叠加使用
  • 前端

【免费下载链接】urql

The highly customizable and versatile GraphQL client with which you add on features like normalized caching as you grow.

项目地址:https://gitcode.com/gh_mirrors/ur/urql
点击查看免费下载
上一篇:ASN 0.80.2:终极网络情报工具,10分钟快速上手指南
下一篇:【亲测免费】 探索生命之树:TreeViewer——跨平台的谱系图绘制神器

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

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

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

立即咨询