news 2026/9/6 19:06:58

Sentry 前端数据获取实战:TanStack Query 与 apiOptions 的完整使用指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Sentry 前端数据获取实战:TanStack Query 与 apiOptions 的完整使用指南

Sentry 前端数据获取实战:TanStack Query 与 apiOptions 的完整使用指南

【免费下载链接】sentryDeveloper-first error tracking and performance monitoring项目地址: https://gitcode.com/GitHub_Trending/sen/sentry

在 Sentry 的前端代码库(static/目录)中,所有 React 组件调用后端 API 的标准方式是:用apiOptions工厂构造 TanStack Query 的 options 对象,再交给useQuery/useInfiniteQuery消费;写操作则配合fetchMutationuseMutation。读完本文,你将掌握这套数据获取模式的基本用法、条件请求(skipToken)、响应头读取(分页 / 命中计数)、TanStack Query 的类型推断规则,以及apiOptions在源码层的 queryKey 构造、缓存结构与测试验证细节。

为什么是 apiOptions 而不是 useQuery 裸写

Sentry 的 API 端点路径是受类型系统约束的:apiOptions的第二个参数只能是KnownSentryApiUrlsKnownGetsentryApiUrls联合类型中已知的 URL 模板(类型定义见 knownSentryApiUrls.generated.ts 与 knownGetsentryApiUrls.ts)。这意味着端点写错、路径参数缺漏都会在编译期被拦住,而不是等到运行时才 404。

规范文档(SKILL.md)开篇就明确了迁移基线:

使用apiOptions搭配 TanStack Query 的useQuery不要使用useApiQuerygetApiQueryDatasetApiQueryData—— 它们已被废弃。

基本用法与条件请求

最简用法:apiOptions.as<ResponseType>()指定响应体类型,第一个参数是 API 路径模板,第二个参数必须包含staleTime

import {skipToken, useQuery} from '@tanstack/react-query'; import {apiOptions} from 'sentry/utils/api/apiOptions'; // Basic usage const query = useQuery( apiOptions.as<ResponseType>()('/organizations/$organizationIdOrSlug/endpoint/', { path: {organizationIdOrSlug: organization.slug}, staleTime: 30_000, }) );

当请求所依赖的 ID 尚未确定时(例如路由参数还在加载中),不需要手写enabled判断——直接把skipToken作为path传入即可整体禁用该查询:

// Conditional fetching — pass skipToken as path to disable the query const query = useQuery( apiOptions.as<ResponseType>()('/organizations/$organizationIdOrSlug/items/$itemId/', { path: itemId ? {organizationIdOrSlug: organization.slug, itemId} : skipToken, staleTime: 30_000, }) );

从源码看(apiOptions.ts),skipToken的作用机制是:queryFn本身被替换为skipToken,同时enabled: pathParams !== skipToken被设为false,查询既不发请求也不进入缓存污染。测试用例 apiOptions.spec.tsx 验证了这一行为:pathskipToken时,生成的queryKey中 URL 仍保留未替换的$tokenId占位符,而queryFn就是skipToken本身。

四条硬性规则

规范文档将以下规则列为必须遵守的 Key rules:

  1. staleTime是必填项——必须显式选择一个值:0、毫秒数字、Infinity'static'。源码中Options类型定义为QueryKeyEndpointOptions & {staleTime: number | 'static'}(apiOptions.ts),漏掉会在编译期报错;apiOptions.spec.tsx 中有专门的@ts-expect-error测试固化了这个约束。
  2. 基于apiOptions做抽象,而不是基于useQuery做抽象。上层封装应返回 options 对象,让调用方自行选择传给useQueryuseQueriesprefetchQuery等任意入口——这是该 API 设计为“options 工厂”而非 hook 的根本原因。
  3. 缓存里存的是{json, headers},而不只是响应体apiOptions默认用select抽取.json,但getQueryDatasetQueryDataretry函数与predicate回调拿到的都是原始ApiResponse<T>结构。
  4. 永远不要用api.requestPromise作为 Query 的 queryFn——它返回的结构不对。如果必须手写queryFn,请使用apiFetch

类型推断铁律:绝不在调用点传泛型

规范文档用一整节强调:永远不要useQueryuseMutationmutationOptionsqueryOptions等 TanStack Query 函数在调用点传类型参数。类型应当从你的queryFn/mutationFn及其回调自然推断出来。传调用点泛型会让推断失效、掩盖真实 bug,并带来额外维护成本:

// ❌ NEVER pass generics to useQuery, useMutation, mutationOptions, etc. useMutation<ResponseType, RequestError, Variables, Context>({...}) mutationOptions<ResponseType, RequestError, Variables, Context>({...}) useQuery<ResponseType, RequestError>({...}) // ✅ Let types be inferred — annotate the mutationFn/queryFn instead useMutation({ mutationFn: (variables: MyVariables) => fetchMutation<MyResponse>({...}), })

具体展开为五条细则:

  1. mutationFn的参数写类型,而不是给 hook 本身传泛型。variables 类型会从mutationFn的签名流出。
  2. fetchMutation<T>标注返回值——fetchMutation的泛型是正确的,因为它标注的是 API 响应。对照实现(queryClient.tsx),fetchMutation接受{method, url, data?, options?}形式的 variables,内部调用QUERY_API_CLIENT.requestPromise并返回Promise<TResponseData>,可以直接作为mutationFn使用。
  3. 永远不要把 error 泛型写成RequestError——那是一种伪装的类型断言。错误默认就是Error,需要RequestError专有属性时用运行时收窄(if (error instanceof RequestError))。
  4. 永远不要显式标注 context 类型——它由onMutate的返回值推断。单独造一个type FooContext = {...}再当泛型传入是多余动作。
  5. query 侧同理——useQueryqueryOptionsuseInfiniteQuery的类型都从queryFnselect流出。

对比示例,左边是错误写法,右边是全部推断的正确写法:

// ❌ Explicit context type + error assertion type MyContext = {previousData: Item[]}; mutationOptions<Item, RequestError, UpdateItemVars, MyContext>({ mutationFn: variables => fetchMutation({...}), onMutate: async () => { const previousData = queryClient.getQueryData(itemQueryOptions); return {previousData}; }, onError: (_error, _variables, context) => { queryClient.setQueryData(key, context?.previousData); }, }) // ✅ Everything is inferred mutationOptions({ mutationFn: (variables: UpdateItemVars) => fetchMutation<Item>({...}), onMutate: async () => { const previousData = queryClient.getQueryData(itemQueryOptions); return {previousData}; }, onError: (_error, _variables, context) => { // context type is inferred from onMutate return queryClient.setQueryData(key, context?.previousData); }, })

读取响应头:分页 Link 与 X-Hits 计数

默认情况下apiOptionsselect只从响应中抽取 JSON body。如果 UI 需要响应头——比如Link头做游标分页、X-Hits/X-Max-Hits显示总命中数——用selectJsonWithHeaders覆盖select

import {useQuery} from '@tanstack/react-query'; import {apiOptions, selectJsonWithHeaders} from 'sentry/utils/api/apiOptions'; const {data} = useQuery({ ...apiOptions.as<Item[]>()('/organizations/$organizationIdOrSlug/items/', { path: {organizationIdOrSlug: organization.slug}, query: {cursor, per_page: 25}, staleTime: 0, }), select: selectJsonWithHeaders, }); // data is ApiResponse<Item[]> — an object with `json` and `headers` const items = data?.json ?? []; const pageLinks = data?.headers.Link; // string | undefined const totalHits = data?.headers['X-Hits']; // number | undefined const maxHits = data?.headers['X-Max-Hits']; // number | undefined

注意X-HitsX-Max-Hits已经是解析好的number | undefined不需要再parseInt。这一点可以在取数层源码中验证:apiFetch.tsx 里apiFetch调用QUERY_API_CLIENT.requestPromiseincludeAllArgs: true以拿到原始Response对象),随后对响应头做了类型收敛:

'X-Hits': typeof hits === 'string' ? Number(hits) : undefined, 'X-Max-Hits': typeof maxHits === 'string' ? Number(maxHits) : undefined,

对应的ApiResponse<T>类型也在这里定义,headers只暴露LinkX-HitsX-Max-HitsX-Sentry-Direct-Hit四个字段(apiFetch.tsx)。apiOptions.spec.tsx 中的 “should extract headers” 测试进一步用expectTypeOf固化了这一点:mock 响应头'X-Hits': '14'经查询后data.headers['X-Hits']是数值14

源码透视:apiOptions 到底做了什么

apiOptions.ts 的核心是_apiOptions工厂,它返回标准 TanStack QueryqueryOptions,结构上有四个关键点:

return queryOptions({ queryKey: [url, strippedOptions, {infinite: false}] as const, queryFn: pathParams === skipToken ? skipToken : apiFetch<TActualData>, enabled: pathParams !== skipToken, staleTime, select: selectJson, });
  • queryKey 是三段式[最终 URL, 去除 undefined 后的请求选项, {infinite: false}]。URL 由getApiUrl(getApiUrl.ts)从路径模板 +path参数渲染而来。{infinite: false}标志让同一端点的普通查询与无限查询天然拥有不同 key,互不污染缓存。
  • stripUndefinedValues递归剔除 undefined(apiOptions.ts)。这样query: {cursor: undefined, per_page: 25}query: {per_page: 25}会命中同一个缓存槽位——选项里没有定义过的字段不会改变 queryKey 身份。apiOptions.spec.tsx 的 “strips undefined top-level/deep values” 测试覆盖了顶层和深层两种情形。
  • 默认select: selectJson(data: ApiResponse<TData>) => data.json,所以useQuery拿到的data直接是响应体;而缓存本体始终保留完整ApiResponse<T>,这正是 Key rule 第 3 条的来源。
  • as<TManualData>是手动数据类型逃生口。源码注释里有一条todo: infer the actual data type from the ApiMapping,说明类型目前由as<>手动指定(TActualData = TManualData),这也是为什么文档要求调用时总是写apiOptions.as<ResponseType>()

无限滚动:asInfinite 变体

对于游标分页列表,apiOptions提供了asInfinite入口(apiOptions.ts),它返回infiniteQueryOptions,且分页方向完全由响应头驱动:

getPreviousPageParam: parsePageParam('previous'), getNextPageParam: parsePageParam('next'), initialPageParam: undefined,

parsePageParamApiResponse.headers.Link里解析 RFC 5988 风格的 Link 头,取previous/next对应的results作为下一页游标,取不到就返回null停止翻页。取数则由 apiFetch.tsx 的apiFetchInfinite完成,它会把context.pageParam?.cursor注入query.cursor。同文件中的useFetchAllPageshook(apiFetch.tsx)则封装了“hasNextPage时自动fetchNextPage()”的拉全量场景。

测试用例给出的行为边界

apiOptions.spec.tsx 除了上文提到的用例外,还固化了若干容易踩坑的行为:

  • 路径参数编码version: 'v 1.0.0'会渲染为/organizations/my-org/releases/v%201.0.0/(URL 编码自动完成);
  • 数字参数自动字符串化path: {tokenId: 123}得到/api-tokens/123/
  • 不做意外前缀替换:模板'$id1/$id'传入{id: '123', id1: '456'}时只替换完整的$id1$id,不会把$id1误当成$id的前缀;
  • 无参端点不允许传path/api-tokens/上写path: {}会触发类型错误({path?: never}约束,见 apiOptions.ts);
  • 未知端点默认neverapiOptions.as<never>()('/unknown/$param/')会因路径不在KnownApiUrls联合中而报编译错误——这是“端点拼写错误即刻暴露”的实现基础。

实践清单

在 Sentry 前端新增或修改 API 调用时,可按以下清单自检:

场景正确做法反模式
读数据useQuery(apiOptions.as<T>()(path, {path, staleTime}))直接api.requestPromise塞进queryFn
依赖未就绪path: id ? {...} : skipToken手写enabled: !!id
需要Link/X-Hits覆盖select: selectJsonWithHeaders对 header 手动parseInt
游标无限滚动apiOptions.asInfinite<T>()+useInfiniteQuery手动维护 cursor 状态
写数据useMutation({mutationFn: (v: Vars) => fetchMutation<T>(...)})useMutation传调用点泛型
复用返回 options 对象,基于apiOptions建抽象基于useQuery包一层 hook
错误收窄if (error instanceof RequestError)error 泛型写死RequestError

所有规则的第一手依据可对照 SKILL.md,实现与行为验证分别在 apiOptions.ts、apiFetch.tsx、queryClient.tsx 与 apiOptions.spec.tsx 中。

【免费下载链接】sentryDeveloper-first error tracking and performance monitoring项目地址: https://gitcode.com/GitHub_Trending/sen/sentry

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/6 19:06:23

猫抓 Cat-Catch 三步上手:浏览器资源嗅探与 m3u8 流媒体捕获实战

猫抓 Cat-Catch 三步上手&#xff1a;浏览器资源嗅探与 m3u8 流媒体捕获实战 【免费下载链接】cat-catch 猫抓 浏览器资源嗅探扩展 / cat-catch Browser Resource Sniffing Extension 项目地址: https://gitcode.com/GitHub_Trending/ca/cat-catch 猫抓 Cat-Catch 是一款…

作者头像 李华
网站建设 2026/9/6 19:03:09

ONNX Runtime 部署排错指南:从装到跑通、跑快的实战清单

ONNX Runtime 部署排错指南&#xff1a;从装到跑通、跑快的实战清单 【免费下载链接】onnxruntime ONNX Runtime: cross-platform, high performance ML inferencing and training accelerator 项目地址: https://gitcode.com/GitHub_Trending/on/onnxruntime ONNX Runt…

作者头像 李华