大家好,我是程序员柏风,AI 最全的 Vibecoding 学习地图 http://coderoadmap.cn 作者。
Query
这一课你将能做到
- 写出带参数、变量、别名、指令的完整查询,为「课时详情页」一次取齐数据
- 用片段(Fragment)消除重复字段集,并理解内联片段在多态类型上的用法
- 解释操作名(Operation Name)在生产环境为什么不是可选项
为什么要学
Query 是 GraphQL 里你 90% 时间在与它打交道的东西。上一课的类型系统是「服务端能提供什么」,这一课是「客户端怎么精确地要」。别名解决字段冲突,变量解决动态参数,片段解决重复,指令解决条件渲染——四件套配齐,真实业务的查询都能优雅写出来。
人话:SQL 的 SELECT 语句决定列,GraphQL 的 Query 决定「图的哪棵子树」。
先建立图像
一次课时详情页查询的解剖图:
query LessonPage($slug: String!, $withProgress: Boolean!) { ← 操作名 + 变量声明
lesson(slug: $slug) { ← 字段 + 参数
...LessonCore ← 片段展开
author { name } ← 嵌套字段(图遍历)
progress: myProgress @include(if: $withProgress) { ← 别名 + 指令
status
percent
}
}
}
执行顺序:解析变量 → 校验(对 Schema)→ 逐层执行 resolver → 拼装成镜像 JSON。
跟着做
步骤 1:最小查询到带参查询
# 最小:匿名查询
{ lessons(first: 5) { title slug } }
# 带参数:参数直接写在字段后
{ lesson(slug: "gqlrm-queries") { title durationMinutes } }
注意第一行是匿名查询——只能有一个,且生产环境禁用(见要点 5)。
步骤 2:变量——把动态值踢出查询字符串
永远不要字符串拼接查询。声明变量,值放在请求体的 variables 里:
query LessonDetail($slug: String!, $first: Int = 10) {
lesson(slug: $slug) {
title
submissions(first: $first) { id status }
}
}
// 请求体
{
"query": "query LessonDetail(...) {...}",
"variables": { "slug": "gqlrm-queries" },
"operationName": "LessonDetail"
}
变量类型声明里的 ! 表示必传;= 10 是默认值。变量是类型安全的:传了 Int 给 String!,校验直接拒绝。
步骤 3:别名——同一个字段查两次
对比两种难度的课时,同名字段必须用别名区分:
query CompareLessons {
easy: lesson(slug: "gqlrm-intro") { title difficulty }
hard: lesson(slug: "gqlrm-auth") { title difficulty }
}
返回的 key 就是别名:{ "data": { "easy": {...}, "hard": {...} } }。
步骤 4:片段——DRY 你的字段集
列表卡片和详情头部都要「标题 + slug + 难度 + 时长」,抽成片段:
fragment LessonCard on Lesson {
title
slug
difficulty
durationMinutes
}
query TrackPage($slug: String!) {
track(slug: $slug) {
title
lessons { ...LessonCard }
}
}
query Related($slug: String!) {
lesson(slug: $slug) {
...LessonCard
track { lessons { ...LessonCard } }
}
}
片段可以嵌套片段。每个片段必须声明 on 某类型,这让校验器能检查字段是否存在。
步骤 5:指令——@include / @skip 条件取字段
未登录用户不查进度:
query LessonPage($slug: String!, $loggedIn: Boolean!) {
lesson(slug: $slug) {
title
myProgress @include(if: $loggedIn) { status percent }
author @skip(if: false) { name } # @skip 是 @include 的反义
}
}
内置指令就这两个(外加 Schema 侧的 @deprecated)。@defer/@stream 属于增量交付,在 Subscription 一课讲。
步骤 6:多根字段 = 并行聚合
一次请求聚合一整个学习看板:
query Dashboard($uid: ID!) {
me: user(id: $uid) { name streakDays }
recentTracks: tracks(first: 3) { title }
pendingReviews: submissions(filter: { status: PENDING }, first: 5) { id }
}
三个根字段互不依赖,服务端并行执行——一个往返顶 REST 三个。
讲清楚要点
- 查询即子树声明:你写下的嵌套结构就是在数据图上圈出一棵子树,响应与之一一镜像。
- 变量必须声明类型:
$slug: String!与 Schema 参数类型做编译期(请求执行前)匹配,是 GraphQL 类型安全的延伸。 - 别名是响应 key 的重命名:解决同字段冲突 + 让语义更清晰(
easy:/hard:)。 - 片段是复用单元,不是性能优化:片段不会减少数据传输,只减少查询文本重复;性能靠「只声明需要的字段」。
- 操作名生产必带:
operationName是监控、限流、持久化查询(Persisted Query)的锚点;匿名查询在治理层面是黑洞。 - 根字段并行、子字段串行:同一层的字段可并行解析,子字段等父字段出结果才开始——理解这个才能预判 N+1。
- 指令只影响取不取,不影响怎么取:
@include(if: false)的字段 resolver 不会执行,省的是服务端工作量。
你可能会懵的地方
- 变量能插到字段名里吗? 不能。变量只能替换「值」,不能替换「结构」(字段名、类型名)。想动态结构,用指令或客户端拼装。
- 一个请求里能写多个查询吗?
能写多个操作,但执行时必须用
operationName指定一个。真正要「一次执行多个」请用多根字段。 - 别名会改变 resolver 收到的参数吗? 不会。别名只是输出 key 重命名,resolver 看到的字段名还是原名。
@include和客户端 if 有区别吗? 有。指令在服务端生效,被跳过的字段不执行、不传字节;客户端 if 是查询发出去前就决定好形状。两者常配合:登录态决定是否带@include变量。- 查询可以嵌套多深? 语法上无限,生产上必须设上限(如 10 层),否则恶意深查询能拖垮服务。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
query { lesson(slug: "${slug}") } 字符串拼接 | 注入风险、缓存失效 | 用 $slug: String! 变量 |
| 生产环境发匿名查询 | 无法监控/限流/做持久化查询 | 每个操作都命名 |
| 复制粘贴 20 个字段到 5 处 | 改一处漏四处 | 抽 Fragment |
| 用别名当「重命名 API 字段」的手段长期欠债 | 查询可读性劣化 | 改 Schema 字段名 + @deprecated 过渡 |
| 一个页面发 8 个独立查询 | 退化回 REST 多次往返 | 多根字段合并成一个操作 |
卡点急救
- 报 "Variable $slug is never used":声明了变量但查询里没引用,删掉声明或补引用——校验器对「声明未用」是硬错误。
- 报 "Unknown fragment LessonCard":片段没和查询放在同一个文档里发出去,检查客户端的文档拼装(codegen 场景检查 import)。
- 返回 400 "Must provide operation name":请求体里塞了多个操作但没传
operationName,补上即可。
面试常问
- GraphQL 变量和字符串拼接的区别? 变量在执行前经类型校验,传输在独立 JSON 字段中,杜绝注入;且查询文本不变使服务端可按文本做缓存与持久化查询白名单。
- Fragment 的作用与边界? 复用字段集、配合代码生成产出组件级类型;边界是片段只能声明在单一类型上,跨类型的公共形状要靠接口。
- 一个查询里多个根字段怎么执行? 并行执行(语义上无依赖),任一失败按可空性冒泡,互不影响其他根字段的部分成功。
自测三问
- 为什么生产环境要求每个操作都有
operationName? @include(if: false)的字段,它的 resolver 会执行吗?- 想在一个请求里同时查「课时详情」和「推荐课时」,有哪两种写法?
小结一句话
Query 的本质是声明数据图上的一棵子树:变量管动态值、别名管冲突、片段管复用、指令管条件,四件套组合出任意复杂页面的一次性取数。
延伸
- 官方文档:Queries and Mutations
- 下一课:Mutation——写操作、输入对象与结果负载的设计范式。