- 前端
【免费下载链接】urql
The highly customizable and versatile GraphQL client with which you add on features like normalized caching as you grow.
@urql/exchange-populate是 urql 生态中用于自动填充 Mutation 选择集(selection set)的官方 exchange。本文以 exchanges/populate/README.md 为主线,结合仓库源码与测试用例,系统讲解populateExchange的安装配置、@populate指令用法、可调参数(maxDepth、skipType)以及底层实现原理,帮助你在项目从 documentCache 向 Graphcache 演进时,不再手动维护 Mutation 返回值中的每个字段。
一、为什么需要自动填充 Mutation 选择集
在 urql 的缓存体系里,Mutation 返回什么字段,直接决定缓存能否被更新。以 文档缓存(document caching) 为例:当应用某处执行了
# Query 1 { todos { id name } } # Query 2 { todos { id createdAt } }之后,如果新增一个 Todo 的 Mutation 想同时刷新上述两个查询,就必须手动把两个查询里出现的字段全部写进 Mutation 的返回选择集:
# 不使用 populate 的写法 mutation addTodo(id: ID!) { addTodo(id: $id) { id # 更新 Query 1 & 2 name # 更新 Query 1 createdAt # 更新 Query 2 } }随着应用规模增长,追踪"哪些查询请求过哪些字段"会越来越困难——这正是populateExchange要解决的问题。根据 docs/advanced/auto-populate-mutations.md 的说明,该 exchange 与 Graphcache 配合使用时尤其有价值:它能在 Mutation 之后自动把此前查询观察过的字段补全,从而让缓存数据自动保持最新。
二、安装与快速开始
1. 安装依赖
populateExchange由独立的@urql/exchange-populate包提供,需要与urql(或@urql/core)一同安装:
yarn add @urql/exchange-populate # 或 npm install --save @urql/exchange-populate从 exchanges/populate/package.json 可以看到,该包以@urql/core和wonka为依赖,并要求项目自行安装graphql(^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0)与@urql/core(^6.0.0)作为 peer 依赖。
2. 注册 exchange
将populateExchange加入createClient的exchanges数组即可:
import { createClient, cacheExchange, fetchExchange } from 'urql'; import { populateExchange } from '@urql/exchange-populate'; const client = createClient({ url: 'http://localhost:1234/graphql', exchanges: [populateExchange({ schema }), cacheExchange, fetchExchange], });关键点:populateExchange必须放在cacheExchange之前。原因有二(见 docs/advanced/auto-populate-mutations.md):
cacheExchange(尤其是 Graphcache)本身不认识@populate指令,需要先由populateExchange将其从文档中移除并替换为真实字段;- 放在缓存前面可以避免不必要的重复工作,让进入缓存层的操作已经是"最终形态"。
3. 获取 schema 数据
populateExchange的schema选项是后端 GraphQL Schema 的 introspection(内省)结果,其类型为IntrospectionQuery(见 populateExchange.ts 的类型定义)。获取方式可参考 docs/graphcache/schema-awareness.md 中介绍的标准流程:
import { getIntrospectionQuery } from 'graphql'; import fetch from 'node-fetch'; // 或 Node.js 环境下你偏好的请求库 import * as fs from 'fs'; fetch('http://localhost:3000/graphql', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ variables: {}, query: getIntrospectionQuery({ descriptions: false }), }), }) .then(result => result.json()) .then(({ data }) => { fs.writeFile('./schema.json', JSON.stringify(data), err => { if (err) { console.error('Writing failed:', err); return; } console.log('Schema written!'); }); });对于体积较大的内省结果,还可以使用@urql/introspection包的minifyIntrospectionQuery进行瘦身(详见 docs/graphcache/schema-awareness.md),该包与 populate 一样位于本仓库的 packages/introspection 目录中。
三、核心用法:@populate指令
注册完成后,Mutation 里只需给字段加上@populate指令,exchange 就会自动补全此前所有查询中"观察"过的字段:
# 使用 populate 的写法 mutation addTodo(id: ID!) { addTodo(id: $id) @populate }Note:上面两种 Mutation 最终发出的 GraphQL 请求完全一致。
换句话说,@populate只是"占位符",在操作真正离开populateExchange之前,它已经被展开为与手动写法等价的完整选择集——这一点在 populateExchange.test.ts 的快照测试中得到印证:当查询过todos { id text creator { id name } }后,addTodo @populate会被展开为
mutation MyMutation { addTodo { __typename id text creator { __typename id name } } }注意展开结果中还自动插入了__typename字段——这是为了让 Graphcache 能够确定实体的具体类型,是 populate 展开时默认附带的。
四、精细控制:选择何时填充、如何限制填充
1. 将@populate放到更深层级
如果不想填充整个 Mutation 响应(以减小 payload),可以把@populate放在更靠下的字段上(见 docs/advanced/auto-populate-mutations.md):
mutation addTodo(id: ID!) { addTodo(id: $id) { id user @populate } }此时只有user子选择集会被自动补全,外层的id仍由你手动声明。这与源码中"对每个带@populate指令的字段单独展开"的处理逻辑一致:handleIncomingMutation会遍历文档,逐字段检查是否带有populate指令,命中才展开(见 populateExchange.ts)。
2. 通过options限制填充范围
populateExchange支持第二个参数options(见 populateExchange.ts),包含两个配置项:
| 配置项 | 类型 | 默认值 | 作用 |
|---|---|---|---|
skipType | RegExp | /^PageInfo\|(Connection\|Edge)$/ | 匹配到该正则的类型名不会被自动填充字段,默认跳过 Relay 分页相关类型 |
maxDepth | number | 2 | 填充的最大嵌套深度,防止无限递归或字段过多 |
示例:
populateExchange({ schema, options: { maxDepth: 3, skipType: /Todo/, }, });从源码看,这两个选项的默认值在创建 exchange 时被解析:const maxDepth = (options && options.maxDepth) || 2;与const skipType = (options && options.skipType) || SKIP_COUNT_TYPE;(populateExchange.ts)。SKIP_COUNT_TYPE即/^PageInfo|(Connection|Edge)$/(同文件第 82 行),用于在默认情况下不展开 Relay 风格的PageInfo、Connection、Edge类型。
在 populateExchange.test.ts 中,maxDepth: 1时查询company { id employees { id todos { id } } }的展开结果只保留两层(company → employees),而employees下的todos不再展开;skipType: /User/时,User类型会被跳过、继续向更深的todos展开——这与直觉相反的行为正是"跳过指定类型"的含义。
五、使用别名合并带变量的查询
当多个查询对同一字段使用了不同的变量时,需要借助 GraphQL 别名(aliases)才能让字段被正确合并(见 docs/advanced/auto-populate-mutations.md)。
无效用法——同一字段todos带不同参数,字段键冲突,无法正确归并:
# Query 1 { todos(first: 10) { id name } } # Query 2 { todos(last: 20) { id createdAt } }配合别名的用法——用firstTodos/lastTodos区分开:
# Query 1 { firstTodos: todos(first: 10) { id name } } # Query 2 { lastTodos: todos(last: 20) { id createdAt } }Note:官方文档指出,这一限制未来可能被放宽或移除。
从源码可以理解这一限制的成因:readFromSelectionSet在记录字段时会以字段名:序列化后的参数作为键存入typeFields(见 populateExchange.ts)。不同参数会生成不同的字段键,而别名则天然避免了同名冲突,让 exchange 能准确区分每一次字段观察。
六、工作原理:从"观察查询"到"展开 Mutation"
结合 populateExchange.ts 的源码,可以梳理出 exchange 的三条数据流(其管线实现见 第 470-478 行):
监听查询(
handleIncomingQuery):每当应用发起查询,exchange 记录该 operation key,并通过readFromSelectionSet沿选择集递归,把每个字段及其所属类型、参数、子选择集存入内存中的typeFieldsMap(第 428-460 行)。同一文档中的 fragment 定义也会被收集进userFragments以便后续解析(第 447-451 行)。处理 teardown(
handleIncomingTeardown):查询被销毁时从活跃集合中移除。源码注释明确指出,当前不会据此删除已记录的字段,以避免缓存数据过期(第 462-468 行)。展开 Mutation(
handleIncomingMutation):当操作是mutation且字段带有@populate指令时,先移除指令本身,再根据字段的返回类型在typeFields中查找之前观察到的字段并递归补全(第 132-343 行)。
展开过程中的几个关键细节:
- 抽象类型处理:如果 Mutation 返回的是接口(interface)或联合(union)类型,exchange 会为每个可能的实现类型生成带
typeCondition的内联片段(inline fragment),并为每个片段注入__typename(第 181-241 行)。测试 populateExchange.test.ts 验证了removeTodo: [Node](接口)与updateTodo: [UnionType](联合)的展开结果。 - 标量字段直接展开,对象字段递归展开:对
GraphQLScalarType字段直接生成为普通字段节点;对对象类型字段,在depth < maxDepth且未访问过该类型时递归填充子选择集(第 243-318 行)。 - 参数保留:查询中带参数的字段(如
createdAt(timezone: "GMT+1"))会被原样记录并回填到 Mutation 中,测试用例对此有快照验证(populateExchange.test.ts)。 - Fragment 支持:查询中通过 fragment spread 观察到的字段同样会被展开(populateExchange.test.ts),而未使用的 fragment 不会被带入 Mutation(同文件第 365-440 行)。
- 无记录可查时:如果某个返回类型从未被查询观察过,exchange 至少会补一个
__typename字段,保证缓存仍有可用的实体标识(populateExchange.ts)。
七、实验性状态与已知限制
需要说明的是,populateExchange目前仍处于experimental(实验性)阶段(见 docs/advanced/auto-populate-mutations.md 开头的说明):例如 GraphQL 字段参数(field arguments)等部分用法尚未被完整覆盖,exchange 也尚未经过大规模生产环境验证。使用时建议:
- 在集成测试中对
@populate展开后的请求做快照断言(本仓库的 populateExchange.test.ts 提供了完整的参考写法,可对照print(response.query)验证展开结果); - 通过
maxDepth、skipType控制展开规模,避免过度填充; - 在从 documentCache 向 Graphcache 迁移的过渡阶段,用它作为"桥梁"逐步减轻手动维护 Mutation 选择集的负担。
八、小结
populateExchange为 urql 开发者提供了一种声明式、可自动维护的 Mutation 写法:只需在字段上加一个@populate指令,exchange 便会基于此前所有查询的"观察记录"自动补全选择集,并处理好__typename、抽象类型内联片段、参数保留、深度与类型限制等细节。配合 Graphcache 使用时,它显著降低了"Mutation 之后缓存数据过时"的风险。相关实现与测试均可在本仓库的 exchanges/populate/src 目录中继续研读。
- 前端
【免费下载链接】urql
The highly customizable and versatile GraphQL client with which you add on features like normalized caching as you grow.
相关推荐
LangExtract 自定义输出 Schema(output_schema)深度指南:用 JSON Schema 锁定结构化提取结果
LangExtract 自定义输出 Schema(output_schema)深度指南:用 JSON Schema 锁定结构化提取结果 导读 lx.extrac
前端使用ChartJs.Blazor解决Blazor应用数据可视化难题的完整方案
使用ChartJs.Blazor解决Blazor应用数据可视化难题的完整方案 在当今数据驱动的Web开发领域,Blazor开发者面临着一个核心挑战:如何在.NE
urql批量操作优化:使用populateExchange自动填充关联数据
urql批量操作优化:使用populateExchange自动填充关联数据 在开发GraphQL应用时,你是否还在为手动编写冗长的mutation查询而烦恼?是
前端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考