【免费下载链接】howtographql
The Fullstack Tutorial for GraphQL
在 GraphQL 全栈架构中,客户端承担着将声明式查询转化为网络请求、管理本地缓存、驱动 UI 更新等关键职责。本文围绕 howtographql 仓库中的 GraphQL Clients 概述 展开,系统讲解 GraphQL 客户端需要解决的五大基础设施问题——直接发送查询/变更、视图层集成、缓存策略、构建时 Schema 校验、视图与数据依赖的协同定位,并结合仓库中 React/React Native、Angular、Vue 等多个前端教程的实际代码,对比 Apollo Client、Relay、urql 三大主流实现。读完本文,你将理解 GraphQL 客户端的设计动机与核心机制,并能在自己的框架中正确配置和使用它们。
一、为什么前端需要一个 GraphQL 客户端
使用 GraphQL API 的前端开发,天然存在一套需要反复实现的"基础设施"能力。原文档将其归纳为四点:
- 直接发送查询与变更:无需手工构造 HTTP 请求;
- 视图层集成:让数据能够自然流入组件;
- 缓存:避免重复请求,提供流畅的用户体验;
- 基于 Schema 的校验与查询优化:在构建期发现错误、优化查询。
诚然,使用原生fetch或NSURLSession直接调用 GraphQL 端点也是可行的——把 JSON 请求体拼好、把响应里的字段逐层解包塞进 UI。但 GraphQL 的价值恰恰在于将这一层"手工搬运"抽象掉:你只需要用声明式语言写出数据需求,剩下的请求发送、响应处理、缓存与更新都由客户端接管。这正是 0-clients.md 全篇的核心主张。
当前生态中两个最主要的 GraphQL 客户端是:
- Apollo Client:社区驱动的通用客户端,覆盖 Web、iOS、Android 等几乎所有主流平台;
- Relay:Facebook 自研客户端,深度优化性能,主要面向 Web 端。
下文将逐项展开这五大能力,并在每一节给出仓库内前端教程的可运行示例作为印证。
二、直接发送查询与变更:从手工 HTTP 到声明式取数
使用 REST 时,你需要针对每个端点手工构造请求、处理状态码、解析响应体。而 GraphQL 客户端把"发送请求并处理响应"封装成了统一机制:你只需提供一个查询文档,客户端负责将其 POST 到 GraphQL 端点并解析结果。
以仓库中的 React + Apollo 教程 2-queries-loading-links.md 为例,定义一个FEED_QUERY查询文档:
{ feed { id links { id createdAt description url } } }在组件中使用useQueryhook 发送:
import { useQuery, gql } from '@apollo/client'; const FEED_QUERY = gql` { feed { id links { id createdAt url description } } } `; const LinkList = () => { const { data } = useQuery(FEED_QUERY); return ( <div> {data && ( <> {data.feed.links.map((link) => ( <Link key={link.id} link={link} /> ))} </> )} </div> ); };gql使用 JavaScript 的 tagged template literals 将 GraphQL 字符串解析为可执行的查询文档;useQuery接收该文档,自动完成网络请求并返回loading、error、data三个状态量——loading在请求进行中为true,error携带失败信息,data为服务端返回的数据。这也是"声明式取数"最直接的体现:组件不关心请求如何发出,只声明自己要什么。
在 Angular 教程 2-queries-loading-links.md 中,同样的能力通过Apollo服务与 RxJS Observable 呈现。查询被集中定义在src/app/graphql.ts:
import gql from 'graphql-tag'; export const ALL_LINKS_QUERY = gql` query AllLinksQuery { allLinks { id createdAt url description } } `;组件中注入服务并订阅结果:
import { Apollo } from 'apollo-angular'; constructor(private apollo: Apollo) {} ngOnInit() { this.apollo.watchQuery<AllLinkQueryResponse>({ query: ALL_LINKS_QUERY }).valueChanges.subscribe((response) => { this.allLinks = response.data.allLinks; this.loading = response.data.loading; }); }watchQuery返回一个可订阅的 Observable,除了一次性query方法之外,它能持续跟踪查询结果的变化——这为后面的缓存与 UI 更新机制埋下了伏笔。
三、视图层集成与 UI 更新:数据如何流入组件
服务端响应抵达客户端之后,数据必须进入 UI。不同框架有不同的集成方式,但共同点是:GraphQL 客户端都提供与框架深度绑定的绑定层。
以 React 为例,原文档指出客户端历史上使用**高阶组件(HOC)**在幕后取数并把数据注入组件props。现代 React 则演进出了更直接的hooks方案。在 2-queries-loading-links.md 中,useQuery就是这一演进的体现——无需包装组件,直接在函数组件内声明数据依赖。
值得一提的是,GraphQL 的声明式特性与**函数响应式编程(FRP)**天然契合:视图只声明数据依赖,FRP 层负责把数据流与 UI 状态接通。React 的 hooks + Observable 组合、Angular 的 RxJS Observable(上文watchQuery().valueChanges即是一条数据流)、Vue 的响应式data都是这种结合的实例。
在 Vue 教程 2-queries-loading-links.md 中,视图层集成通过组件的apollo对象完成:
import { ALL_LINKS_QUERY } from '../constants/graphql'; export default { name: 'LinkList', data () { return { allLinks: [], loading: 0 } }, components: { LinkItem }, apollo: { allLinks: { query: ALL_LINKS_QUERY } } }模板中配合v-if="loading"显示加载态,v-for遍历allLinks渲染列表。客户端取到数据后自动更新响应式data,Vue 的响应式系统随即驱动视图刷新——整个过程对组件作者完全透明。
四、缓存查询结果:为何必须归一化(Normalization)
大多数应用都希望缓存已获取的数据,以提供流畅体验并节省流量。直觉做法是把查询结果整体塞进本地 store,下次遇到相同查询直接返回。但原文档明确指出:这种"整体缓存"方案对大多数应用极其低效。
原因在于 GraphQL 查询往往是嵌套结构,且不同查询会以不同形状覆盖同一批对象。比如feed查询返回的Link对象,可能在vote相关的另一查询中以不同的字段组合再次出现。若按查询整体缓存,同一个Link会被复制多份,任何一处更新都难以同步到其他副本,缓存一致性无从谈起。
更优的做法是归一化(normalize):将(可能嵌套的)查询结果"拍平",store 中只存放可被全局唯一 ID引用的单条记录。这样每条Link只存一份,多个查询共享同一份记录,更新一处即可全局生效。
Apollo Client 的实现即为此设计——1-getting-started.md 中创建客户端实例时传入的InMemoryCache就是归一化缓存的核心:
import { ApolloProvider, ApolloClient, createHttpLink, InMemoryCache } from '@apollo/client'; const httpLink = createHttpLink({ uri: 'http://localhost:4000' }); const client = new ApolloClient({ link: httpLink, cache: new InMemoryCache() });Angular 教程 1-getting-started.md 还提到一个关键配置:new InMemoryCache({ dataIdFromObject: o => o.id }),即指定 Apollo 如何识别并去重服务端返回的对象——这正是归一化缓存中"全局唯一 ID"的落地方式:告诉缓存每个对象的 ID 从哪个字段取,缓存即可据此建立记录索引。
Relay 同样采用归一化模型。在 1-getting-started.md 中,Relay Environment 由两大部分构成:负责网络通信的Network和负责缓存的Store(基于RecordSource实现):
const { Environment, Network, RecordSource, Store } = require('relay-runtime'); const store = new Store(new RecordSource()); const network = Network.create((operation, variables) => { return fetch('__RELAY_API_ENDPOINT__', { method: 'POST', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' }, body: JSON.stringify({ query: operation.text, variables, }), }).then(response => response.json()); }); const environment = new Environment({ network, store });RecordSource存放的就是归一化后的记录集合,每条记录以全局唯一 ID 为键。这也解释了为什么 Relay 教程要求 fragment 中必须包含id字段——2-queries-loading-links.md 中明确指出:id是 Relay 在缓存中唯一标识、存储与检索 link 项所必需的。
五、构建时 Schema 校验与优化:把错误挡在发布之前
GraphQL Schema 包含了客户端对该 API 能做的一切操作信息,因此存在一个巨大机会:在构建期(build-time)校验乃至优化客户端将要发送的查询。
原理很简单:当构建环境能访问 Schema 时,它可以解析项目中所有 GraphQL 代码,与 Schema 逐项比对。拼写错误的字段名、不存在的类型、缺失的必填参数,都会在应用到达真实用户之前被捕获——否则同样的错误要等到运行时才暴露,代价要大得多。
这并非纸上谈兵。Relay 正是以"编译期严谨"著称的客户端。在 1-getting-started.md 中,作者明确描述了relay-compiler的职责:"Relay Compiler 是一个你在构建期用来校验和优化项目中 GraphQL 代码的工具。"项目需要三件套:
react-relay:Relay 运行时,负责网络与缓存;relay-compiler:构建期校验与优化 GraphQL 代码;babel-plugin-relay:Babel 插件,把项目中的 GraphQL 代码转换为 Relay Compiler 需要的格式。
实际运行方式(2-queries-loading-links.md):
relay-compiler --src ./src --schema ./schema.graphql--src指向所有包含graphql代码的文件目录,--schema指向完整的 GraphQL Schema 文件。编译器扫描src中的全部 GraphQL 代码、对照 Schema 校验,并生成对应的 JavaScript 表示存入./src/__generated__:
Created: - Link_link.flow.js - Link_link.graphql.js - LinkList_viewer.flow.js - LinkList_viewer.graphql.js - LinkListPageQuery.graphql.js如果跳过编译直接运行应用,会立刻看到类似Module not found: Can't resolve './__generated__/LinkListPageQuery.graphql'的编译错误——这正是"构建期校验"在工程实践中的直观体现:未编译的 GraphQL 代码根本无法进入运行时。
六、协同定位(Colocation):让视图与数据依赖并肩而行
GraphQL 的一个强大理念是:UI 代码与其数据需求可以放在一起。视图与数据依赖的紧密耦合极大改善了开发者体验——你不再需要在头脑中维护"某块 UI 的数据来自哪里"的映射关系。
协同定位的效果取决于平台。原文档指出:在 JavaScript 应用中,数据依赖与 UI 代码可以写进同一个文件;而在 Xcode 中,可以用 Assistant Editor 同时编辑 view controller 与 GraphQL 代码。
这一理念在 Relay 中被贯彻得最为彻底,甚至成为其核心标识。在 2-queries-loading-links.md 中,作者如此定义:colocation 意味着 React 组件在其定义处(同一文件内)声明数据依赖,形式是 GraphQL Fragment。
实现方式是createFragmentContainer高阶组件——接收两个参数:一个 React 组件,以及用graphql函数包装的数据依赖(Fragment):
import { createFragmentContainer, graphql } from 'react-relay'; export default createFragmentContainer(Link, graphql` fragment Link_link on Link { id description url } `);Fragment 命名有约定:<文件名>_<prop名>。Link.js文件、注入linkprop,因此 Fragment 命名为Link_link。
子组件与父组件之间的 Fragment 通过展开运算符复用。LinkList需要一组链接,它引用子组件的 Fragment 并声明连接查询:
export default createFragmentContainer(LinkList, graphql` fragment LinkList_viewer on Viewer { allLinks(last: 100, orderBy: createdAt_DESC) @connection(key: "LinkList_allLinks", filters: []) { edges { node { ...Link_link } } } } `);这里的@connection指令是 Relay 对列表的抽象(Connection),服务于后续的游标分页与缓存更新——key参数用于在缓存中标识这条连接。
那么,组件都只写 Fragment 而不写完整查询,真正的查询是谁拼出来的?答案是QueryRenderer——Relay 组件树的根。它接收三样东西:一个 Relayenvironment、一个根query、以及一个处理 loading/error/success 三种状态的render函数:
const LinkListPageQuery = graphql` query LinkListPageQuery { viewer { ...LinkList_viewer } } `; <QueryRenderer environment={environment} query={LinkListPageQuery} render={({error, props}) => { if (error) { return <div>{error.message}</div> } else if (props) { return <LinkList viewer={props.viewer} /> } return <div>Loading</div> }} />各组件通过 Fragment 声明局部数据需求,QueryRenderer在树根处把这些 Fragment 组合成实际发送给服务器的完整查询——你可以通过浏览器 DevTools 的 Network 面板查看它最终拼出的查询。这与 Apollo 形成鲜明对比:Apollo 同样支持协同定位,但常规做法是直接写查询而非Fragment。
七、客户端生态对照:Apollo、Relay 与 urql 的设计取向
通过上文可以总结出三大客户端在核心机制上的取向差异:
| 维度 | Apollo Client | Relay | urql |
|---|---|---|---|
| 数据依赖形式 | 完整查询文档(支持 Fragment) | Fragment + 编译器组合 | 完整查询文档 |
| 缓存 | InMemoryCache归一化缓存 | Store+RecordSource归一化缓存 | 归一化文档缓存 |
| 校验时机 | 运行时 + 可选工具链 | 构建期(relay-compiler强校验) | 运行时 |
| 视图绑定 | HOC / hooks(useQuery)/ 服务注入 | FragmentContainer+QueryRenderer | hooks(useQuery)/ render props |
| 平台覆盖 | Web、iOS、Android 等多平台 | 主要面向 Web | 以 React 为主 |
urql 的查询流程在 2-queries-loading-links.md 中有清晰记录:低层 API 是executeQuery、executeMutation、executeSubscription,返回基于 Wonka 库的"结果流";而 React 场景下推荐使用useQueryhook,返回[result]数组——第一个元素是包含fetching、error(CombinedError,区分networkError与graphQLErrors)、data的结果对象,第二个是用于重新取数的execute函数:
const [result] = useQuery({ query: FEED_QUERY }) const { data, fetching, error } = result if (fetching) return <div>Fetching</div> if (error) return <div>Error</div> const linksToRender = data.feed.links无论选择哪家客户端,其背后共享的底层模型是一致的:声明式查询 → 客户端代为发送请求 → 归一化缓存 → 响应式驱动 UI 更新。这正是 0-clients.md 所描绘的图景,也是后续 更多 GraphQL 概念、工具链与生态 等进阶章节的基础。
八、小结
GraphQL 客户端存在的根本理由,是把"数据如何到达 UI"这一横切关注点从业务代码中剥离:声明式查询取代手工 HTTP、框架绑定层接管视图更新、归一化缓存解决数据一致性与重复请求、构建期校验把错误挡在发布之前、协同定位让视图与其数据需求同处一室。理解这五大机制,你就能在选型时做出有依据的判断——追求强类型编译保障与性能优化选 Relay,追求灵活性与多平台覆盖选 Apollo Client,追求轻量与 React 生态契合选 urql,并在自己的项目中正确地配置客户端、编写查询与设计缓存策略。
【免费下载链接】howtographql
The Fullstack Tutorial for GraphQL
相关推荐
regnety_064.ra3_in1k性能深度测评:ImageNet-1k数据集83.7%准确率背后的秘密
regnety_064.ra3_in1k性能深度测评:ImageNet 1k数据集83.7%准确率背后的秘密 regnety_064.ra3_in1k是一个基于
人工智能计算机视觉深度学习TW-Elements与GraphQL客户端:Apollo Client集成实战
TW Elements与GraphQL客户端:Apollo Client集成实战 你是否在前端开发中遇到过UI组件与数据获取逻辑难以协同的问题?是否想让Tail
UI组件前端Snabbdom与GraphQL客户端:Apollo与Relay集成实践
Snabbdom与GraphQL客户端:Apollo与Relay集成实践 你还在为前端状态管理与虚拟DOM渲染的性能问题烦恼吗?当应用数据复杂度提升时,传统的状
前端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考