查询层模式Skill:把 TanStack Query、错误转换与依赖注入打包成一套可复用的前端数据桥梁
核心结论速览
- 它是什么:查询层模式(query-layer)是一套「架构型」AI 技能,教 AI 助手按统一范式,在 UI 组件与服务层之间搭出一层反应式桥梁,技术栈是 TanStack Query + WellCrafted 工厂 + 运行时依赖注入。
- 它解决什么问题:服务层的
TaggedError直接漏到界面、缓存与乐观更新各写各的、切换服务实现要改一堆组件——这些「数据层脏活」被统一收敛到查询层。 - 它的差异化:明确给出「三层错误流」「双重接口(
.options反应式 /.execute()命令式)」「查询键层级化」「乐观更新」四套可直接照抄的写法,并附反模式提醒(禁止双重包装错误)。 - 谁最需要它:使用 Svelte + TanStack Query、正在做「服务层 / 查询层 / UI 层」分层的开发者或团队,尤其是需要多服务商切换(如 OpenAI / Groq)的项目。
- 上手成本:概念门槛中等(需理解 Result 类型、访问器函数、查询键),但一旦落地,新增一个查询或突变基本是「填空式」操作。
该技能叫什么?如何下载安装 AI 技能提升开发效率?
基本信息
| 项目 | 内容 |
|---|---|
| 技能中文名 | 查询层模式 |
| 技能英文名 | query-layer |
| 技能 ID | 11868 |
| 作者 | epicenter |
| 版本 | 1.1 |
| 定位 | 前端架构模式 / 数据层设计技能 |
它到底解决什么问题
在中大型前端项目里,UI 组件和服务层之间往往缺一层「正经的胶水」:组件直接 await services.xxx(),于是缓存、重试、加载态、错误提示、乐观更新全散落在各个组件里;服务返回的是领域错误,界面需要的是「能直接弹吐司的错误」;用户一改设置里的服务商(OpenAI 换成 Groq),调用它的十几个组件都得跟着改。
查询层模式技能就是把这层胶水标准化:它让 AI 助手在生成代码时,自动遵循「UI → 查询层 → 服务层」的固定架构,产出带缓存键、带错误转换、带依赖注入的查询与突变。
一键安装 AI 技能提升开发效率的思路
所谓「一键安装」,本质是把一套团队约定好的架构规范,变成 AI 助手随时可调用的上下文,从此不用每次在对话里重复贴几百行范式代码。
安装前提(通用流程,具体以官方说明为准):
- 你使用的 AI 编程工具需支持「技能 / Skill」机制,具备技能市场或本地技能目录的加载能力;
- 本地项目已具备可写入的技能目录,且你对项目有写权限;
- 若是团队协作,建议统一技能版本(本技能当前为 1.1),避免成员之间范式不一致。
安装步骤:
- 在技能市场或技能管理面板中,按技能 ID 11868 检索「查询层模式 / query-layer」;
- 点击安装或启用,确认技能被写入本地技能目录;
- 在项目对话中确认技能已被识别——通常表现为 AI 助手能主动引用该技能的规则(如「错误必须在查询层边界转换」);
- 重启或刷新 AI 助手会话,使技能上下文生效。
验证是否生效:向 AI 助手提一个贴近真实需求的问题,例如「帮我写一个消费 services.db.recordings.getAll 的查询,用 TanStack Query,错误要转成用户可显示的类型」。如果回答中出现了 defineQuery、Err({ title, description, action })、.options 等该技能特有的范式,而不是泛泛的 useQuery 示例,说明技能已成功加载。若未生效,请检查技能是否启用、是否被更高优先级的规则覆盖,具体以官方说明为准。
该技能的主要功能和特点
- 标准化的查询层架构:明确「UI 组件 → 查询层(RPC/查询) → 服务层(纯函数)」三层职责,查询层负责调用注入配置后的服务、转换错误、管理缓存、提供双接口。
- 三层错误流转换:服务层的
TaggedError<'名称'>(领域特定)→ 查询层的UserFacingError(展示就绪)→ UI 层的吐司通知,边界清晰,杜绝「组件里catch完再猜错误含义」。 - 反模式防护:专门点名「双重包装错误」这一高频踩坑点,要求「转换一次,直接使用」,并给出
onError: (error) => notify.error.execute(error)的正确写法。 - 运行时依赖注入:查询层依据
settings.value[...]动态选择服务实现,示例覆盖转录服务在 OpenAI / Groq 之间切换、录制器在 navigator / ffmpeg / cpal 之间切换(含 Tauri 环境判断window.__TAURI_INTERNALS__)。 - 双重接口模式:每个查询/突变同时暴露反应式
.options(静态对象,配合createQuery/createMutation使用)与命令式.execute()/.fetch()(事件处理器与顺序工作流使用),并给出「何时用哪种」的对照表。 - 缓存管理范式:乐观更新(先
setQueryData立即刷新 UI,再invalidateQueries后台拉新)+ 层级化查询键(['db','recordings']、['db','recordings','latest']、['db','recordings', id])便于精准失效。 - 参数化与初始数据:支持
getById: (id: Accessor<string>) => defineQuery({...})的访问器写法,以及基于已有缓存生成initialData与initialDataUpdatedAt,避免二次加载闪烁。 - 统一 RPC 命名空间:所有模块(db、recorder、transcription、clipboard、sound、analytics、notify)统一挂在
rpc下,调用方只需import { rpc } from '$lib/query'。 - 多服务协同示例:
notify模块演示一次突变里同时触发吐司与操作系统通知,加载态只走吐司以避免通知轰炸。
和普通工具比强在哪:直接问 AI「怎么写 useQuery」,你得到的是通用示例;启用本技能后,你得到的是符合三层架构、错误已转换、缓存键已分层、还能随时切换服务实现的项目内代码,可读性和可维护性差异巨大。
如何使用该技能?
步骤一:明确你处在哪一层
先判断需求属于哪一类,技能对每类都有对应范式:
- 创建消费服务的查询或突变;
- 把服务层错误转成 UI 错误类型;
- 基于用户设置做运行时服务选择;
- 为即时反馈加乐观缓存更新;
- 理解双重接口(反应式与命令式)如何取舍。
步骤二:定义查询键(先分层,后写查询)
export const dbKeys = {
recordings: {
all: ['db', 'recordings'] as const,
latest: ['db', 'recordings', 'latest'] as const,
byId: (id: string) => ['db', 'recordings', id] as const,
},
};
输入:模块名 + 资源名 + 可选参数。输出:可直接传给 invalidateQueries 的层级键,支持「整体失效」与「单条失效」两种粒度。
步骤三:写基础查询
export const db = {
recordings: {
getAll: defineQuery({
queryKey: dbKeys.recordings.all,
queryFn: () => services.db.recordings.getAll(),
}),
},
};
步骤四:写带错误转换的查询
enumerateDevices: defineQuery({
queryKey: recorderKeys.devices,
queryFn: async () => {
const { data, error } = await recorderService().enumerateDevices();
if (error) {
return Err({
title: '❌ 枚举设备失败',
description: error.message,
action: { type: 'more-details', error },
});
}
return Ok(data);
},
}),
输入:纯函数式服务调用返回的 Result。输出:Ok(data) 或已转为展示就绪的 Err(UserFacingError),UI 层无需再判断错误类型。
步骤五:在组件中用反应式接口
<script lang="ts">
import { createQuery } from '@tanstack/svelte-query';
import { rpc } from '$lib/query';
const recordings = createQuery(() => rpc.db.recordings.getAll.options);
</script>
{#if recordings.isPending}
<Spinner />
{:else if recordings.error}
<Error message={recordings.error.description} />
{:else}
{#each recordings.data as recording}
<RecordingCard {recording} />
{/each}
{/if}
注意:.options 是静态对象、不带括号,必须包在访问器函数里传入,这是本技能反复强调的关键规则。
步骤六:在事件处理器与工作流中用命令式接口
async function handleBulkDelete(recordings: Recording[]) {
for (const recording of recordings) {
const { error } = await rpc.db.recordings.delete.execute(recording);
if (error) {
notify.error.execute(error);
return;
}
}
notify.success.execute({ title: '所有录制删除成功' });
}
步骤七:需要即时反馈时加乐观更新
在突变成功后立刻 queryClient.setQueryData 写入新数据,再 invalidateQueries 让后台重新拉取;配合 onSettled 钩子失效相关状态键,保证缓存与服务器最终一致。
步骤八:接入统一命名空间
把各模块挂进 rpc:
export const rpc = { db, recorder, transcription, clipboard, sound, analytics, notify } as const;
至此,应用任何位置都能「一处导入、两种用法」。
同类型竞品对比分析
| 方案名称 | 定位 | 核心功能 | 安装/使用门槛 | 适用场景 | 优势与不足 |
|---|---|---|---|---|---|
| 查询层模式(query-layer,本技能) | AI 技能:架构范式 + 代码生成规范 | 三层错误流、双重接口、运行时依赖注入、乐观更新、层级查询键、RPC 命名空间 | 需支持技能机制的 AI 工具;团队需理解 Result 与访问器写法 | Svelte + TanStack Query 的分层项目、多服务商切换场景 | 优势:范式统一、错误边界清晰、可照抄落地;不足:强绑定 TanStack Query 与 WellCrafted 风格,学习曲线偏陡 |
| TanStack Query 原生手写 | 数据请求库本身 | 缓存、重试、失效、乐观更新等底层能力 | 门槛低,但规范全靠团队自觉 | 任意 React / Svelte / Vue 项目 | 优势:灵活、生态成熟;不足:无错误转换与依赖注入约定,易演化成「每个组件一套写法」 |
| SWR + 自建错误映射 | 轻量数据请求库方案 | 请求去重、重新验证、缓存 | 门槛低 | 中小型项目、页面级数据获取 | 优势:体积小、API 简单;不足:突变编排与乐观更新能力弱,跨层错误规范化仍需自建 |
| RTK Query | Redux 生态的数据层方案 | 端点定义、自动缓存、代码生成 | 需引入 Redux 体系,门槛中等偏高 | 已在用 Redux 的大型应用 | 优势:与状态管理深度整合;不足:与 Svelte 生态契合度低,样板代码较多 |
| 自研 fetch 封装 | 项目内私有工具 | 请求拦截、统一错误码 | 起步门槛最低 | 早期项目、需求简单 | 优势:完全可控;不足:缓存/反应性/乐观更新全部自建,长期维护成本高 |
选型建议:若项目已在 TanStack Query 上,且存在「服务层错误直接漏到 UI」「服务商可切换」的痛点,本技能的收益最直接;若只是页面级取数,原生 TanStack Query 或 SWR 已足够。
该技能的典型应用场景与实际体验情况
场景一:转录服务多服务商切换(后端开发者 / 全栈工程师)
痛点:产品支持 OpenAI 与 Groq 两家转录服务,用户在设置里一改,散落在各处的调用点全要改。
使用方式:按技能的「服务选择模式」,在查询层写一个 transcribeBlob,用 switch 读取 settings.value['transcription.selectedTranscriptionService'],每个分支带入对应 API Key、模型名、输出语言、提示词与温度参数;未选择时返回 Err({ title: '⚠️ 未选择转录服务', ... })。
实际体验:好处是「配置差异全部收口在一处」,新增服务商只是加一个 case;注意点是各服务商参数并非完全对称,需逐项核对设置键名,否则容易出现参数错位。
场景二:桌面端与浏览器端录制器选择(桌面应用开发者)
痛点:同一份前端代码要跑在浏览器与 Tauri 桌面端,录音实现不同(navigator / ffmpeg / cpal)。
使用方式:recorderService() 内先判断 window.__TAURI_INTERNALS__,浏览器直接返回 services.navigatorRecorder;桌面端用 recorderMap 按 settings.value['recording.method'] 取对应实现。
实际体验:这是该技能里最「开箱即用」的一段,几乎可以原样照搬;注意点在于运行环境判断要放在最前面,避免在无 Tauri 环境下访问桌面服务导致报错。
场景三:录音列表的乐观更新(前端工程师)
痛点:新建一条录音后要等后端返回、再重新拉列表,界面明显卡顿。
使用方式:在 create 突变的 mutationFn 内,服务调用成功后立刻 queryClient.setQueryData 把新记录插入 recordings.all 与 byId 缓存,再 invalidateQueries 触发后台刷新。
实际体验:交互体感提升最明显,「点完立即出现」;注意点是乐观写入的数据必须足够完整(含 id 与时间戳),否则后续按时间排序的 getLatest 会取到异常值。
场景四:批量删除与顺序工作流(业务开发)
痛点:批量删除时逐条调用,中途失败需要中断并提示;停止录制后要继续转录,属于典型串行流程。
使用方式:在事件处理器里用 .execute() 逐条调用,遇错即 notify.error.execute(error) 并 return;顺序工作流中先 .execute() 停止录制拿到 blob,失败则直接返回。
实际体验:.execute() 与 .options 的分工一旦形成肌肉记忆,代码可读性提升显著;注意点是 .execute() 不要用在组件渲染上下文里,createMutation 需要组件上下文,这条规则在技能中被明确列出。
该技能能为用户带来的价值
- 统一范式,降低协作成本:错误转换、缓存键、接口形态都有唯一答案,新人接手时不需要「读懂十几种写法」,代码评审也可以直接对着规则清单走。
- 把错误处理前置到边界:三层错误流让领域错误不会污染 UI 层,组件里只关心
error.description怎么显示,减少大量防御性判断代码。 - 切换服务商的边际成本接近于零:依赖注入把「改设置」和「改代码」解耦,新增一个服务商通常只需在查询层加一个分支。
- 交互体感提升:乐观更新让列表类操作「点完即现」,配合后台失效刷新,兼顾速度与一致性。
- 可量化的收益方向:新增一个查询/突变的代码量从「重复样板」压缩为「填空式定义」;错误提示的一致化减少了「同一个失败在不同页面有不同文案」的问题(具体提升幅度依团队规模与项目复杂度而定)。
- 长期价值:查询层是少有能「越用越省」的架构投资——它同时沉淀了规范、示例与反模式,配合 AI 助手使用时,规范不再依赖口口相传。
常见问题 FAQ
Q1:这个技能怎么安装?需要付费吗?
A:在支持技能机制的 AI 编程工具中,通过技能市场或技能管理面板检索技能 ID 11868(或名称「查询层模式 / query-layer」)并启用即可,具体入口与是否收费以官方说明为准。安装后建议用第 1 部分给出的验证问题测试是否生效。
Q2:我只用 React,不用 Svelte,这个技能还有用吗?
A:技能的核心内容——三层错误流、运行时依赖注入、查询键层级化、乐观更新、禁止双重包装错误——是框架无关的架构思想。但示例代码大量使用 createQuery(() => ...options) 这类 Svelte 写法,React 用户需要自行替换为对应的 hooks 调用方式,技能不会自动帮你完成这层映射。
Q3:为什么一定要在查询层转换错误,直接在组件里 catch 不行吗?
A:技能明确要求「始终在查询边界转换错误,切勿返回原始服务错误」。原因是服务层抛的是领域特定错误(TaggedError<'名称'>),组件关心的是「标题写什么、描述写什么、要不要给「查看更多」按钮」。若在组件里转换,同一类错误会在不同组件里被写出不同文案,且容易触发「双重包装」反模式。
Q4:.options 为什么不能加括号?
A:.options 是静态对象,不是函数。在 Svelte 中需包在访问器函数里传入,如 createQuery(() => rpc.db.recordings.getAll.options);写成 options() 会直接报错。这是技能「关键规则」中特别强调的一条。
Q5:它和直接用 TanStack Query 官方文档学有什么区别?
A:官方文档教你「库怎么用」,本技能教你「在分层架构里怎么用才不出乱子」。前者给你 useQuery 的 API,后者给你错误边界、依赖注入、RPC 命名空间、查询键分层等一套团队级约定,并明确指出哪些写法是反模式。若你的项目只有零星几个请求,官方文档足够;若项目要长期演进并多人协作,本技能的范式价值更明显。