- 前端
【免费下载链接】urql
The highly customizable and versatile GraphQL client with which you add on features like normalized caching as you grow.
本文以
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。其核心流程如下:
- 客户端哈希:客户端把 GraphQL 查询文本转换为 SHA256 哈希,发送哈希而不是完整查询;
- 服务端识别:若服务端此前见过该查询,直接按哈希处理请求,流程与常规请求无异;
- Miss 重试注册:若服务端不认识该哈希,会返回
PersistedQueryNotFound错误。此时客户端应改发「完整查询 + 哈希」的组合,服务端据此"登记"这条查询,后续请求即可命中缓存; - 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 中清晰可见:
- 浏览器环境优先使用内置Web Crypto API(
window.crypto.subtle.digest); - Node.js 环境回退到Node Crypto 模块(
crypto.createHash('sha256')),通过间接require/import加载以避免打包副作用; - 两者都不可用时返回空字符串(开发环境打印警告)。
通过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处理。
配置项速查表
| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
preferGetForPersistedQueries | boolean \| 'within-url-limit' \| 'force' | 'within-url-limit' | 持久化查询是否使用 GET;'force'无视 URL 长度强制 GET |
generateHash | (query: string, document) => Promise<string \| null \| undefined> | 内置 SHA256 | 自定义哈希函数;返回空值则跳过持久化 |
enforcePersistedQueries | boolean | false | 启用非自动模式,忽略 APQ 错误、禁用重试 |
enableForMutation | boolean | false | 对 mutation 操作启用持久化 |
enableForSubscriptions | boolean | false | 对 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.
相关推荐
micro-github部署指南:用Now.sh一键上线你的GitHub认证服务
micro github部署指南:用Now.sh一键上线你的GitHub认证服务 micro github是一个轻量级微服务,能帮助开发者轻松为应用添加GitH
前端Relay 持久化查询(Persisted Queries)实战指南:从 persistConfig 配置到服务端执行
Relay 持久化查询(Persisted Queries)实战指南:从 persistConfig 配置到服务端执行 Relay 编译器内置对持久化查询(Pe
前端开发工具urql 持久化查询实战指南:@urql/exchange-persisted 的安装、配置与底层原理
urql 持久化查询实战指南:@urql/exchange persisted 的安装、配置与底层原理 @urql/exchange persisted 是 u
前端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考