ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

urql 自动持久化查询(APQ)实战指南:基于 @urql/exchange-persisted 的 persistedExchange 配置与原理

urql 自动持久化查询(APQ)实战指南:基于 @urql/exchange-persisted 的 persistedExchange 配置与原理 前端【免费下载链接】urqlThe 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点击查看免费下载本指南以仓库中 with-apq 示例 为主线讲解如何用urql/exchange-persisted的persistedExchange为 urql 客户端启用 Automatic Persisted Queries自动持久化查询。读完本文你将掌握APQ 的完整工作流程、persistedExchange的全部配置项及其源码级行为、请求体/URL 的底层生成逻辑以及如何基于 with-apq 示例快速搭建可运行项目。一、什么是 Automatic Persisted QueriesAutomatic Persisted QueriesAPQ是 GraphQL 社区的一种事实标准传输优化方案客户端第一次发送请求时携带完整的query文本并附带一个基于查询内容计算的哈希sha256Hash服务端将该查询与哈希缓存之后所有请求都可以省略query文本只发送sha256Hash服务端根据哈希直接返回缓存好的查询结果。APQ 的核心收益有两点显著缩小请求体积长查询文本不再随每次请求传输尤其对移动端、弱网场景收益明显便于 CDN 与 HTTP 缓存持久化请求天然适合走 GET 方法从而更容易被缓存命中详见下文的preferGetForPersistedQueries配置。persistedExchange专门负责实现这套流程它为请求注入persistedQuery扩展字段并自动处理服务端返回的两种错误——PersistedQueryNotFound哈希未命中需要回退重发完整查询与PersistedQueryNotSupported服务端不支持持久化查询之后自动停用该功能。注意这里的 APQ 是运行时自动协商的动态方案与预先在构建期持久化全部查询的 Persisted Queries 不同。persistedExchange同样支持后者只需开启enforcePersistedQueries见下文配置详解。二、示例项目结构速览仓库中的 with-apq 示例 是一个基于 Vite React 的最小可运行应用文件构成如下src/App.jsx创建 urqlClient在exchanges中装配persistedExchange与fetchExchange并通过Provider注入组件树src/LocationsList.jsx使用useQuery发起Locations查询的列表组件src/index.jsxReact 应用入口使用createRoot挂载Appindex.html承载应用根节点#root的 HTML 模板vite.config.js启用vitejs/plugin-react的最小 Vite 配置package.json声明urql/core、urql/exchange-persisted、graphql、react、react-dom、urql等依赖。示例使用公开的 APQ 天气演示端点https://trygql.formidable.dev/graphql/apq-weather无需自建服务端即可验证 APQ 行为。三、运行示例在 examples/with-apq 目录下安装依赖并启动开发服务器即可yarn install yarn run start # 或使用 npm npm install npm run startstart脚本对应vite命令启动后 Vite 会在本地开启开发服务器并在浏览器中打开应用。页面中LocationsList组件会执行带变量{ query: LON }的查询渲染加载态、错误信息或匹配的城市列表。如果你只想快速浏览代码而非本地运行也可以直接查看示例的三个核心文件App.jsx、LocationsList.jsx 与 index.jsx。四、核心代码解析客户端如何装配 persistedExchange示例 App.jsx 中客户端配置如下这是启用 APQ 最关键的几行代码import React from react; import { Client, Provider, fetchExchange } from urql; import { persistedExchange } from urql/exchange-persisted; import LocationsList from ./LocationsList; const client new Client({ url: https://trygql.formidable.dev/graphql/apq-weather, exchanges: [ persistedExchange({ preferGetForPersistedQueries: true, }), fetchExchange, ], }); function App() { return ( Provider value{client} LocationsList / /Provider ); } export default App;要点如下persistedExchange必须放在终止型 exchange如fetchExchange之前。它本身不发送请求而是为后续 exchange 的请求注入persistedQuery扩展并拦截/重放结果persistedExchange.ts 源码明确说明它adds support for Persisted Queries to anyfetchExchangeor other API exchanges following it。示例中省略了cacheExchange直接让persistedExchange紧邻fetchExchange。若引入缓存型 exchange应保持persistedExchange位于缓存之后、终止 exchange 之前避免缓存层把未命中重试的陈旧结果缓存下来。preferGetForPersistedQueries: true表示持久化查询在 URL 长度允许时改用 GET 请求细节见下文。客户端创建后通过Provider value{client}注入LocationsList里用useQuery消费import React from react; import { gql, useQuery } from urql; const LOCATIONS_QUERY gql query Locations($query: String!) { locations(query: $query) { id name } } ; const LocationsList () { const [result] useQuery({ query: LOCATIONS_QUERY, variables: { query: LON }, }); const { data, fetching, error } result; return ( div {fetching pLoading.../p} {error pOh no... {error.message}/p} {data ( ul {data.locations.map(location ( li key{location.id}{location.name}/li ))} /ul )} /div ); }; export default LocationsList;组件解构出fetching、error、data三态分别渲染加载提示、错误信息与结果列表key{location.id}保证列表更新正确。五、APQ 底层工作流程源码级persistedExchange的实现位于 exchanges/persisted/src/persistedExchange.ts核心逻辑分为请求改造与结果处理两段。5.1 请求改造注入 persistedQuery 扩展对每个进入的 operationgetPersistedOperation会先基于原操作复制一份带persistAttempt: true上下文标记的新操作然后调用哈希函数const sha256Hash await hashFn( stringifyDocument(operation.query), operation.query ); if (sha256Hash) { persistedOperation.extensions { ...persistedOperation.extensions, persistedQuery: { version: 1, sha256Hash, }, }; if (persistedOperation.kind query) { persistedOperation.context.preferGetMethod preferGetForPersistedQueries; } }关键行为哈希基于序列化后的文档stringifyDocument的输出保证相同语义的查询得到稳定哈希extensions.persistedQuery遵循 APQ 惯例version: 1sha256Hash对应 types.ts 中定义的PersistedRequestExtensions类型只有query操作会被设置preferGetMethodmutation 与 subscription 即使开启相应开关也不会强制 GET若哈希函数返回null/undefined该操作不会被当作持久化操作处理直接跳过本 exchange 的逻辑。5.2 结果处理未命中与不支持的回退结果流经map时若返回结果带extensions.persistedQuery且包含错误会分两种情形处理均在!enforcePersistedQueries前提下PersistedQueryNotSupported服务端明确不支持持久化查询。此时 exchange 将supportsPersistedQueries置为false后续操作全部走普通请求并删掉extensions.persistedQuery后重放一次原始操作PersistedQueryNotFound服务端缓存未命中该哈希。exchange 构造一个带persistedQuery.miss: true标记的跟进操作并重新发送——miss标记会促使fetchExchange在本次请求中同时携带完整query文本从而让服务端完成查询注册对应 fetchOptions.ts 中!!request.extensions.persistedQuery.miss时仍保留query的分支。错误判定通过检查error.graphQLErrors中是否存在PersistedQueryNotFound/PersistedQueryNotSupported消息完成const isPersistedMiss (error: CombinedError): boolean error.graphQLErrors.some(x x.message PersistedQueryNotFound); const isPersistedUnsupported (error: CombinedError): boolean error.graphQLErrors.some(x x.message PersistedQueryNotSupported);值得注意的边界当persistedQuery.miss已为true却仍收到未命中错误即第二次 miss时说明上游缓存层可能回放了陈旧的错误结果exchange 会在非生产环境打印一条包含 two misses 的警告该行为也被 persistedExchange.test.ts 中的测试用例覆盖。这是把persistedExchange误放到缓存类 exchange 之前的典型症状此时应调整其位置到fetchExchange紧前方。5.3 竞态与卸载保护persistedOps$通过takeUntil监听对应 operation 的teardown事件一旦组件卸载或查询被取消正在进行的哈希计算与请求会被及时终止避免无效结果回流见源码takeUntil(pipe(operations$, filter(op op.kind teardown op.key operation.key)))。六、配置项详解persistedExchange(options?: PersistedExchangeOptions)接受一个可选配置对象各字段在 persistedExchange.ts 中有完整 JSDoc 定义6.1 preferGetForPersistedQueries类型OperationContext[preferGetMethod]即boolean | force | within-url-limit默认值within-url-limit作用控制持久化查询是否使用 GET 请求。GET 请求便于 CDN 缓存但 URL 有长度上限。示例设置为true等价于URL 不超过 2048 字符时用 GET。具体判定逻辑位于 fetchOptions.ts 的makeFetchURLif (finalUrl.length 2047 useGETMethod ! force) { operation.context.preferGetMethod false; return operation.context.url; }即 URL 超过 2047 字符时自动回退为 POST只有force会无视长度强制 GET。测试 persistedExchange.test.ts 用it.each([true, false, force, within-url-limit])验证了这四种取值都会原样写入operation.context.preferGetMethod。6.2 enforcePersistedQueries类型boolean默认false作用关闭自动协商机制。开启后exchange 忽略PersistedQueryNotFound与PersistedQueryNotSupported错误假定所有持久化查询服务端都已预置。这适用于把 APQ 切换为构建期生成的 Persisted Queries常用于对 API 做查询文本混淆/隐藏的场景。6.3 generateHash类型(query: string, document: TypedDocumentNode) Promisestring | undefined | null默认实现内置 SHA-256 哈希见 sha256.ts。浏览器环境优先使用 WebCryptowindow.crypto.subtle.digestNode 环境通过间接require(crypto)/import(crypto)使用createHash(sha256)两者都不可用时会打印警告并返回空字符串。用法若在构建期用 loader 生成哈希如graphql-persisted-document-loader可传入(_, document) document.documentId直接复用构建产物避免运行时重复计算。注意返回值若为null/undefined该操作会跳过持久化逻辑有专门测试覆盖见 persistedExchange.test.ts 的 skips operation when generateHash returns a nullish value。平台注意React Native 等缺少 WebCrypto 的环境需要传入自定义哈希函数。6.4 enableForMutation 与 enableForSubscriptions类型boolean默认均false作用默认仅query走持久化分别开启后 mutation / subscription 操作也会进入持久化流程operationFilter中的分支判断。常配合enforcePersistedQueries用于对 GraphQL API 做查询混淆的封闭场景。七、fetchExchange 侧如何消费持久化请求persistedExchange只负责贴标签真正把extensions.persistedQuery序列化为请求的是核心包的 fetchOptions.ts。makeFetchBody的判定逻辑if ( documentId in request.query request.query.documentId (!request.query.definitions || !request.query.definitions.length) ) { body.documentId request.query.documentId; } else if ( !request.extensions || !request.extensions.persistedQuery || !!request.extensions.persistedQuery.miss ) { body.query stringifyDocument(request.query); }含义正常情况下无persistedQuery扩展或扩展存在但未 miss请求体省略query字段只携带operationName、variables、extensions.persistedQuery当miss: trueAPQ 首次注册时请求体重新包含完整query文本服务端借此建立哈希与查询的映射若查询文档自带documentId构建期持久化方案则直接走documentId通道。同时makeFetchOptions会依据preferGetMethod决定method: GET还是POST并把extensions等字段拼入 URL 查询参数GET 场景这部分逻辑同样位于 fetchOptions.ts。八、常见问题与排查建议服务端未命中后页面出现两次请求这是 APQ 的正常协商过程第一次带哈希注册、第二次带查询属于预期行为不应视为错误。控制台出现 two misses 警告说明persistedExchange位于某个会缓存结果的 exchange如ssrExchange、缓存类 exchange之后陈旧错误被回放。请把它移到这些 exchange 之后、fetchExchange之前的位置。持久化查询没有走 GET检查preferGetForPersistedQueries取值URL 超过 2047 字符时默认会回退为 POST除非显式设置force。React Native 上报哈希相关错误默认哈希依赖 WebCrypto在缺少该 API 的平台请通过generateHash提供自定义实现。无法确认服务端是否支持 APQ可以先只加persistedExchange并用网络面板观察首次请求的extensions.persistedQuery与服务端响应若返回PersistedQueryNotSupportedexchange 会自动降级为普通请求功能不受影响。九、延伸阅读persistence-and-uploads.md官方文档中关于持久化查询与文件上传的完整章节exchanges/persisted/README.mdurql/exchange-persisted包自身的快速上手说明persistedExchange.tspersistedExchange完整实现源码persistedExchange.test.ts覆盖未命中重试、哈希跳过、preferGetMethod取值等行为的测试用例sha256.ts默认 SHA-256 哈希的浏览器/Node 双实现fetchOptions.ts请求体与 URL 的序列化逻辑含 2047 字符限制与miss分支with-apq 示例本文对应的完整可运行示例。赞分享前端【免费下载链接】urqlThe 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点击查看免费下载相关推荐micro-github部署指南用Now.sh一键上线你的GitHub认证服务micro github部署指南用Now.sh一键上线你的GitHub认证服务 micro github是一个轻量级微服务能帮助开发者轻松为应用添加GitH前端urql 持久化查询实战指南urql/exchange-persisted 的安装、配置与底层原理urql 持久化查询实战指南urql/exchange persisted 的安装、配置与底层原理 urql/exchange persisted 是 u前端urql 持久化查询APQ与文件上传实战从 Automatic Persisted Queries 到 GraphQL Multiparturql 持久化查询APQ与文件上传实战从 Automatic Persisted Queries 到 GraphQL Multipart 本文以 urq前端上一篇ComfyUI-Impact-Pack V8技术深度解析5大核心模块解锁AI图像增强专业级应用下一篇Vin象棋如何用AI视觉识别技术5分钟打造你的免费象棋大师助手创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表