客户端
Apollo/Urql/Relay。
30 分钟
客户端
这一课你将能做到
- 用 Apollo Client 完成「课时列表 + 提交代码」的查询、变更与缓存更新
- 解释归一化缓存的工作原理,并说出三大客户端(Apollo / urql / Relay)的选型逻辑
- 配置代码生成(GraphQL Code Generator)让查询获得端到端类型安全
为什么要学
直接用 fetch 调 GraphQL 完全可行——它就是个 POST。但真实应用马上会遇到三个问题:同一个 Lesson 在列表和详情各查一次、数据不一致;Mutation 返回后要手动同步七八处 UI;查询字符串和 TypeScript 类型永远对不上。客户端库就是为这三件事而生的:归一化缓存、缓存自动更新、类型生成。
人话:fetch 是自行车,客户端库是汽车——都能到,但长途你会累死在半路上。
先建立图像
归一化缓存的核心思想——按 类型:id 拆平存储:
# 查询返回
{ "track": { "id": "t1", "title": "GraphQL",
"lessons": [ { "id": "101", "title": "概览" }, ... ] } }
缓存内部(不是嵌套树,是平铺表):
Track:t1 → { title: "GraphQL", lessons: [Ref(Lesson:101), ...] }
Lesson:101 → { title: "概览", ... } ← 全局唯一一份
Lesson:102 → { ... }
Mutation 返回 Lesson:101 的新数据时,所有引用它的 UI 自动更新——这是归一化缓存的杀手锏。
跟着做
步骤 1:最小接入 Apollo Client
npm install @apollo/client graphql
import { ApolloClient, InMemoryCache, HttpLink, gql } from '@apollo/client'
const client = new ApolloClient({
link: new HttpLink({ uri: '/graphql', credentials: 'include' }),
cache: new InMemoryCache()
})
步骤 2:查询课时列表
const LESSONS = gql`
query Lessons($first: Int!, $after: String) {
lessons(first: $first, after: $after) {
edges { node { id title difficulty } }
pageInfo { hasNextPage endCursor }
}
}
`
function LessonList() {
const { data, loading, error, fetchMore } = useQuery(LESSONS, {
variables: { first: 20 }
})
if (loading) return <Skeleton />
if (error) return <ErrorBox code={error.graphQLErrors?.[0]?.extensions?.code} />
return (
<>
{data.lessons.edges.map(e => <LessonCard key={e.node.id} lesson={e.node} />)}
{data.lessons.pageInfo.hasNextPage &&
<button onClick={() => fetchMore({ variables: { after: data.lessons.pageInfo.endCursor } })}>加载更多</button>}
</>
)
}
步骤 3:Mutation + 缓存更新
提交代码后让「我的进度」自动刷新——Payload 回读的字段若带 id 且已在缓存中,Apollo 自动合并:
const SUBMIT = gql`
mutation SubmitCode($input: SubmitCodeInput!) {
submitCode(input: $input) {
submission { id status }
progress { id status percent } # 带 id → 命中归一化缓存,UI 自动更新
userErrors { message }
}
}
`
const [submit] = useMutation(SUBMIT)
await submit({
variables: { input: { lessonId, language: 'JAVA', code } },
optimisticResponse: { # 乐观更新:先假装成功渲染
submitCode: {
submission: { id: 'temp', status: 'PENDING' },
progress: { id: progressId, status: 'IN_PROGRESS', percent: 50 },
userErrors: []
}
}
})