├── manuscript ├── images │ ├── authorization_1024.jpg │ ├── graphiql-headers_1024.jpg │ ├── graphiql-authorization_1024.jpg │ └── github-personal-access-token_1024.jpg ├── Book.txt ├── 99-end │ ├── 02-thanks.md │ ├── 01-keep-learning.md │ └── 00-learning-paths.md ├── 00-foreword │ ├── 01-about-the-author.md │ ├── 05-challenge.md │ ├── 04-how-to-read-the-book.md │ ├── 02-requirements.md │ ├── 00-introduction.md │ └── 03-faq.md ├── 03-graphql-setup │ └── index.md ├── 02-apollo │ └── index.md ├── 01-graphql │ └── index.md ├── 06-apollo-client │ └── index.md ├── 04-graphql-fundamentals │ └── index.md └── 05-graphql-react │ └── index.md └── README.md /manuscript/images/authorization_1024.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-road-to-graphql/the-road-to-graphql-chinese/HEAD/manuscript/images/authorization_1024.jpg -------------------------------------------------------------------------------- /manuscript/images/graphiql-headers_1024.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-road-to-graphql/the-road-to-graphql-chinese/HEAD/manuscript/images/graphiql-headers_1024.jpg -------------------------------------------------------------------------------- /manuscript/images/graphiql-authorization_1024.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-road-to-graphql/the-road-to-graphql-chinese/HEAD/manuscript/images/graphiql-authorization_1024.jpg -------------------------------------------------------------------------------- /manuscript/images/github-personal-access-token_1024.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-road-to-graphql/the-road-to-graphql-chinese/HEAD/manuscript/images/github-personal-access-token_1024.jpg -------------------------------------------------------------------------------- /manuscript/Book.txt: -------------------------------------------------------------------------------- 1 | {frontmatter} 2 | 3 | 00-foreword/00-introduction.md 4 | 00-foreword/01-about-the-author.md 5 | 00-foreword/02-requirements.md 6 | 00-foreword/03-faq.md 7 | 00-foreword/04-how-to-read-the-book.md 8 | 00-foreword/05-challenge.md 9 | 10 | {mainmatter} 11 | 12 | 01-graphql/index.md 13 | 02-apollo/index.md 14 | 03-graphql-setup/index.md 15 | 04-graphql-fundamentals/index.md 16 | 05-graphql-react/index.md 17 | 06-apollo-client/index.md 18 | 07-apollo-react/index.md 19 | 08-apollo-server/index.md 20 | 21 | {backmatter} 22 | 23 | 99-end/00-learning-paths.md 24 | 99-end/01-keep-learning.md 25 | 99-end/02-thanks.md 26 | -------------------------------------------------------------------------------- /manuscript/99-end/02-thanks.md: -------------------------------------------------------------------------------- 1 | ## 感谢 2 | 3 | 现在我们已经走到了 “GraphQL 学习之道” 的终点。希望你享受阅读本书,并能在 GraphQL 方面得到一些指引。如果你喜欢本书,可以将它作为一种学习 GraphQL 的方式分享给你的朋友,这是一个不错的礼物。同时,在 [Amazon](https://www.amazon.com/s/?field-keywords=The+Road+to+GraphQL) 和 [Goodreads](https://www.goodreads.com/book/show/42641103-the-road-to-graphql) 反复阅读也可以帮你改善将来的项目。 4 | 5 | 最重要的是,我想感谢你阅读本书并完成了书中全部课程,我最大的愿望就是你能通过这些材料获得很棒的学习体验。希望你现在用 GraphQL 去构建应用的能力得到提升。我在教育的方向努力前进,很需要你给的反馈,不论是肯定还是批评。 6 | 7 | 访问[我的站点](https://www.robinwieruch.de/)获取更多关于软件和网站开发的话题,并[订阅](https://www.getrevue.co/profile/rwieruch)更新。这些更新都是有深度的内容,决不会有垃圾。 8 | 9 | 如果你喜欢这次学习体验,希望你可以和其它人分享。想想那些你身边想学习这个主题的人。 我相信开发人员需要在这一主题上与现代应用的新的水平保持一致。 10 | 11 | 感谢阅读 12 | Robin. -------------------------------------------------------------------------------- /manuscript/00-foreword/01-about-the-author.md: -------------------------------------------------------------------------------- 1 | ## 关于作者 2 | 3 | 我是一个致力于学习和讲授 JavaScript 编程的德国软件和 Web 工程师。在我拿到计算机科学硕士学位后,我仍然坚持学习。我从初创团队中收获了许多经验,期间无论是上班时还是工作之余我都大量的使用了 JavaScript,最终这样的经验导致我想要去教授他人学习与之相关的话题。 4 | 5 | 前几年中,我与一家名为 Small Improvements 的公司的优秀工程师团队密切合作,开发大规模应用程序。这家公司提供一个 SaaS 产品,使消费者能够向企业提供反馈。这个应用前端使用了 JavaScript 后端使用了 Java。在首个迭代中,Small Improve 的前端是用 Java 的 Wicket 框架和 jQuery 写的。当第一代单页面应用(Single Page Application, SPA)变得流行时,公司将前端应用迁移到了 Angular 1.x。在使用了 Angular 超过两年后,公司意识到 Angular 对于状态密集型应用来说并不是最好的解决方案,所以他们就跳到了 React 和 Redux。这使得他们的应用能够成功地大规模运行。 6 | 7 | 我在这家公司的期间,我定期的在我的个人网站上写关于 web 开发的文章。我收到了许多来自读者的宝贵反馈,这些反馈使我的写作和教学风格得到了提高。日积月累,我掌握了教授别人的能力。我感觉我的第一篇文章涵盖了太多的信息,这对于学生来说过犹不及,后来我通过一次只专注于一个主题来改善这个问题。 8 | 9 | 现在,我是一个自由软件工程师和教育工作者。我发现当看到学生从我给与的明确目标和快速循环反馈中茁壮成长时,我着实感到欣慰。你也可以在我的[个人网站](https://www.robinwieruch.de/about)上找到更多关于我的信息以及如何支持我或与我共事。 -------------------------------------------------------------------------------- /manuscript/99-end/01-keep-learning.md: -------------------------------------------------------------------------------- 1 | ## 永远不要停止学习 2 | 3 | 学习金字塔显示了记忆存留率和脑力活动之间的关系, 提供了有用的数据给教和学。自我教编程以来, 它一直是我测算教学效果最有效的方法之一。 下面是典型的脑力活动如何随着记忆存留率消退的。 4 | 5 | - **5% 听课** 6 | - **10% 阅读** - 本书是一个不错的开始 7 | - **20% 视听教程** - 录屏教程是不错的动手学习资料. 8 | - **30% 演示** - 代码演示贯穿本书. 9 | - **50% 评论** (注: 这里有一个 [Slack 组](https://slack-the-road-to-learn-react.wieruch.com/) 你可以参与其中) 10 | - **75% 动手练习** - 通过实践章节和购买源码来加深你的理解. 11 | - **90% 教授别人** - 在 Slack 社区, 开源社区, 或其它平台(如 Facebook Groups 和 Reddit) 上帮助其他小伙伴 12 | 13 | 本书开头我就说过没人能通过读书掌握编程,并且全书我一直强调课程的运用才是记住他们最好的方法。金字塔中能获得最大投资回报是最后一条:教导别人。我有这样的经历,我开始开博客记录我在 web 开发方面的经验,回答 Quora, Reddit, Stack Overflow 上的问题,并且写书。 教别人迫使你更深入地研究这些话题,学习细微差异,因为你不想误人子弟。想想朋友,同事,以及来自 Stack Overflow 和 Reddit 上的同伴,他们渴望学习 GraphQL 在现代程序中与 React.js 和 Node.js 一起使用。订一个见面会, 教他们 GraphQL。教导者和学习者都会从中收获成长。 14 | 15 | 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 《GraphQL 学习之道》 The Road to GraphQL (简体中文版) 2 | 3 | [![Slack](https://slack-the-road-to-learn-react.wieruch.com/badge.svg)](https://slack-the-road-to-learn-react.wieruch.com/) 4 | 5 | 这是 [The Road to GraphQL](https://roadtoreact.com/) 的官方仓库(简体中文版)。如果你愿意的话,请到[亚马逊](https://www.amazon.com/dp/1730853935)或者 [Goodreads](https://www.goodreads.com/book/show/42641103-the-road-to-graphql) 上留下你的评论。 6 | 7 | ## 更新、帮助和支持 8 | 9 | * 通过[电子邮件](https://www.getrevue.co/profile/rwieruch)或者 [Twitter](https://twitter.com/rwieruch) 获取本书的更新。 10 | 11 | - 进入官方 [Slack 频道](https://slack-the-road-to-learn-react.wieruch.com/) 获取帮助。 12 | 13 | - 看看如何[帮助这本书](https://www.robinwieruch.de/about/) 14 | 15 | ## 贡献 16 | 17 | 您可以通过提交新的Issue和Pull Requests来帮助改进这本书。 18 | 19 | 您可以提交Pull Request来纠正拼写,或者给某个课程增加更多细节。在写这样一本技术类的书籍时,会很容易进入盲区,会忽略一些有待解释和过多解释的内容。 20 | 21 | 另外,在遇到问题时,您可以在Github创建新的问题(Issue)。为了让这些问题尽快和更容易解决,请提供更多细节,比如报错信息、截图、出错页码、当前node版本(命令行: node -v)以及你的代码仓库地址。这些细节不全是必需的,但大部分可以为修复问题和改进本书提供很大的帮助。 22 | 23 | 非常感谢您的帮助, 24 | -------------------------------------------------------------------------------- /manuscript/00-foreword/05-challenge.md: -------------------------------------------------------------------------------- 1 | ## 挑战 2 | 3 | 我将很多我学过的东西写成了课程,它们成就了现在的我。在我的职业生涯中,在教授别人的同时我也有很大的收获,所以我希望你用同样地方式来学习。当然,首先你需要自学。然后,我给你的挑战是在学习的过程当中教会他人。以下是一些如何完成这个挑战的提示: 4 | 5 | * 从本书中挑一个主题写一篇博客。这个博客不是让你复制粘贴学习材料,而是用你自己的方式讲授这个主题。博客中,你可以用你自己的语言去解释概念、解决问题,甚至在每个细节上讨论得更加深入。之后,你将看到它是如何填补你的知识空缺,并且如何为你的职业生涯打开长远大门的。 6 | 7 | * 如果你活跃于社交媒体,你可以将你从本书学到的技巧分享给你的朋友们。你可以在推特上发布最近你从本书中学到的有趣实验,你也可以在前端开发的 Facebook 组群中炫技。记得对你的进度进行高质量的截屏以便他们可以跟进。 8 | 9 | * 如果你有自信记录你的学习,请在 Facebook Live、YouTube Live、或者 Twitch 上直播分享你学习本书的过程。可能会有很多人跟随你的直播学习,之后你也可以把直播记录上传到 YouTube 上。要知道,直接的语言交流是描述问题和解决问题的良好方式。如果某部分记录过于冗长,你可以剪掉多余的部分或者用延时摄影来显示重点。同时,考虑将 bug 和 灾难现场保留在视频中,因为这对于遇到同样问题的人来说非常有用。以下是一些如何拍出高质量视频的提示: 10 | 11 | 用你的母语录制视频。当然如果你的母语是英语的话那便最好不过了,因为它的受众最广。 12 | 13 | * 说出你的想法,行动以及你正在解决的问题。可视化只是这个挑战的一部分,而另一部分是在开发中不断的叙述你的想法和行为。视频中的开发结果不需要完美,反而它应该涵盖整个过程的跌宕起伏。如果你遇到 bug,请微笑面对,然后尝试自己修复问题并在网上寻找解决办法。与此同时,要注意说出你遇到的问题,并且打算如何解决它,这样才能帮助观众在整个观看过程中更好地跟随你的想法。 14 | 15 | * 在录制之前请检查你的音频设备,确保它可以清晰的录制你的声音,并且检查周围是否有可能会影响到你录制的杂音。此外,确保你的 IDE 或者命令行终端的字号大到足够让观众清晰的看到任何一段他们想看的文字。 16 | 17 | * 在将你的视频放到 YouTube 之前编辑它。尽量让视频保持简洁,不要按部就班的朗诵书中的内容而是用你自己的语言进行总结。 18 | 19 | 你可以联系我帮你宣传任何你发布的东西。如果你的内容非常棒,我甚至可能将它引用在之后版本的官方材料中。最后,为了加强你的学习体验,我希望你能接受这些挑战。 -------------------------------------------------------------------------------- /manuscript/99-end/00-learning-paths.md: -------------------------------------------------------------------------------- 1 | # 最后的一些想法 2 | 3 | 本书的最后几章鼓励你去应用你所学到的。到目前为止,本书已经教了你如何在 JavaScript 的客户端和服务端中使用 GraphQL。你曾在客户端用 React,在服务端用 Express,在两端同时用 Apollo 作为复杂的 GraphQL 库。但这个生态系统还有很多的探索空间。如果你还没完成本书的所有练习,没有读完本书所有的内容,你应该先完成它们。完成之后,我们来看看还可以做什么来提升 GraphQL 技能。 4 | 5 | ## 下一步学习路径 6 | 7 | 你已经构建几个书里的应用程序。当你读那些章节的时候,你已经修改了程序来运用高级技术(如基于权限的论证),工具(如 GraphQL Playground)或特性(GitHub commenting)。甚至可能你紧紧地跟上了书上的所有练习。但还没完,还有许多特性,技术,和工具是你可以运用在那些应用上的。激发自己的创造力,并且挑战自我去实现它们。我很期待你的想法,所以新创意随时可以告诉我。 8 | 9 | 我们在客户端用 React,在服务端用 Express。因为 GraphQL 和 Apollo 库都跨框架的,你可以把它们和任意其它解决方案结合起来。如果你熟悉 Angular 或者 Vue,你可以把你所学的 GraphQL 和 Apollo 知识,迁移到这些框架上来构建客户端程序。如果你对在服务端用 Koa 或 Hapi 更感兴趣,你可以用这些中间件替代 Express 来做之前你在书中所做的。GraphQL 服务程序所使用的数据库也同样适用,书中你用 PostgreSQL 和 Sequelize ORM 去把你的 GraphQL 解析器连接到 PostgreSQL 数据库上。要不要用其它的比如 MongoDB 和 Mongoose 来替代 PostgreSQL 和 Sequelize, 你说了算。同样,Apollo 客户端和服务端的库也可以用生态里的相应的其它库来替代。你可以在本书的简介的章节里阅读它们。你也可以用 [Yoga GraphQL](https://github.com/prisma/graphql-yoga) 和 [Prisma](https://www.prisma.io/) 来启动和运行你的 GraphQL 服务器。本书里用到的所有技术都不是一成不变的,你可以替换它们来构建 GraphQL 的 JavaScript 应用程序。 10 | 11 | 我的最终建议是继续使用你在本书中所使用的 Apollo 服务端。因为它是一个实现你想法的理想入门工具。正如我之前提到的,你可以自由地替换那些封装在引擎盖下的技术,但最好根据你的应用特性有选择地聚焦。既然用户管理已经帮你实现了,你就可以开始添加你自己的特性了。 12 | -------------------------------------------------------------------------------- /manuscript/00-foreword/04-how-to-read-the-book.md: -------------------------------------------------------------------------------- 1 | ## 如何使用本书 2 | 3 | 没有人是能通过读一本书就学会编程的。因为编程是一门需要动手练习的学问,作为一个开发者,你需要不断的克服挑战来提高你的技能。此外,要想熟练的使用像函数式编程和面向对象这样的编程范式,更需要大量的实战经验;要想掌握现代 web 应用中如状态管理这样复杂的概念,亦或者像 React 和 Express 这样的框架,也要花大量的时间。所以你只有通过不断地实战演练才能真正地理解这些概念。 4 | 5 | 我希望这本书能够为你提供更多的实战经验和挑战来帮助你成为一名程序员。这些挑战旨在创造一个循序渐进的学习体验,每个挑战都契合你当前的学习进度。如果本书能够将挑战与你的技能水平控制在一个良好的平衡点上,那么你的学习体验应该是一个[循序渐进](https://www.robinwieruch.de/lessons-learned-deep-work-flow/)的过程。我是从《Deep Work》这本书中了解到的这种方式,当我阅读那它时,它的洞察令我赞叹,所以我希望把这种循序渐进的学习方式引进到这本书当中。 6 | 7 | ### 提示 & 技巧 8 | 9 | 正如前文提到的,本书有很多实战任务,这些任务引导你用学过的知识去解决问题。让你知道如何将理论知识运用在实战当中,这样可以加深你对抽象知识的理解,以便之后更好的运用在你自己的项目当中。 10 | 11 | 本书所有章节的内容都是基于之前章节的,所以在继续学习下一节内容之前确保自己已经消化了当前的内容。单独使用不同的知识点将提深你知识的广度,将不同的知识点结合在一起将提深你知识的深度。此外,记笔记也将有助于你消化课程的内容。在课程中记录下你的问题以便之后寻找答案。这些笔记也可能成为对本书的反馈,帮助我在之后的版本中提升课程质量。 12 | 13 | 我建议你自己敲样例代码而不是粘贴复制。敲代码有助于你找到语法错误和 bug,这样当用自定义数据跑样例代码时,你就能自己去修复出现的错误了。即使你遇到了 bug ,也不要灰心,通常情况下任何应用在变得复杂时都会出现 bug。而且在学习过程中,出现 bug 也是可以接受的,这样我们才有机会去学习如何修复它们。 14 | 15 | 对于材料的吸收学习,你需要用一个 IDE 打开它,然后键入样例代码并观察它的输出。我提供了一些额外的工具例如 [GraphQL Playground](https://github.com/prisma/graphql-playground),它可以展示样例代码如何在真实环境下运作。课程之余,你应该找时间将学到的工具和知识运用在你自己的项目上。若不学以致用,你将永远掌握不了它们。如果你还不知道自己该做一个什么项目,[这篇文章](https://www.robinwieruch.de/how-to-learn-framework/)可以帮助你找到它。 -------------------------------------------------------------------------------- /manuscript/00-foreword/02-requirements.md: -------------------------------------------------------------------------------- 1 | ## 基本要求 2 | 3 | 要想充分理解本书的内容,你应该熟悉基本的 Web 开发知识,其中包括 HTML,CSS 和 JavaScript。除此之外你也需要熟悉 [API](https://www.robinwieruch.de/what-is-an-api-javascript/) 这个术语,因为它会经常被提到。同时,我建议你加入本书的官方 [Slack 讨论组](https://slack-the-road-to-learn-react.wieruch.com/),在那里你可以帮助他人或者寻求帮助。 4 | 5 | ### 编辑器和终端 6 | 7 | 对于开发环境,你需要一个可用的编辑器或者IDE,以及一个终端(命令行工具),然后[参照我的环境搭建指南](https://www.robinwieruch.de/developer-setup/)配置环境。该指南适用于 MacOS 用户,但你也可以在上面找到一个 Windows 的搭建指南。 8 | 9 | ### React 10 | 11 | 在客户端上,本书使用了 React 来讲授 JavaScript 中的 GraphQL。我的另一本书《The Road to learn React》(《React 学习之道》)讲授了所有关于 React 的基础知识。它同时还讲了如何从 JavaScript ES5 迁移到 JavaScript ES6。那本书可以免费阅读,在看完它之后,你应该就能掌握本书中实现 GraphQL 客户端应用程序的所有基础知识。 12 | 13 | ### Node 14 | 15 | 至于服务器端上,本书使用了 Node 加 Express 作为库来讲授 JavaScript 中的 GraphQL。在将这些技术用于你的第一个支持 GraphQL 的应用程序之前,你不必了解太多关于它们的知识。本书将指导你完成用 Express 设置 Node 应用的过程,并且像你展示如何将 GraphQL 穿插其中。之后,在你的前端应用中就能够消费你的后端应用提供的 GraphQL API 了。 16 | 17 | ### Node and NPM 18 | 19 | 你需要安装 [node 和 npm](https://nodejs.org/en/),这两个工具都是用来管理你在本书中所用到的各种库的。本书中提及的 node 包需要通过 npm (node package manager,Node 包管理器)来安装,这些包可能是一些库,或者是集成在一起的框架。你可以在命令行验证 node 和 npm 的安装版本。如果没有看到任何输出结果,那就说明你需要安装他们,下面是我在写这本书的时候使用的版本: 20 | 21 | {title="Command Line",lang="text"} 22 | ~~~~~~~~ 23 | node --version 24 | *v10.11.0 25 | npm --version 26 | *v6.4.1 27 | ~~~~~~~~ 28 | 29 | 如果你看过《React 学习之道》,你应该已经熟悉整个设置过程了,因为那本书中也采用了 npm 生态系统。 -------------------------------------------------------------------------------- /manuscript/00-foreword/00-introduction.md: -------------------------------------------------------------------------------- 1 | # 简介 2 | 3 | 本书的主旨是 GraphQL 在现代应用中的使用。GraphQL 只是一种规范,虽然它可以运用在许多编程语言中,但本书将主要聚焦于 JavaScript 中的 GraphQL。GraphQL 规范的缔造者 Facebook,发布了用 JavaScript 实现 GraphQL 的样例,而用 JavaScript 的原因是:对于许多公司以及他们的客户端服务器架构来说 JavaScript 是近来最备受瞩目的编程语言和生态系统了。 4 | 5 | GraphQL 是一种可以用在任何地方的查询语言,但它通常用于客户端与服务器应用间的通讯。它并不局限于网络层协议的使用,所以数据可以在客户端和服务器应用间读写。它用于补足 JavaScript 中各种库和框架的网络协议栈。客户端可以用像 React、Angular、 Vue 等流行的解决方案,而服务端可以在 Node.js 的环境中使用 Express、Koa、 Hapi 等中间件库。有了这些前后端框架,剩下的就只是通过网路发送带有 GraphQL 指令的 HTTP 请求去链接两端了。 6 | 7 | GraphQL 的出现开启了 web 开发的新纪元。RESTful 应用是现在业界在客户端与服务器数据传输上最受欢迎的方式,但是现代的需求已经发生了改变。现在许多应用不得不处理多种客户端,比如桌面应用,web 应用,移动端应用,可穿戴设备应用,他们都需要服务端暴露 API。这就变成了两种选择,一种是用 REST 实现多个针对不同客户端的 API,而另一种则是用 GraphQL 实现一个对所有客户端应用都通用的 API,对于该如何选择就不言而喻了。但 GraphQL 不仅仅只是一个统一的接口,随着开源社区的发展,它的生态系统赋予了它强大的功能甚至更多的可能性。 8 | 9 | 我恰好有幸与一位 Java GraphQL 开源代码的贡献者共事过。他的努力使他成为了首批 GraphQL 开源代码的贡献者之一,并最终构建了可靠的 Java 实现。这段经历塑造了我自己对 GraphQL 的思考,而后当我的雇主为其企业级应用评估 GraphQL 时,又加深了我对它的思考。最初,我们客户端与服务器之间的粘合主要受 REST 的影响,我们拥有适用于所有 RESTful 资源的 API 端点,但最终遇到了问题,像 Facebook 这样的第三方客户端,无法消费我们的 API 。由于 API 端点过于严格,因此需要对它进行聚合和修改。 聚合意味着将更多的资源放入单个 API 端点,而修改则是为了提供 API 端点的变形,以满足对同一个请求资源的不同表示。我们引入了自己实现的方法去服务器请求资源,最终我们实现了简单的 GraphQL。在客户端方面,当时只有 Relay for React 这一个成熟的库按照消费 GraphQL API 来进行发布的,因此我们也投入了时间去研究它。最后,发现 GraphQL 尚处于发展阶段,因此我们推迟了将其引入我们的技术栈的决定。但不可否认,当时的我们都未料到它会变得如此强大且受欢迎。 10 | 11 | 因为这本书以 GraphQL 在客户端的应用为开端,所以你将使用 React 作为 UI 框架去消费 GraphQL API,被消费的是 GitHub 的 GraphQL API。它之所以能成为开发者们热衷选择的原因是:GitHub 是向公众发布 GraphQL API 的少数几家流行公司之一;GitHub 的稳定增长也为 GraphQL 本身带来了一定的可信度;还有就是第三方 API 通常会在刚开始的时候聚焦于客户端 GraphQL 应用上。此时本书讲授如何在客户端程序中去消费 GraphQL API 而无需你自己去实现一个 GraphQL 的服务器。 12 | 13 | 当本书内容转向服务器端的 GraphQL 时,我们将实现最终可由 GraphQL 客户端应用程序消费的 GraphQL API。你将实现如身份验证、数据库连接、分页这样强大的功能。最后,你应该能够牢牢掌握如何在 JavaScript 应用中使用 GraphQL 的知识。 -------------------------------------------------------------------------------- /manuscript/00-foreword/03-faq.md: -------------------------------------------------------------------------------- 1 | ## FAQ 2 | 3 | **为什么本书用 React 来讲授 GraphQL?** 4 | 5 | 在现代应用中 GraphQL 常被用来连接客户端和服务器应用。这些应用通常使用了像 React、Angular 和 Vue 这样框架来构建。所以对于 GraphQL 的教学来说,使用像 React 这样的在工程上真实应用的技术是合理的。我之所以选择 React 是因为它不仅 API 易用而且它还易学,在你的技术栈中,它仅仅是视图层。你可以在 [The Road to learn React](https://roadtoreact.com) ([React 学习之道](https://github.com/the-road-to-learn-react/the-road-to-learn-react-chinese.git))上面学习 React。当然,你也可以将你学到的 GraphQL 知识运用在其他客户端应用中。 6 | 7 | **为什么本书用 Node/Express 来讲 GraphQL?** 8 | 9 | 你可以用客户端应用来消费一个第三方的 GraphQL API,但你也可以在服务端实现你自己的 GraphQL API。因为使用同一个语言(JavaScript)的话会更加高效,所以我选择了 Node.js 作为后端。对于 Node.js 应用来说 Express 是最流行的选择,这就是为什么选择它而没有选择 Hapi 或者 Koa 的原因。 10 | 11 | **我如何获取更新?** 12 | 13 | 我通过两种方式来分享更新的内容,你可以用过[邮件订阅通知](https://www.getrevue.co/profile/rwieruch)或者在 [Twitter](https://twitter.com/rwieruch) 上关注我来获取更新。无论哪种方式,我都只会分享保证质量的内容。当你收到本书更改的提醒时,你就可以下载它最新的版本了。 14 | 15 | **我如何获取源码项目的权限?** 16 | 17 | 如果你购买了整个课程那其中已经授权了访问源码项目和任何其他附加文件的权限,你可以在你的[课程面板](https://roadtoreact.com/my-courses)上找到他们。如果你是通过非[官方平台](https://roadtoreact.com)购买的本课程,请创建一个官方平台账号,然后去 Admin 页面用其中的邮件模板和我联系。之后我会为你解锁课程。如果你没有购买完整的课程,你可以随时访问并升级你的课程来获得访问材料的权限。 18 | 19 | **如果我在亚马逊上购买了此书我们获得它的拷贝吗?** 20 | 21 | 如果你在亚马逊上购买了本书,你应该也可以在我的个人网站上看到这本书。由于我使用亚马逊来作为我免费知识变现的途径之一,我由衷的感谢你和你的支持,并且邀请你登录 [Road to React](https://roadtoreact.com),在那里你可以给我写一封邮件(Admin 页面),然后我可以为你解锁整个课程。除此之外,在这个平台上你可以随时下载最新版本的电子书。 22 | 23 | **在我读书期间遇到困难如何寻求帮助?** 24 | 25 | 这本书有一个为那些想要跟随同学和老司机一起读书的人创建的 [Slack 讨论组](https://slack-the-road-to-learn-react.wieruch.com/)。你可以加入这个频道去寻求帮助或者帮助他人。教学相长,帮助他人的过程中你也会有所收获。如何实在没有别的办法,你可以随时联系我。 26 | 27 | **如果我在书中发现一些问题,有地方可以帮助解决吗?** 28 | 29 | 如果你发现问题,请加入 Slack 聊天组。同时,检查本书在 GitHub 上的 [issues](https://github.com/the-road-to-graphql/the-road-to-graphql/issues) ([中文翻译版的 issues](https://github.com/the-road-to-graphql/the-road-to-graphql-chinese/issues))或者你将要构建的应用的仓库,看看针对你的问题是否已经有了解决方案。如果找不到同样的问题,请新建一个 issue 来解释你遇到的问题,比如提供一个截图和一些细节信息(比如页数、node 的版本等)。 30 | 31 | **如果我承担不起完整课程的费用怎么办呢?** 32 | 33 | 如果你承担不起完整的课程费用但想要学习这个课程,比如你是一个学生或者在你的国家这个课程的价格过于昂贵,那么请你联系我。我想支持任何可以提高我们开发者文化多样性的事业,所以如果你是少数名族或者在一个支持多样性的组织里工作,同时也想要学习这门课程,那么也请你联系我。 34 | 35 | **我能提供帮助来改进这本书吗?** 36 | 37 | 是的,我非常乐意收到你的反馈。你可以在 [GitHub](https://github.com/the-road-to-graphql/the-road-to-graphql)([中文翻译版](https://github.com/the-road-to-graphql/the-road-to-graphql-chinese))上提一个 issue 来表述在技术方面或者文本内容方面可以改进的地方。你也可以在 GitHub 上针对文件或者仓库提 pull request。 38 | 39 | **我如何为这个项目做出其他支持?** 40 | 41 | 如果你觉得我的课程有用并且想做出贡献,请在我网站的 [About Page](https://www.robinwieruch.de/about/) 上寻找如何提供支持的相关信息。读者们向他人传递良好的口碑也是非常有帮助的,这样其他人就可能发现提高他们 web 开发技能的方法。通过任何提供的渠道做贡献都将给予我更多的自由时间去创建更精深的课程,并且持续提供免费的材料。 42 | 43 | **有退款保证吗?** 44 | 45 | 有的,两个月之内,如果你觉得本书货次价高我将 100% 的退款。退款时请直接联系我。 46 | 47 | **你写这本书的动机是什么?** 48 | 49 | 我想要持续的讲授这个课程。因为我经常发现网上的资料得不到更新,或者只有一小部分话题能得到更新。人们常常很难找一个内容一致且实时更新的学习资源,而我则想要提供一个这样的学习体验。同时,我希望我能够通过这些课程的免费内容或[其他的方式](https://www.robinwieruch.de/giving-back-by-learning-react/)为那些不幸的人提供帮助。除此之外,我发现我最近非常满足于教授别人编程技巧,对于我来说它比任何朝九晚五的工作都要有意义。所以我希望将来也能在这条路上继续前行。 -------------------------------------------------------------------------------- /manuscript/03-graphql-setup/index.md: -------------------------------------------------------------------------------- 1 | # GraphQL 准备, 相关工具和 API 2 | 3 | 循序渐进通常是学习新东西最简单的方式,使用 JavaScript 来学习 GraphQL 是一件非常幸运的事,因为它同时教你应用程序的客户端和服务端。同时了解网络服务的前后端非常有用,但问题是你需要同时了解两个环境。因此这里采用循序渐进的方式会比较困难,所以我鼓励初学者从客户端应用程序开始,服务端则使用一个使用了 GraphQL 服务的第三方 API。 4 | 5 | [GitHub](https://github.com) 是首批采用 GraphQL 技术的大型科技公司之一。他们甚至[发布了](https://githubengineering.com/the-github-graphql-api)一个公共 GraphQL API [官方文档](https://developer.github.com/v4),这在开发者中非常受欢迎,因为大多数人使用 GitHub 来托管项目,对其非常熟悉。 6 | 7 | 在本章中,我希望能涵盖开始使用 GitHub GraphQL API 所需的一切,并通过使用该 API 从客户端的角度学习如何在 JavaScript 中使用 GraphQL。 你能够了解到 GitHub 的术语,以及如何使用 GraphQL API 来消费帐户数据。 从客户端的角度来看,我们将使用该 GraphQL API 实现一些应用程序,因此在本节中投入时间是有意义的,可以避免犯一些基础性的错误。 之后,我们会转向服务端,实现我们自己的 GraphQL 服务器。 8 | 9 | ## 利用 GitHub 数据来提供 API 10 | 11 | 如果你还有没 GitHub 账号,对它的生态系统了解也不多,请关注[这个 GitHub 官方的学习实验室](https://lab.github.com/)。如果想深入了解 Git 及其基本命令,请查看[本指南](https://www.robinwieruch.de/git-essential-commands/)。如果你想将来在 GitHub 上与其他人分享项目,会发现这些非常有用。这也是向潜在客户或招聘公司展示开发作品的好方式。 12 | 13 | 在我们与 GitHub GraphQL API 交互的过程中,你将使用自己的账号信息来进行读写操作。在此之前,为了能够在使用 API 的时候读到这些信息,请提供附加信息以完善 GitHub 的个人信息。 14 | 15 | ### 练习: 16 | 17 | * 如果没有 GitHub 账号的话,创建一个 18 | * 完善你的 GitHub 账号的其他信息 19 | 20 | ### GitHub 代码库 21 | 22 | 你还可以在 GitHub 上创建代码库。用他们的官方词汇来说:*“代码库是 GitHub 的基本元素。它们最容易被想象为项目的文件夹。代码库包含项目的所有文件(包括文档),并存储每个文件的修订历史。代码库可以有多个协作者,可以是公有仓库也可以是私有仓库。”*[GitHub 的术语表](https://help.github.com/articles/github-glossary/)解释了关键术语—— repository,issue,clone,fork,push ——在接下来了解 GraphQL 的章节中会使用到这些术语。基本上来说代码库是一个可以和他人分享应用程序源码的地方。我鼓励你将一些项目放在 GitHub 代码库中,以便以后可以使用 GraphQL API 来访问它们。 23 | 24 | 如果你没有上传任何项目,你可以随时 “fork” 其他 GitHub 用户的代码库,并在其副本上进行操作。fork 大体来说是其他代码库的克隆,可以让你在不改变原始仓库的基础上进行添加修改。GitHub 上有很多开放的代码库,可以克隆到本地或者 fork 到你的代码库列表中,这样你就可以通过实验来了解它们的机制。例如,如果你访问[我的 GitHub 主页](https://github.com/rwieruch),你可以看到我所有的代码库,但并非所有的代码库都是我的,因为有些是从别人那儿 fork 来的。如果你想使用它们来进行练习,或者通过 GitHub 的 GraphQL API 来访问,请随意 fork 这些仓库。 25 | 26 | ### 练习: 27 | 28 | * 创建或者 fork 几个 GitHub 代码库,并验证它们是否以副本的形式存在于你的账户中。副本通过用户名标识,后面跟着代码库的名称,共同组成了该代码库的全称。例如,一个名为 *原作者名字/TestRepo* 的代码库,在你 fork 之后,会被命名为 *你的名字/TestRepo*。 29 | 30 | ### 分页数据 31 | 32 | GitHub 的 GraphQL API 允许一次请求多个代码库,这对于分页来说非常有用。分页是为处理大量数据而发明的一种编程机制。例如,假设你的 GitHub 账号拥有超过一百个代码库,但是 UI 仅显示其中的 10 个。因为一次只需要一部分数据,所以每次请求都返回整个列表的数据是不切实际且低效的,而分页可以解决这样的问题。 33 | 34 | 通过使用 GitHub 的 GraphQL API 进行分页,你可以按需调整,所以请务必按照自己的需求(例如个人或组织账号中的代码库数量)来调整数字(例如每页数量,偏移)。你需要拥有足够多的代码库才能在实际中看到分页功能,如果每页显示 10 个的话,我推荐超过 20 个代码库,如果每页显示两个的话,建议 5 个以上。 35 | 36 | ### Issue 和 Pull Request 37 | 38 | 一旦你深入了解了 GitHub 的 GraphQL API 并开始请求一些嵌套关系的数据(例如代码库的 issue,pull request 等),请确保代码库中存在一些 issue 和 pull request,这样在实现获取代码库 issue 的功能时才能够看到结果。请求一个组织的代码库可能会更好,因为其一般都有较多的 issue 和 pull request。 39 | 40 | ### 练习: 41 | 42 | * 在 [GitHub 的词汇表](https://help.github.com/articles/github-glossary/)中查阅更多信息。 思考以下问题: 43 | * 什么是 GitHub 的组织账号和个人账号? 44 | * 什么是 GitHub 的组织用户和个人用户? 45 | * 什么是代码库,issue 和 pull request? 46 | * 什么是 GitHub 代码库的 star 和 关注者(watcher)? 47 | * 创建或者 fork 足够多的代码库,来使用分页功能。 48 | * 在你的 GitHub 代码库中创建 pull request 和 issue。 49 | 50 | ## 使用 GitHub 的 access token 来读写数据 51 | 52 | 为了使用 GitHub GraphQL API,你需要在他们的网站上生成一个 access token。Access token 授权用户与数据间的交互,使其能够对其所拥有的数据进行读写操作。[按照分步说明](https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line)来获得个人 access token,并检查所需的权限范围,因为之后会利用它来实现一个多功能的 GitHub 客户端。 53 | 54 | ![](images/github-personal-access-token_1024.jpg) 55 | 56 | Access token 随后会用于与 GitHub GraphQL API 进行交互。请注意不要与任何第三方共享这些 access token。 57 | 58 | ## 与 GitHub GraphQL API 交互 59 | 60 | 有两种常见的方法可以与 GitHub GraphQL API 进行交互,而且无需为其编写任何源代码。第一种是使用 [GitHub GraphQL Explorer](https://developer.github.com/v4/explorer/)。只需要使用 GitHub 账号登录就可以使用 GraphQL API 进行数据查询或变更操作,这是一个简化初次体验的好方式。第二种是使用一个应用程序形式的通用客户端。GraphiQL 是一个客户端,可以让 GraphQL 请求功能集成到你的应用,或者作为一个独立的应用程序。前者可以通过[在应用程序中直接设置 GraphiQL](https://github.com/skevy/graphiql-app) 来实现;后者可以通过[将 GraphiQL 作为独立的应用](https://github.com/skevy/graphiql-app)来实现,更容易使用。它是一个关于 GraphiQL 的轻量级 shell,可以手动或通过命令行下载和安装。 61 | 62 | 因为需要注册才能使用,所以 GitHub GraphQL Explorer 知道你的凭证,但 GraphiQL 也需要知道你创建的 access token。你可以在 header 配置中为每个请求的 HTTP header 中添加 access token。 63 | 64 | ![](images/graphiql-headers_1024.jpg) 65 | 66 | 在下一步中,我们在 GraphiQL 配置中添加一个键值对。为了与 GitHub GraphQL API 通信,在头部加上 “Authorization”,值为 “bearer [你的 access token]”。为 GraphiQL 应用程序保存此设定。 这样,你就准备好使用 GraphiQL 应用程序向 GitHub GraphQL API 发送请求了。 67 | 68 | ![](images/graphiql-authorization_1024.jpg) 69 | 70 | 如果你使用自己的 GraphiQL 应用程序,则需要为 GitHub 的 GraphQL API 提供 GraphQL 端点:`https://api.github.com/graphql`。 对于 GitHub GraphQL API,使用 [HTTP POST 方法](https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Request_methods)进行查询和变更,并将数据作为负载进行传输。 71 | 72 | 本节提供了两种与 GitHub GraphQL API 交互的方法。GitHub 的 GraphQL Explorer 只能用于 GitHub API,集成到应用程序或独立的 GraphiQL 可用于任何的 GraphQL API。不同点在于后者需要一些额外的配置。GitHub GraphQL Explorer 实际上只是为使用 GitHub GraphQL API 而定制的独立 GraphiQL 应用程序。 73 | 74 | | | 75 | 76 | 为了使用 GitHub GraphQL API 来了解 GraphQL,在设置好 GitHub 之后,你已经可以开始实现你的第一个可交互的 GraphQL 客户端了。继续阅读,利用你刚设置好的工具和 React 来创建你的第一个 GraphQL 客户端应用。 77 | -------------------------------------------------------------------------------- /manuscript/02-apollo/index.md: -------------------------------------------------------------------------------- 1 | # Apollo 2 | 3 | ​在给定问题上找到正确的解决方案并不总是那么简单,使用 GrahpQL 构建 Web 应用程序就是一个很好的例子来说明在不断变化的时代中如何应对源源不断的挑战。此外,不断变化的挑战也造成了必须不断进化的解决方案,因此即使作出选择也成为了一项任务。这篇文章将会解读用于 GraphQL 的 Apollo 的解决方案的优缺点,以及你决定使用它时的替代解决方案。 4 | 5 | ​GraphQL 作为一个查询语言,不仅在 JavaScript 中有一个可供参考的实现,Apollo 在其之上构建了它自己的生态系统,以使 GraphQL 可供更广泛的用户使用。它包括客户端和服务端,他们为两端都提供了一个庞大的库生态系统。这些库也提供了一个中间层 — Apollo Engine,它是一个 GraphQL 网关。本质上讲,Apollo 能成为在 JavaScript 应用中使用 GraphQL 的最流行选择之一也是有原因的。 6 | 7 | ## Apollo 优势 8 | 9 | ​为了更全面的对比,接下来我们来看看使用 Apollo 的一些优势。如果你认为这些列表里有任何遗漏,请随时联系我。 10 | 11 | ### Apollo 的生态圈 12 | 13 | ​虽然 GraphQL 还处在早期阶段,但 Apollo 生态圈为其他许多挑战提供了解决方案。除此之外,我们可以看到生态圈的增长,因为在每一次技术会议上,相关公司都会宣布 Apollo 和使用了 Apollo 技术栈的库的一些更新。然而,Apollo 不只是涵盖了 GraphQL;他们还花费精力投入 REST 接口,以便向后兼容 RESTful 架构。这甚至使 GraphQL 超越了网络层和远程数据,也为本地数据提供了状态管理解决方案。 14 | 15 | ### Apollo 背后的公司和社区 16 | 17 | ​Apollo 背后的公司正在为其成功倾注大量资源。他们也积极参与开源,提供了很多深入讲解他们产品的文章,并得到了技术会议的支持。总的来说,GraphQL 生态系统[似乎在未来处于良好的状况](https://techcrunch.com/2018/05/15/prisma)。随着越来越多的开发人员接受将 Apollo 用于客户端和 JavaScript 服务端应用程序,GraphQL 背后的社区正在成长。 18 | 19 | ### 谁在使用 Apollo ? 20 | 21 | 有很多拥有成熟技术的公司已经在投入 Apollo 的使用了,他们以前熟悉的或许是像 Meteor 那样的流行框架,但像 Airbnb 和 Twitch 这样的新兴并极受欢迎的公司已经在使用它了。以下为部分案例: 22 | 23 | * Airbnb [[1]](https://medium.com/airbnb-engineering/reconciling-graphql-and-thrift-at-airbnb-a97e8d290712) [[2]](https://youtu.be/oBOSJFkrNqc) 24 | * [Twitch](https://about.sourcegraph.com/graphql/twitch-our-graphql-transformation) 25 | * [The New York Times](https://open.nytimes.com/the-new-york-times-now-on-apollo-b9a78a5038c) 26 | * [KLM](https://youtu.be/T2njjXHdKqw) 27 | * [Medium](https://www.infoq.com/news/2018/05/medium-reactjs-graphql-migration) 28 | 29 | ### Apollo 的文档 30 | 31 | ​虽然 Apollo 持续在发展,但它背后的团队和社区仍然使文档保持最新,他们有许多关于如何构建应用程序的洞见。事实上,他们已经覆盖了初学者所能接触到的绝大多数领域 32 | 33 | ### Apollo 库 34 | 35 | ​Apollo 提供了大量的库,用于为 JavaScript 应用程序实现更高效的 GraphQL 技术栈,并且他们的库都是开源的,以便更易于管理。例如:[Apollo Link](https://www.apollographql.com/docs/link/) 提供了一个 API 用于将不同功能链接到 GraphQL 的控制流中。这使得自动的网络重试或 RESTful API 端点替代 GraphQL 端点成为可能(这些端点也可以一起使用)。 36 | 37 | ​ Apollo 还提供了可替换的库,可以在 Apollo 客户端缓存中看到。Apollo 客户端本身是不会偏向其存储数据的缓存的,就像 Apollo 或者其社区宣传的任何缓存一样。已有可用缓存可以用于设置 Apollo 客户端实例。 38 | 39 | ### Apollo 的功能 40 | 41 | ​Apollo 内置了许多特性将复杂性从应用间抽离出来,并且处理客户端和服务端应用程序之间的交集。例如: Apollo 客户端缓存请求,从而使当缓存中已经存在结果时,请求不会被发送两次。该功能可为应用程序提供性能提升,节省宝贵的网络流量。并且, Apollo 客户端会规范化数据,从而使 GraphQL 查询中嵌套的数据被存储在 Apollo 客户端缓存中的规范化数据结构中。数据可以通过标识符从 Apollo 客户端缓存中被读取,而不需要在 "author" 实体中查找 "article" 实体。除了缓存和规范化, Apollo 客户端还有很多新功能,比如:错误管理,支持分页和乐观 UI ,预获取数据,将数据层(Apollo 客户端)连接到视图层(如:React)。 42 | 43 | ### 与其他框架协作 44 | 45 | ​Apollo 中有一个库可以使 Apollo 客户端连接 React。 就像 Redux 和 MobX 这样的库一样。React-Apollo 有高阶组件和 render prop 组件来连接彼此。但是,还有其他的库不仅可以连接 Apollo 客户端和 React,还可以连接 Apollo 到 Angular 或者 Vue。这就是使 Apollo 客户端视图层没有限制的原因,这对 JavaScript 生态圈的增长非常有用。 46 | 47 | ​Apollo 在服务端也与库无关,它提供了几种与 Node.js 库连接的解决方案。基于 Express.js 的 Apollo 服务端是最受开发人员和公司欢迎的选择之一,而且还有其他针对 Apollo 服务端的解决方案,如使用 Node.js 的 Koa 和 Hapi。 48 | 49 | ### 使用 Apollo 处理现代数据 50 | 51 | ​还记得我们必须在组件的生命周期方法中强制触发数据获取么?Apollo 客户端解决了这个问题,因为它的数据查询是声明式的。React 经常使用高阶组件或者 render prop 的方式在组件渲染时去自动触发查询。GraphQL 变更是强制触发的,但是那只是因为高阶组件或者 render prop 授予了执行变更的函数权限(比如:在点击按钮时)。从本质上讲,Apollo 信奉声明式编程多过命令式编程。 52 | 53 | ### 使用 GraphQL 和 Apollo 管理现代状态 54 | 55 | ​随着 GraphQL 在 JavaScript 应用程序中的兴起, 状态管理进入了另一种混乱状态。即使使用像 Apollo 客户端一样的 GraphQL 库消除了很多痛点,因为它负责远程数据的状态管理,一些开发者会对在哪里放置像 Redux 或者 MobX 这样的库而感到困惑。但是,只需要将这些库用于本地数据管理并将远端数据留给 Apollo 即可。不再需要在 Redux 中通过异步 actions 获取数据了,因此它成了一个可预测所有剩余应用状态(如:本地数据/视图数据/UI 数据)的容器。事实上,剩余应用状态可能足够简单到只需要去使用 React 本身的状态管理而不是 Redux。 56 | 57 | ​与此同时,Apollo 已经发布了它自己的解决方案,即通过 GraphQL 来处理所有事情来管理原本应该由 React,Redux 或者 MobX 来管理的本地状态。Apollo Link State 库使我们可以通过 GraphQL 的操作来管理本地数据,但 Apollo 的客户端除外。Apollo 说:"你不在需要其他的状态管理库,我们会处理你的数据"。这些是开发 JavaScript 应用程序的激动人心的时刻。 58 | 59 | ### 便捷的开发体验 60 | 61 | ​使用 Apollo 来写 JavaScript 应用程序变得越来越容易。社区正在推出实现工具。有很多开发工具可以用作浏览器插件,第三方工具来进行 GraphQL 的操作,比如: GraphiQL,以及用于简化开发 Apollo 应用程序的库。例如:Apollo Boost 库提供了一个几乎零配置的 Apollo 客户端设置,以便客户端应用程序开始使用 GraphQL。Apollo 删除了 JavaScript 中 GraphQL 参考实现附带的所有样板实现。 62 | 63 | ## Apollo 劣势 64 | 65 | ​为了更全面的对比,接下来我们来看看使用 Apollo 的一些缺点。如果您认为任何一个列表里缺少了任何内容,请随时与我联系。 66 | 67 | ### 风险 68 | 69 | ​GraphQL 仍处在早期阶段。Apollo 的用户和所有早期 GraphQL 的采用者都在使用全新的技术。Apollo 团队正在围绕 GraphQL 开发一个丰富的生态系统,提供基础功能和像缓存以及监控一样的高级功能。这伴随着未知的困难,主要因为一切都不是一成不变的。当你更新 GraphQL 相关库时,有些零星的改动可能会带来挑战。相比之下,GraphQL 中的库可能比 Apollo 团队更加保守,但是提供的这些功能也通常没那么强大。 70 | 71 | ​ 快速发展也阻碍了开发者继续学习的能力。GraphQL 和 Apollo 的教程有时已经过时,找到答案可能需要外部资源。不过,大多数的新技术都是这样。 72 | 73 | ### 在建设中 74 | 75 | ​Apollo 团队和社区快速实现了很多新功能,但是速度如此之快也需要付出代价。搜索解决方案经常会导向 GitHub, 因为关于该主题的其他信息很少。尽管你确实可以找到关于你的问题的 GitHub issue,但是通常没有解决方案。 76 | 77 | ​快速开发还伴随着忽视过早期版本的代价。据我的经验,[人们对于 Apollo 放弃 Redux 作为内部状态管理的解决方案感到困惑](https://github.com/apollographql/apollo-client/issues/2593)。Apollo 并没有对 Redux 应该如何与它一起使用发表意见,但是因为它的内部已经决定放弃 Redux 作为内部状态管理的解决方案,当 Apollo 2.0 发布时,许多人并不知道如何继续。我认为 Apollo 背后的团队可能正在努力跟上快节奏的 GraphQL 生态系统,但是能够注意到开源开发中的所有声音并不是那么容易。 78 | 79 | ### 它大胆且时尚 80 | 81 | ​Apollo 很大胆,因为它不仅是 JavaScript 中 GraphQL 的客户端和服务端间的网络层生态系统,还将自己定位为未来的数据管理解决方案。它使用 GraphQL,用于 RESTful API 的 apollo-link-rest ,以及用于本地状态管理的 apollo-link-state 来连接客户端和后端应用程序。一些专家对于 "GraphQL 一切"持怀疑态度,但是时间会证明它是否影响市场。 82 | 83 | ​Apollo 很时尚,因为它能跟得上最新趋势。在 React 中,最新趋势是 render prop 组件。正应如此,可以说 render prop 组件的好处是多于高阶组件的,React Apollo 库紧挨着高阶组件介绍了 [render prop 组件](https://www.robinwieruch.de/react-render-props-pattern/)。提供多种解决方案是个非常明智之举,因为高阶和 render prop 组件都有各自的优缺点。但是,Apollo 确实提倡 render props 多于高阶组件,不清楚这是否是炒作驱动开发或营销或是他们真的相信这是未来之路。Render props 在 React 中相对较新,所以会开发人员需要花时间才能意识到他们自己带来的缺陷(参见:高阶组件)。我已经看见 React 应用程序因为在一个 React 组件中使用多个 render prop 组件而变得越来越冗长,尽管一个 render prop 不会依赖另一个 render prop,而不是通过使用高阶组件来协同 React 组件。毕竟,Apollo 提供了两种解决方案, rende props 和高阶组件,所以开发者可以根据具体情况做决定。对于用户来说,这是一个好兆头,Apollo 团队正在跟上其他库的最新趋势,而不是把自己局限在泡沫里。 84 | 85 | ### 缺少竞争 86 | 87 | ​这些问题大都与 GraphQL 的新特性有关,这些问题可以应用于同一领域的几乎任何其他开源解决方案。但是,其中主要的一个问题是 GraphQL 在 JavaScript 领域中缺少竞争。下一节将会罗列几种 Apollo 的替代方案,但是与 Apollo 的生态系统相比,它们是很有限的。虽然可以为 GraphQL 写你自己的库(比如: React 客户端中一个简单的 GraphQL),但是并没有很多开发者尝试过。Apollo 解决的问题并非微不足道,但是我认为竞争对于 JavaScript 生态圈中的 GraphQL 来说是一个健康的推动。现在 GraphQL 有巨大的潜力,开源开发者使用它非常的明智。 88 | 89 | ## 针对 JavaScript, React 和 Node.js 的 Apollo 替代选择 90 | 91 | ​有一些缺点源于使用 GraphQL 作为 RESTful 驱动架构的替代方案。在 JavaScript 中也有一些替代 Apollo 客户端和 Apollo 服务端去消费 GraphQL APIs 的方案。以下列表应该会提供有关 JavaScript 生态系统中解决方案的洞见,用于 React 在客户端和 Node.js 在服务端的场景。 92 | 93 | ### 针对 React 的 Apollo Client 替代选择 94 | 95 | ​对于 React,Angular,Vue 或者类似应用程序的 [Apollo 客户端](https://github.com/apollographql/apollo-client),有一些替代选择。像 Apollo 一样,他们都有各自的优缺点。 96 | 97 | * 普通的 HTTP 请求:尽管可以使用复杂的 GraphQL 库来操作 GraphQL,GraphQL 并不关注它自身的网络层。因此你可以将 GraphQL 和普通的 HTTP 方法结合使用,只使用一个端点 ,其中包括固定的针对 GraphQL 查询和变更的负载结构。 98 | 99 | * [Relay]((https://github.com/facebook/relay)): Relay 是 Facebook 的一个在 React 应用程序客户端中使用 GraphQL 的库。它是出现在 Apollo 之前的第一批 GraphQL 客户端库。 100 | 101 | * [urql]((https://github.com/FormidableLabs/urql)): urql 是来自 Formidable Labs 的 GraphQL 客户端库,用于在 React 应用程序中使用 GraphQL。它是开源的,是日益壮大的 Apollo 的简化替代品。 102 | 103 | * [graphql.js](https://github.com/f/graphql.js/): graphql.js 不应该被误解为 GraphQL 的参考实现。它就是个简单的,服务于没有 Vue,React 或 Angular 这样的强大的库的应用程序的 GraphQL 客户端。 104 | 105 | * [AWS Amplify - GraphQL 客户端](https://github.com/aws/aws-amplify): AWS Amplify 系列为支持云的应用提供库。其中一个模块是被用于常规 GraphQL 服务或 AWS AppSync APIs 的 GraphQL 客户端。 106 | 107 | ### 针对 Node.js 的 Apollo Server 替代选择 108 | 109 | ​对于 Express,Koa,Hapi 或者其他 Node.js 库的 [Apollo 服务端](https://github.com/apollographql/apollo-server),还有一些替代方案可以查看。显然它们都有自己的优劣势,然而并没有在这里讨论。 110 | 111 | * [express-graphql](https://github.com/graphql/express-graphql): 这个库提供了一个低级 API 去连接 GraphQL 层到 Express 的中间件。它使用纯 GraphQL.js 参考实现来定义 GraphQL 的概要,而 Apollo 服务端却为开发者简单化了它。 112 | 113 | * [graphql-yoga](https://github.com/prisma/graphql-yoga): 一个专注于简单启动,性能和出色的开发者体验的功能完整的 GraphQL 服务。它建立在其他的GraphQL 库之上,可以为我们减少样板代码。 114 | 115 | | | 116 | 117 | 当你想用 GraphQL 接口多于 RESTful 接口时,有很多原因去使用 Apollo 及其为 JavaScript 应用程序开发的生态系统。它们的库与框架无关,因此它们可以和各种客户端框架,如:React, Angular, Vue 和服务端应用程序,如:Express,Koa,Hapi 一起使用。 -------------------------------------------------------------------------------- /manuscript/01-graphql/index.md: -------------------------------------------------------------------------------- 1 | # GraphQL 2 | 3 | 对于客户端和服务器应用间的网络请求来说,[REST](https://en.wikipedia.org/wiki/Representational_state_transfer) 是用来连接两端最流行的选择。在 REST 的世界中,一切都围绕着“可被 URL 访问的资源”来发展。你可以通过 HTTP GET 方法请求来读取资源,也可以通过 HTTP POST 方法请求新建资源,亦或者通过 HTTP PUT 和 DELETE 方法请求更新或删除资源。这些操作被称为增删查改(CRUD Create,Read,Update,Delete)。资源可以是来自 authors,articles 或者 users 的任何东西。REST 并不限制用于传输数据的格式,但多数情况下人们会用 JSON。总之,REST 使应用之间能通过用 URL 和 HTTP 方法进行交流。 4 | 5 | {title="Code Playground",lang="json"} 6 | ~~~~~~~~ 7 | // a RESTful request with HTTP GET 8 | https://api.domain.com/authors/7 9 | 10 | // the response in JSON 11 | { 12 | "id": "7", 13 | "name": "Robin Wieruch", 14 | "avatarUrl": "https://domain.com/authors/7", 15 | "firstName": "Robin", 16 | "lastName": "Wieruch" 17 | } 18 | ~~~~~~~~ 19 | 20 | 尽管 REST 已经成为现行通用标准很长一段时间了,但最近出现了一个来自 Facebook 的技术,GraphQL,它很可能成为其潜在的继任者。接下来的部分将介绍 GraphQL 的优劣势,以便能提供给需要 REST 替代方案的开发人员更多选择。 21 | 22 | ## 什么是 GraphQL? 23 | 24 | 简要来说,GraphQL 是一种由 Facebook 开源的**查询语言**,Facebook 毫无疑问是一家处于 web 软件开发顶尖的公司。虽然 GraphQL 开源于 2015 年,但 Facebook 从 2012 年开始就在内部的移动开发应用中使用它,并把它当做 REST 架构的替代方案。它允许请求声明想要的数据,这让客户端能更好的控制(服务器)发什么样的数据回来。这点对于 RESTful 架构来说就很难实现了,因为后端在每一个 URL 上都定义了固定数据的资源,所以即使前端只需要一部分数据,它也不得不请求一个资源上的所有数据。这个问题被称之为数据冗余(overfetching)。在最糟的情况下,一个客户端应用不得不通过多次请求去获取多个资源。这也是数据冗余,它同时还增加了对瀑布网络的请求数。如果将一个像 GraphQL 这样的查询语言应用在服务器端和客户端上的话,客户端发送一个请求就可以获取所有需要的数据了。最终,Facebook 移动端应用的网络流量显著减少,因为 GraphQL 使数据传输变得更加高效了。 25 | 26 | Facebook 开源了 GraphQL 的规范以及它的在 JavaScript 上的实现范例,之后多个主流语言也根据这个规范实现了 GraphQL。GraphQL 的生态系统通过对多种语言的实现得到了广度发展,但同时,随着像 Apollo 和 Relay 这样的基于 GraphQL 的库的出现,它也得到了深度发展。 27 | 28 | 一个 GraphQL 操作可以是查询(query),变更(mutation)或者订阅(subscription)。其实每一个操作只是一个符合 GraphQL 查询语言规范的字符串而已。幸运的是,GraphQL 还在不停的发展,所以在未来可能会出现更多的操作。 29 | 30 | 一旦 GraphQL 的操作到达后端,GraphQL schema 就会针对它进行匹配,然后为前端解析数据。GraphQL 不指定特殊的网络协议,但通常会使用 HTTP,它也不指定特殊的负载(payload),但通常会使用 JSON。它也完全与应用的架构无关。总之它就只是个查询语言。 31 | 32 | {title="Code Playground",lang="json"} 33 | ~~~~~~~~ 34 | // a GraphQL query 35 | author(id: "7") { 36 | id 37 | name 38 | avatarUrl 39 | articles(limit: 2) { 40 | name 41 | urlSlug 42 | } 43 | } 44 | 45 | // a GraphQL query result 46 | { 47 | "data": { 48 | "author": { 49 | "id": "7", 50 | "name": "Robin Wieruch", 51 | "avatarUrl": "https://domain.com/authors/7", 52 | "articles": [ 53 | { 54 | "name": "The Road to learn React", 55 | "urlSlug": "the-road-to-learn-react" 56 | }, 57 | { 58 | "name": "React Testing Tutorial", 59 | "urlSlug": "react-testing-tutorial" 60 | } 61 | ] 62 | } 63 | } 64 | } 65 | ~~~~~~~~ 66 | 67 | 在 GraphQL 的调用中,一次查询就已经请求了多个资源(author,article),并且仅仅查询了 article 的特定的字段(name,urlSlug),即使在它的 schema 中提供了更多的数据。RESTful 架构的话至少需要两次瀑布请求分别去获取 author 和 articles,但是 GraphQL 实现了一步到位。此外,这个请求仅仅选择了必要的字段而非整个实体(entity)。 68 | 69 | 下面是对 GraphQL 的概括。服务器应用提供一个 GraphQL schema,这个 schema 定义了所有可以利用的数据的结构和类型,而客户端应用只需请求必要的数据。 70 | 71 | ## GraphQL 的优势 72 | 73 | 下面将列出在应用中使用 GraphQL 的主要优势。 74 | 75 | ### 声明式数据获取 76 | 77 | 正如前文所示,GraphQL 可以在查询中声明想要获取的数据,并且客户端在一次查询中可以跨资源的选择实体和字段。举个例子,Airbnb 是这样使用 GraphQL 的,由 GraphQL 决定为 UI 获取哪些字段,这样的方式就像 UI 去驱动数据的获取 (UI-driven data fetching)。在 Airbnb 上,一个搜索页面的搜索结果通常包括房屋、入住体验以及其他特定领域的信息。要想通过一次请求就获取所有数据,只为 UI 定制化数据的 GraphQL 查询是完美的选择。这样的方式也让前后端很好的各司其职:客户端知道自己对数据的需求;服务器知道数据结构并且知道如何从数据源(如:数据库,微服务,第三方 API)去解析数据。 78 | 79 | ### 没有数据冗余 80 | 81 | 对于 GraphQL 来说不存在数据冗余的情况。如果移动端使用为 web 端设计的 RESTful API,通常是会出现数据冗余的。但是用 GraphQL 的话,移动端就可以选择不同的字段组合,也就是说它可以只获取需要用来显示在屏幕上的信息。 82 | 83 | ### 独立性 84 | 85 | GraphQL 不仅仅是为了 React 开发者而存在的。虽然 Facebook 使用 React 客户端应用展示了 GraphQL,但它独立于任何前端或者后端的解决方案。由于 GraphQL 的范例是用 JavaScript 写的,所以像 Angular,Vue,Express,Hapi,Koa 等 JavaScript 的前后端库都可以使用 GraphQL,而且这仅仅只是 JavaScript 的生态系统。就在两个实体间(比如客户端和服务器)无关编程语言这一点上,GraphQL 确实模仿了 REST。 86 | 87 | ### 谁在使用 GraphQL 88 | 89 | 虽然 GraphQL 规范以及它在 JavaScript 上实现的背后驱动者是 Facebook,但还有许多其它的著名公司也正在将它运用在各自的应用上。由于来自于现代应用的强烈需求,这些公司也在 GraphQL 上投入了人力物力。除了 Facebook 以外,以下这些著名公司都使用了 GraphQL: 90 | 91 | * GitHub [[1]](https://githubengineering.com/the-github-graphql-api/) [[2]](https://youtu.be/lj41qhtkggU) 92 | * Shopify [[1]](https://shopifyengineering.myshopify.com/blogs/engineering/solving-the-n-1-problem-for-graphql-through-batching) [[2]](https://youtu.be/2It9NofBWYg) 93 | * [Twitter](https://www.youtube.com/watch?v=Baw05hrOUNM) 94 | * [Coursera](https://youtu.be/F329W0PR6ds) 95 | * [Yelp](https://youtu.be/bqcRQYTNCOA) 96 | * [Wordpress](https://youtu.be/v3xY-rCsUYM) 97 | * [The New York Times](https://youtu.be/W-u-vZUSnIk) 98 | * [Samsara](https://youtu.be/g-asVW9JFPw) 99 | * [查看更多](https://graphql.org/users/) 100 | 101 | 当 GraphQL 被 Facebook 开源时,其它公司在移动应用上也面临着同样的问题。此时 Netflix 提出了 [Falcor](https://github.com/Netflix/falcor),一种 GraphQL 的备选方案。这再次证明了现代应用需要像 GraphQL 和 Falcor 这样的解决方案。 102 | 103 | ### 单一数据源 104 | 105 | GraphQL 模式是 GraphQL 应用中的单一数据源。它提供一个中心位置,在这个中心位置上描述了所有可用的数据。GraphQL 模式通常在服务端定义,但是客户端可用基于模式对数据进行读(查询)写(变更)操作。总的来说,服务端应用提供它所有可以使用的数据,然后客户端应用通过 GraphQL 查询请求部分数据,或者通过 GraphQL 变更修改部分数据。 106 | 107 | ### GraphQL 符合现在发展趋势 108 | 109 | GraphQL 符合现代应用开发的趋势。你可能只有一个后端应用,但有多个与之对应的客户端,例如 web 上,手机上以及智能手表上,这些客户端都依赖于这一个后端应用的数据。GraphQL 不仅可以用来连接前后端,也能满足每一个客户端应用的需求:网络流量的需求,有嵌套关系的数据,只请求需要的数据;并且不需要为每一个客户端定制化 API。在服务器端,它可能是一个完整的后端,但也可能是一组提供不同功能的微服务。在微服务的情况下可以将 GraphQL 模式的拼接特性运用到极致——你可以将所有的功能聚合到一个 GraphQL 模式中。 110 | 111 | ### 拼接性 112 | 113 | 模式拼接特性让多个模式组合成一个模式成为可能。设想一下,你的后端是一个微服务架构,而每一个微服务都处理特定领域的数据和逻辑。在这种情况下,每一个微服务都会定义他自己的 GraphQL 模式,之后你可以使用模式拼接将它们组合成一个客户端可以访问的模式。每个微服务都可以拥有自己的 GraphQL 端点,然后由一个 GraphQL API 网关将所有的模式组合起来形成一个全局的模式。 114 | 115 | ### 内省系统 116 | 117 | 通过 GraphQL 的内省系统可以从 GraphQL API 获取 GraphQL schema。因为模式包含了所有 GraphQL API 上可以使用的数据,所以它可以很好地自动化生成 API 文档。在测试的时候或者在从多个微服务获取模式进行拼接的时候,它也可以用来模拟 GraphQL 客户端。 118 | 119 | ### GraphQL 是强类型的 120 | 121 | GraphQL 是强类型的查询语言因为它是由富有表现力的 GraphQL 模式定义语言(SDL,Schema Definition Language)来写的。强类型使 GraphQL 更少出错,在编译时能被校验,并且可以使它支持与编译器和 IDE 的集成,例如自动补全和校验。 122 | 123 | ### 版本控制 124 | 125 | 在 GraphQL 中并没有像 REST 使用的 API 版本控制规则。在 REST 中通常提供多个 API 的版本(比如 api.domain.com/v1/,api.domain.com/v2/)进行版本控制,因为资源或者资源的结构随着时间在不断的改变。但对于 GraphQL 来说,它可以声明废弃一个字段。之后,客户端在查询这个废弃的字段时将收到一个废弃警告。一段时间过后当大部分客户端不再使用这个废弃的字段时,它可能会从 schema 中被移除。这使得 GraphQL API 在不需要多个版本共存的情况下就可以随着时间不断的演进。 126 | 127 | ### GraphQL 的生态系统正在不断成长 128 | 129 | GraphQL 的生态系统在不断成长。现在不仅仅有为 GraphQL 强类型特性在编辑器和 IDE 上集成的插件,还有针对 GraphQL 本身独立的应用。可能你知道 REST API 有 [Postman](https://www.getpostman.com),类似的,现在 GraphQL API 也有 [GraphiQL](https://github.com/graphql/graphiql) 和 [GraphQL Playground](https://github.com/prismagraphql/graphql-playground)。还有像 [Gatsby.js](https://www.gatsbyjs.org/) 这样的库,它是为了能在 React 上使用 GraphQL 而产生的静态网站生成器。使用 Gatsby.js,你就能够在构建时用 GraphQL API 来创建一个提供博客内容的博客引擎,并且你将拥有一个无头字段的内容管理系统(CMS, content management systems),这个管理系统通过 GraphQL API 来提供(博客)内容。GraphQL 不仅在技术方面不断发展壮大,许多关于 GraphQL 的会议,聚会,社区以及新闻报道和播客也在不断涌现。 130 | 131 | ### 我该在所有技术栈上使用 GraphQL 吗? 132 | 133 | 在现有的技术栈上采用 GraphQL 不是一个"一刀切"的过程。从单一的后端应用迁移到微服务架构时,是在新的微服务上使用 GraphQL API 的最佳时机。在多个微服务情况下,团队可以引进 GraphQL 网关,通过模式拼接来合并一个全局的模式。同时 API 网关也用于之前的 REST 应用,而这就是如何通过将 API 捆绑在网关上来使其迁移到 GraphQL 上的过程。 134 | 135 | ## GraphQL 的劣势 136 | 137 | 下面的内容将介绍一些 GraphQL 的劣势。 138 | 139 | ### GraphQL 查询的复杂性 140 | 141 | 人们常常误以为 GraphQL 是服务器端数据库的替代品,但实际上它就只是一个查询语言而已。在服务器上,一次需要解析的数据查询,通常由与 GraphQL 无关的实现去执行数据库的访问。同时,当你需要在一次查询中访问多个字段时(authors,articles,comments),GraphQL 也不能解决性能瓶颈的问题。不论是一个 RESTful 架构的请求还是 GraphQL 的,各种资源和字段仍然需要从数据源获取。这样,当客户端一次请求过多的嵌套字段时,问题就会出现。前端开发者不是总能意识到服务器端需要执行获取数据的工作量,所以后端通常会有像最大查询深度,查询复杂度加权,递归避免,或者持久性查询这样的机制去阻止来自其它端的低效的查询请求。 142 | 143 | ### GraphQL 速率限制 144 | 145 | 另一个问题就是速率限制。对于 REST,它能很轻易的说出"我们一天只允许这么多的资源访问"这样的话,相反,对于单独的 GraphQL 操作就很难去做这样的声明,因为一个 GraphQL 操作即可以是一个昂贵的操作也可以是一个廉价的操作。这就是[拥有公共 GraphQL API 的公司会用特别的计算方法来限制速率](https://developer.github.com/v4/guides/resource-limitations/)的原因,这些方法通常就是前文提到的最大查询深度和查询复杂度加权。 146 | 147 | ### GraphQL 的缓存 148 | 149 | 在 GraphQL 中实现一个简单的缓存比在 REST 中实现要复杂许多。因为在 REST 中资源用 URL 进行访问,所以你可以用 URL 作为标识符在资源级别上缓存数据。而在 GraphQL 中,这就变得复杂了,因为就算是操作同一个实体每一次查询都可能是不同的。你可能在一次查询中只请求了作者的名字,但是在下次查询中想要知道邮件地址,即使他们都在查询同一个实体。这种情况下你就需要一个更加精细的在字段级别的缓存了,而这实现起来可能会比较困难。然而,大多数建立在 GraphQL 上的库都已经实现了开箱即用的缓存机制。 150 | 151 | ## 为什么不用 REST 152 | 153 | GraphQL 通常可以作为 RESTful 架构的代替方案去连接客户端和服务器。因为 REST 用了 URL 来标识资源,所以它常常会导致低效的瀑布请求。举个例子,假设你想要获取带有 id 的 author 实例,然后你想通过 author 的 id 来获取它所有的 article。在 GraphQL 中,只需一个请求就可以完成所有操作,这比 RESTful 高效多了。如果你只想要获取 author 的 article 而不是整个 author 实体,GraphQL 也可以让你只选择你想要的部分。但用 REST 的话,你就会拿到整个 author 实体造成数据冗余的现象。 154 | 155 | 如今,客户端应用不是为 RESTful 服务器应用而生的。在 Airbnb 平台上的搜索结果要显示房间,入住体验以及其他相关的信息。而房间和入住体验都来自于不同的资源,所以在 REST 中你不得不执行多次网络请求。而用 GraphQL 的话,你可以在一次查询中请求所有的实体,这些实体可以是并列的(例如房间和入住体验),也可是嵌套的(如 author 的 article)。GraphQL 将数据的选择权交给了客户端而不是服务器。这就是 GraphQL 被发明出来的首要原因——Facebook 的移动端应用与 web 端所需求的数据不同。 156 | 157 | 当然,在某些案例中,使用 REST 去连接客户端与服务器应用的价值仍然存在。这些应用通常是资源驱动的,并且不需要像 GraphQL 这样一个灵活的查询语言作为连接方式。即使这样,我建议你在你的下一个客户端—服务器架构中尝试一下 GraphQL,然后观察它是否满足你的需求。 158 | 159 | ## GraphQL 的替代方案 160 | 161 | 对于 GraphQL 来说,REST 是最热门的替代方案,而且它仍然是当下连接客户端服务器应用最流行的架构。REST 之所以能够变得比 [RPC](https://en.wikipedia.org/wiki/Remote_procedure_call) 和 [SOAP](https://simple.wikipedia.org/wiki/SOAP_(protocol)) 这样的网络技术更加流行是因为它用了 HTTP 原生的特性。而像 SOAP 这样的协议试图在它之上去构建自己的网络传输协议。 162 | 163 | Netflix 的 Falcor 是另一个可供选择的方案,并且它是与 GraphQL 在同一个时代开发的产品。Netflix 当时遇到了和 Facebook 同样的问题,并且最终也开源了他们的解决方案。可能由于 GraphQL 太受欢迎了,所以 Falcor 没有得到太多的关注。但是在过去一段时间中,Netflix 的开发者们也为之付出了很多工程上的努力,所以它可能还是值得去研究一下的。 164 | 165 | | | 166 | 167 | 有大量的理由让你采用 GraphQL 去开发你的 JavaScript 应用,而不是去继续实现另一个 RESTful 架构的应用。GraphQL 拥有很多优点,并且与现代软件架构结合得很好。本书接下来将介绍 GraphQL 是如何运用在许多实际解决方案中的,看完这些章节之后你应该就会明白它对你来说是否有用了。 -------------------------------------------------------------------------------- /manuscript/06-apollo-client/index.md: -------------------------------------------------------------------------------- 1 | # Apollo Client 2 | 3 | Apollo 是一个由开发人员构建的作为 GraphQL 应用程序基础设施的整个生态系统。你可以在客户端将其用于 GraphQL 客户端应用程序,或者在服务器端将其用于 GraphQL 服务端应用程序。在编写本教程时,Apollo 提供了在 JavaScript 中最丰富和最流行的 GraphQL 生态系统。尽管还有其他用于 React 应用程序的库,如 [Relay](http://facebook.github.io/relay) 和 [Urql](https://github.com/FormidableLabs/urql),但它们仅适用于 React 应用程序,并不像 Apollo Client 那么受欢迎。Apollo 是与框架无关的,这意味着你还可以将其与 React 之外的库一起使用,比如 Vue 和 Angular 等,因此你在本教程中学到的所有内容都可以迁移到其他库或者框架中。 4 | 5 | ## 从在命令行中使用 Apollo Boost 开始 6 | 7 | 这个应用程序首先通过 Apollo Boost 来引入 Apollo 客户端。通过 Apollo Boost, 你可以不做任何配置地,快速、方便地的创建一个 Apollo 客户端。为了便于学习,本节重点放在 Apollo 客户端而不是 React。首先,请找到 [Node.js 样板项目及其安装说明](https://github.com/rwieruch/node-babel-server)。目前你将在命令行中的 Node.js 环境中使用 Apollo 客户端。在一个最小的 Node.js 项目之上,你将通过 Apollo Boost 引入 Apollo 客户端来体验没有视图层库的 GraphQL 客户端。 8 | 9 | 接下来,你将使用 GitHub 的 GraphQL API,然后在命令行中输出查询和变更的结果。要做到这一点,你需要有一个 GitHub 网站的个人访问令牌,这个我们在前一章中已经介绍过了。如果你还没有完成,请参照 [GitHub 的说明](https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line/) 生成具有足够权限的个人访问令牌。 10 | 11 | 在克隆安装了 Node.js 样板项目并且创建了个人访问令牌后,请在命令行新项目的根文件夹中安装以下两个包: 12 | 13 | {title="Command Line",lang="json"} 14 | ~~~~~~~~ 15 | npm install apollo-boost graphql --save 16 | ~~~~~~~~ 17 | 18 | 其中 [apollo-boost](https://github.com/apollographql/apollo-client/tree/master/packages/apollo-boost) 包提供一个零配置的 Apollo 客户端,而 [graphql](https: //github.com/graphql/graphql-js) 包允许在客户端和服务器上进行 GraphQL 查询,变更和订阅,它是基于 [Facebook 的 GraphQL 规范](https://github.com/facebook/graphql) 的 JavaScript 参考实现。 19 | 20 | 在接下来的步骤中,你将在项目的 *src/index.js* 文件中配置和使用 Apollo Boost 提供的 Apollo 客户端。这个项目比较小,而且你只会在本节中实现它,因此,为了便于学习,目前我们可以将所有内容都放在一个文件中。 21 | 22 | 在你的 *src/index.js* 文件中,你可以从 Apollo Boost 导入 Apollo 客户端。然后,你可以通过使用 URI 为参数调用其构造函数来创建一个客户端实例。客户端需要知道数据的来源以及应该写入的位置,那么你可以将 GitHub 的 API 端点传递给它。 23 | 24 | {title="src/index.js",lang="javascript"} 25 | ~~~~~~~~ 26 | import ApolloClient from 'apollo-boost'; 27 | 28 | const client = new ApolloClient({ 29 | uri: 'https://api.github.com/graphql', 30 | }); 31 | ~~~~~~~~ 32 | 33 | Apollo 客户端正是这样工作的。但请记住,GitHub 的 GraphQL API 需要个人访问令牌,这就是为什么在创建 Apollo 客户端实例时必须定义一次的原因。因此,你可以使用 `request` 属性来定义一个函数,该函数可以访问通过 Apollo 客户端发出的每个请求的上下文。在该函数里,你将使用 Apollo Boost 的授权头作为其默认头信息之一传递下去。 34 | 35 | {title="src/index.js",lang="javascript"} 36 | ~~~~~~~~ 37 | import ApolloClient from 'apollo-boost'; 38 | 39 | const client = new ApolloClient({ 40 | uri: 'https://api.github.com/graphql', 41 | # leanpub-start-insert 42 | request: operation => { 43 | operation.setContext({ 44 | headers: { 45 | authorization: `Bearer YOUR_GITHUB_PERSONAL_ACCESS_TOKEN`, 46 | }, 47 | }); 48 | }, 49 | # leanpub-end-insert 50 | }); 51 | ~~~~~~~~ 52 | 53 | 你在之前的应用程序中做了相同的操作,仅使用 axios 进行纯 HTTP 请求。你使用 GraphQL API 端点为 axios 配置了一次,从而让所有的请求都默认访问这个 URI,并且设置授权头。这里也一样,为所有后续的 GraphQL 请求配置一次客户端就足够了。 54 | 55 | 请记住要用你之前在 GitHub 网站上创建的个人访问令牌替换 `YOUR_GITHUB_PERSONAL_ACCESS_TOKEN` 字符串。然而,你也许并不希望将访问令牌直接放到源代码中,那么你可以创建一个 *.env* 文件,该文件将包含你的项目文件夹中的所有环境变量。如果你不想在公共的 GitHub 代码库中共享个人令牌,你还可以将该文件添加到 *.gitignore* 文件中。你可以在命令行中创建该文件: 56 | 57 | {title="Command Line",lang="json"} 58 | ~~~~~~~~ 59 | touch .env 60 | ~~~~~~~~ 61 | 62 | 然后只需在此 *.env* 文件中定义环境变量即可。在 *.env* 文件中,粘贴如下键值对,其中键的命名由你决定,但值必须是你的 GitHub 个人访问令牌。 63 | 64 | {title=".env",lang="javascript"} 65 | ~~~~~~~~ 66 | GITHUB_PERSONAL_ACCESS_TOKEN=xxxXXX 67 | ~~~~~~~~ 68 | 69 | 在任何 Node.js 应用程序中,你都可以通过 [dotenv](https://github.com/motdotla/dotenv) 包在源代码中使用 key 作为环境变量。请按照他们的说明在你的项目中安装它。通常,你只需运行 `npm install dotenv`,然后在 *index.js* 文件的顶部添加 `import 'dotenv/config';`。然后,你就可以在 *index.js* 文件中使用 *.env* 文件中的个人访问令牌了。如果你遇到错误,请继续阅读本节以了解如何解决此问题。 70 | 71 | {title="src/index.js",lang="javascript"} 72 | ~~~~~~~~ 73 | import ApolloClient from 'apollo-boost'; 74 | 75 | # leanpub-start-insert 76 | import 'dotenv/config'; 77 | # leanpub-end-insert 78 | 79 | const client = new ApolloClient({ 80 | uri: 'https://api.github.com/graphql', 81 | request: operation => { 82 | operation.setContext({ 83 | headers: { 84 | # leanpub-start-insert 85 | authorization: `Bearer ${process.env.GITHUB_PERSONAL_ACCESS_TOKEN}`, 86 | # leanpub-end-insert 87 | }, 88 | }); 89 | }, 90 | }); 91 | ~~~~~~~~ 92 | 93 | 注意:刚才安装的 dotenv 包可能还需要其他配置步骤。由于安装说明可能因 dotenv 版本的不同而有所不同,请在安装后查看其 GitHub 网站从而找到最佳配置。 94 | 95 | 当你使用 `npm start` 启动你的没有查询或变更而只有 Apollo Client 的应用程序时,你可能会看到以下错误:*"Error: fetch is not found globally and no fetcher passed, to fix pass a fetch for your environment ..."*。发生这个错误是因为基于 promise 的用于进行远程 API 请求的 [原生 fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) 只能在浏览器中使用。你无法在一个仅运行在命令行中的 Node.js 应用程序中使用它。但是,Apollo Client 通常是在浏览器环境中使用 fetch API 来执行查询和变更的,而不是在 Node.js 环境中。你可能还记得,可以使用简单的 HTTP 请求执行查询或变更,因此 Apollo 客户端就使用了浏览器中的原生 fetch API 来执行这些请求。这个错误的解决方案是使用一个 node 工具包使得 fetch 在 Node.js 环境中可用。幸运的是,有一些包可以解决这个问题,可以通过命令行来进行安装: 96 | 97 | {title="Command Line",lang="json"} 98 | ~~~~~~~~ 99 | npm install cross-fetch --save 100 | ~~~~~~~~ 101 | 102 | 然后,在你的项目中将它匿名导入: 103 | 104 | {title="src/index.js",lang="javascript"} 105 | ~~~~~~~~ 106 | # leanpub-start-insert 107 | import 'cross-fetch/polyfill'; 108 | # leanpub-end-insert 109 | import ApolloClient from 'apollo-boost'; 110 | ~~~~~~~~ 111 | 112 | 当你再次从命令行启动应用程序时,错误应该会消失,不过到目前为止什么都还没有发生。我们使用配置创建了一个 Apollo 客户端的实例。在下文中,你将会使用 Apollo 客户端来执行第一个查询。 113 | 114 | ### 练习: 115 | 116 | * 查看[本节源码](https://github.com/the-road-to-graphql/node-apollo-boost-github-graphql-api/tree/fd067ec045861e9832cc0b202b25f8d8efd651c9) 117 | * 延伸阅读:[其他视图集成,如 Angular 和 Vue](https://www.apollographql.com/docs/react/integrations.html) 118 | * 花几分钟的时间进行[测验](https://www.surveymonkey.com/r/5T3W9BB) 119 | 120 | ## Apollo 客户端和 GraphQL 查询 121 | 122 | 现在,你将使用 Apollo 客户端向 GitHub 的 GraphQL API 发送你的第一个查询。从 Apollo Boost 导入如下实用程序来定义查询: 123 | 124 | {title="src/index.js",lang="javascript"} 125 | ~~~~~~~~ 126 | import 'cross-fetch/polyfill'; 127 | # leanpub-start-insert 128 | import ApolloClient, {gql} from 'apollo-boost'; 129 | # leanpub-end-insert 130 | ~~~~~~~~ 131 | 132 | 使用 JavaScript 模板字面量定义你的查询: 133 | 134 | {title="src/index.js",lang="javascript"} 135 | ~~~~~~~~ 136 | ... 137 | 138 | # leanpub-start-insert 139 | const GET_ORGANIZATION = gql` 140 | { 141 | organization(login: "the-road-to-learn-react") { 142 | name 143 | url 144 | } 145 | } 146 | `; 147 | # leanpub-end-insert 148 | ~~~~~~~~ 149 | 150 | 命令式地让 Apollo 客户端将查询发送到 GitHub 的 GraphQL API。由于 Apollo 客户端是基于 promise 的,`query()` 方法会返回一个你最终可以 resolve 的 promise。由于应用程序是在在命令行中运行的,把结果输出到控制台就足够了。 151 | 152 | {title="src/index.js",lang="javascript"} 153 | ~~~~~~~~ 154 | ... 155 | 156 | # leanpub-start-insert 157 | client 158 | .query({ 159 | query: GET_ORGANIZATION, 160 | }) 161 | .then(console.log); 162 | # leanpub-end-insert 163 | ~~~~~~~~ 164 | 165 | 这就是用 Apollo 客户端发送查询的全部内容。如上所述,Apollo 客户端的底层是使用 HTTP 在 POST 方法中将定义好的查询作为有效负载发送出去。使用 `npm start` 启动应用程序后命令行上的结果应类似于以下内容: 166 | 167 | {title="Command Line",lang="json"} 168 | ~~~~~~~~ 169 | { 170 | data: { 171 | organization: { 172 | name: 'The Road to learn React', 173 | url: 'https://github.com/the-road-to-learn-react', 174 | __typename: 'Organization' 175 | } 176 | }, 177 | loading: false, 178 | networkStatus: 7, 179 | stale: false 180 | } 181 | ~~~~~~~~ 182 | 183 | GraphQL 查询中请求的信息可以在 `data` 对象中找到。`data` 对象包含 `organization` 对象及其 `name` 和 `url` 字段。Apollo 客户端会自动请求 GraphQL [元字段](http://graphql.org/learn/queries/#meta-fields) `__typename`。Apollo 客户端可以使用元字段作为标识符,以允许缓存和乐观 UI 更新。 184 | 185 | 有关请求的更多元信息可以在 `data` 对象旁边找到。它显示数据是否仍在加载,[网络状态](https://github.com/apollographql/apollo-client/blob/master/packages/apollo-client/src/core/networkStatus .ts) 的具体细节,以及请求的数据是否在服务器端过时了。 186 | 187 | ### 练习: 188 | 189 | * 查看[本节源码](https://github.com/the-road-to-graphql/node-apollo-boost-github-graphql-api/tree/7a800c78e0e09f84b47f4e714abac1d23f5e599e) 190 | * 探索 GitHub 的 GraphQL API 191 | * 轻松地浏览他们的文档 192 | * 为 `organization` 字段添加其他字段 193 | * 延伸阅读:[为什么你应该使用 Apollo Client](https://www.apollographql.com/docs/react/why-apollo.html) 194 | * 延伸阅读:[networkStatus 属性及其可能值](https://github.com/apollographql/apollo-client/blob/master/packages/apollo-client/src/core/networkStatus.ts) 195 | * 花 3 分钟时间来进行[测验](https://www.surveymonkey.com/r/5MF35H5) 196 | 197 | ## 带分页,变量,嵌套对象和列表字段的 Apollo Client 198 | 199 | 在前几节使用不含 Apollo 的 GraphQL 应用程序构建 React 时,你已经了解了 GraphQL 的分页和其它功能。本节将介绍其中的一些功能,如 GraphQL 变量。上一个查询中 organization 字段的 `login` 参数可以用这样的变量进行替换。首先,你必须在 GraphQL 查询中引入这个变量: 200 | 201 | {title="src/index.js",lang="javascript"} 202 | ~~~~~~~~ 203 | const GET_ORGANIZATION = gql` 204 | # leanpub-start-insert 205 | query($organization: String!) { 206 | organization(login: $organization) { 207 | # leanpub-end-insert 208 | name 209 | url 210 | } 211 | } 212 | `; 213 | ~~~~~~~~ 214 | 215 | 其次,在你的查询对象的 variables 对象中定义这个变量: 216 | 217 | {title="src/index.js",lang="javascript"} 218 | ~~~~~~~~ 219 | client 220 | .query({ 221 | query: GET_ORGANIZATION, 222 | # leanpub-start-insert 223 | variables: { 224 | organization: 'the-road-to-learn-react', 225 | }, 226 | # leanpub-end-insert 227 | }) 228 | .then(console.log); 229 | ~~~~~~~~ 230 | 231 | 这就是你在应用程序中的使用 Apollo Client 实例将变量传递给查询的方法。接下来,将嵌套的 `repositories` 列表字段添加到你的 organization 字段。这样你就可以请求一个组织中的所有 GitHub 代码库。你可能还想重命名查询变量,但请记得在使用 Apollo Client 时更改它。 232 | 233 | {title="src/index.js",lang="javascript"} 234 | ~~~~~~~~ 235 | # leanpub-start-insert 236 | const GET_REPOSITORIES_OF_ORGANIZATION = gql` 237 | # leanpub-end-insert 238 | query($organization: String!) { 239 | organization(login: $organization) { 240 | name 241 | url 242 | # leanpub-start-insert 243 | repositories(first: 5) { 244 | edges { 245 | node { 246 | name 247 | url 248 | } 249 | } 250 | } 251 | # leanpub-end-insert 252 | } 253 | } 254 | `; 255 | 256 | client 257 | .query({ 258 | # leanpub-start-insert 259 | query: GET_REPOSITORIES_OF_ORGANIZATION, 260 | # leanpub-end-insert 261 | variables: { 262 | organization: 'the-road-to-learn-react', 263 | }, 264 | }) 265 | .then(console.log); 266 | ~~~~~~~~ 267 | 268 | 在我们之前创建的应用程序中,你已经看到过类似的查询结构,因此本节提供几个练习供你测试所学的 GraphQL 技能。通过这些练习能够强化你的 GraphQL 技能,这样你以后可以集中精力将 Apollo Client 连接到你的 React 应用程序,而不会遇到任何障碍。在练习结束后,你可以在此应用程序的 GitHub 代码库中找到所有练习的答案,但你应该先尝试自己完成它们。 269 | 270 | ### 练习: 271 | * 查看[本节源码](https://github.com/the-road-to-graphql/node-apollo-boost-github-graphql-api/tree/a5b1ce61a3dae3ead1b9795f5bf6e0d090c5d24f) 272 | * 探索 GitHub 的 GraphQL API 273 | * 通过查询一个按照 star 数量排序的有序代码库列表来扩展 `repositories` 列表字段 274 | * 将 repository `node` 的内容提取为可重用的 GraphQL 片段 275 | * 延伸阅读:[GraphQL 中的分页](https://graphql.org/learn/pagination) 276 | * 为代码库列表添加分页功能 277 | * 在查询中添加 `pageInfo` 字段,它应包含 `endCursor` 和 `hasNextPage` 字段 278 | * 添加 `after` 参数并为其引入一个新的 `$cursor` 变量 279 | * 执行第一个查询时不带 `cursor` 参数 280 | * 使用前一个查询结果的 `endCursor` 作为 `cursor` 参数执行第二个查询 281 | * 花三分钟进行[测验](https://www.surveymonkey.com/r/SWL9NJ7) 282 | 283 | ## Apollo Client 和 GraphQL 变更 284 | 285 | 前几节你学习了如何使用 Apollo Client 从 GitHub 的 GraphQL API 查询数据。一旦配置好了 Apollo Client,就可以使用其 `query()` 方法发送带有可选 `variables` 的 GraphQL `查询`。正如你所了解的那样,我们不仅可以使用 GraphQL 来读取数据,还可以通过变更来写入数据。在本节中,你将定义一个变更来为 GitHub 上的代码库加 star。Apollo Client 实例会发送变更,但首先你必须定义它。 286 | 287 | {title="src/index.js",lang="javascript"} 288 | ~~~~~~~~ 289 | const ADD_STAR = gql` 290 | mutation AddStar($repositoryId: ID!) { 291 | addStar(input: { starrableId: $repositoryId}) { 292 | starrable { 293 | id 294 | viewerHasStarred 295 | } 296 | } 297 | } 298 | `; 299 | ~~~~~~~~ 300 | 301 | 代码库的标识符是必需的,否则 GitHub 的 GraphQL 服务器不知道你要为哪个代码库加 star。在下一个代码片段中,Apollo Client 用来为具有给定标识符的特定 GitHub 代码库加注星标。可以通过在查询中将 `id` 字段添加到你的代码库 `node` 字段来检索标识符。使用 Apollo Client 上的 `mutate()` 方法在 `mutation` 和 `variables` 有效载荷中发送变更。可以对结果进行任何操作以适用于你的应用程序,但在本例中,只需在命令行中记录结果即可。 302 | 303 | {title="src/index.js",lang="javascript"} 304 | ~~~~~~~~ 305 | client 306 | .mutate({ 307 | mutation: ADD_STAR, 308 | variables: { 309 | repositoryId: 'MDEwOlJlcG9zaXRvcnk2MzM1MjkwNw==', 310 | }, 311 | }) 312 | .then(console.log); 313 | ~~~~~~~~ 314 | 315 | 结果应该会封装在一个 `addStar` 对象(变更的名称)中,它应该准确反映你在变更中定义的对象和字段:`starrable`,`id` 和 `viewerHasStarred`。 316 | 317 | 你又完成了另一个学习步骤,其中只使用了 Apollo Client 而没有使用任何视图层库。这样做是为了避免混淆 Apollo Client 和 React Apollo 的功能。 318 | 319 | 请记住,Apollo Client 可以作为独立的 GraphQL 客户端使用,而无需将其连接到像 React 这样的视图层,尽管仅在命令行上查看数据似乎有点沉闷。我们将在下一节中看到 Apollo 如何将数据层连接到 React 视图层。 320 | 321 | ### 练习: 322 | 323 | * 查看[本节源码](https://github.com/the-road-to-graphql/node-apollo-boost-github-graphql-api/tree/ed3363c9981c552223117e5e775bb8c535f79ff5) 324 | * 仿照 `addStar` 变更实现 `removeStar` 变更 325 | * 花三分钟的时间进行[测验](https://www.surveymonkey.com/r/5XMNFSY) 326 | 327 | | | 328 | 329 | 你已经了解了如何在 Node.js 项目中单独使用 Apollo 客户端。在本章之前,你已经将 React 与不带 Apollo 的 GraphQL 结合使用。在下一章中,你将把这两种结合起来。尽情期待你的第一个完整的使用 Apollo 客户端和 GraphQL 的 React 客户端应用程序吧。 -------------------------------------------------------------------------------- /manuscript/04-graphql-fundamentals/index.md: -------------------------------------------------------------------------------- 1 | # GraphQL 基础 2 | 3 | 在我们开始构建一个包含客户端和服务器端完整的 GraphQL 应用之前,让我们通过前面章节安装的一些工具来体验一下 GraphQL 的工作方式。你可以选用 GraphiQL 或者 GitHub 提供的 GraphQL Explorer。 接下来,通过执行你的第一个 GraphQL 查询、变更,以及探索 GitHub 的 GraphQL API 中的一些特性,例如分页等,来学习 GraphQL 基础操作。 4 | 5 | ## GraphQL 基础: 查询 6 | 7 | 在本节中,你可以使用查询和变更同 GitHub API 交互,可以通过 GraphiQL 应用或者 GitHub 的 GraphQL Explorer 发送查询请求到 GitHub API,本节暂时不会涉及和 React 集成相关知识。 这两种工具都需要使用个人申请的 access token 授权。在 GraphiQL 应用的左侧,允许你输入 GraphQL 查询和变更语句来调试 GraphQL 请求。尝试输入下面的语句获取你的个人信息数据。 8 | 9 | {title="GitHub GraphQL Explorer",lang="json"} 10 | ~~~~~~~~ 11 | { 12 | viewer { 13 | name 14 | url 15 | } 16 | } 17 | ~~~~~~~~ 18 | 19 | `viewer` 对象可以被用来获取当前授权的用户信息。通过你的个人 access token 获得授权并完成查询请求后,应该能看到相关的用户信息被正确返回。`viewer` 是一个 GraphQL 中**对象**的概念。 对象承载某个实体的数据,这些数据可以通过 GraphQL 中的**字段**访问。字段被用于获取对象中指定的属性。举个例子来说,`viewer` 对象暴露了多个字段,在这个例子中,只有 `name` 和 `url` 在查询中用到了。在大多数基本情况下,一个查询只包含对象和字段,当然对象也是一种字段。 20 | 21 | 当你在 GraphiQL 中执行完上面的查询语句,你可以看到类似如下的返回内容,上面的 name 和 URL 被替换成对应的返回值。 22 | 23 | {title="GitHub GraphQL Explorer",lang="json"} 24 | ~~~~~~~~ 25 | { 26 | "data": { 27 | "viewer": { 28 | "name": "Robin Wieruch", 29 | "url": "https://github.com/rwieruch" 30 | } 31 | } 32 | } 33 | ~~~~~~~~ 34 | 35 | 恭喜你,你成功地执行了第一个查询并且获取到了你在 GitHub 中个人信息中的相关字段。现在,让我们看看怎么去获取其他的资源,例如 GitHub 中开放出来的组织信息。为了获取特定的 GitHub 组织信息,你需要传入一个**参数**: 36 | 37 | {title="GitHub GraphQL Explorer",lang="json"} 38 | ~~~~~~~~ 39 | { 40 | organization(login: "the-road-to-learn-react") { 41 | name 42 | url 43 | } 44 | } 45 | ~~~~~~~~ 46 | 47 | 使用 GitHub 的 API 时,组织资源要求指定 `login` 参数。如果你之前使用过 GitHub,应该知道这个参数是组织 URL 地址中的一部分。 48 | 49 | {title="Code Playground",lang="json"} 50 | ~~~~~~~~ 51 | https://github.com/the-road-to-learn-react 52 | ~~~~~~~~ 53 | 54 | 通过提供一个 `login` 参数,你可以获取到该参数相关的组织数据。在这个例子中,我们指定了两个字段去获取组织中的 `name` 和 `url`。这次请求应该返回类似如下内容: 55 | 56 | {title="GitHub GraphQL Explorer",lang="json"} 57 | ~~~~~~~~ 58 | { 59 | "data": { 60 | "organization": { 61 | "name": "The Road to learn React", 62 | "url": "https://github.com/the-road-to-learn-react" 63 | } 64 | } 65 | } 66 | ~~~~~~~~ 67 | 68 | 在上面的查询中,你传入了一个参数给某个字段。同理,你也可以使用 GraphQL 添加参数到不同的字段。由于 GraphQL 的参数支持在在字段级别作出约束,这为结构化查询提供了很大的灵活性。另外,参数可以是不同的类型。对于上面组织的例子,使用了一个类型为 `String` 的参数,但你也可以使用一组固定的选项作为枚举、整数或布尔值。 69 | 70 | 如果你想要一个字段不同参数返回的数据,则需要在 GraphQL 中使用 **别名**。下面的查询语句不能被正常处理,因为 GraphQL 不知道如何在结果中解析两个组织对象: 71 | 72 | {title="GitHub GraphQL Explorer",lang="json"} 73 | ~~~~~~~~ 74 | { 75 | organization(login: "the-road-to-learn-react") { 76 | name 77 | url 78 | } 79 | organization(login: "facebook") { 80 | name 81 | url 82 | } 83 | } 84 | ~~~~~~~~ 85 | 86 | 你会看到一个错误,例如 `Field 'organization' has an argument conflict`。使用别名,可以将结果解析为两个字段: 87 | 88 | 89 | {title="GitHub GraphQL Explorer",lang="json"} 90 | ~~~~~~~~ 91 | { 92 | # leanpub-start-insert 93 | book: organization(login: "the-road-to-learn-react") { 94 | # leanpub-end-insert 95 | name 96 | url 97 | } 98 | # leanpub-start-insert 99 | company: organization(login: "facebook") { 100 | # leanpub-end-insert 101 | name 102 | url 103 | } 104 | } 105 | ~~~~~~~~ 106 | 107 | 结果应类似如下内容: 108 | 109 | {title="GitHub GraphQL Explorer",lang="json"} 110 | ~~~~~~~~ 111 | { 112 | "data": { 113 | "book": { 114 | "name": "The Road to learn React", 115 | "url": "https://github.com/the-road-to-learn-react" 116 | }, 117 | "company": { 118 | "name": "Facebook", 119 | "url": "https://github.com/facebook" 120 | } 121 | } 122 | } 123 | ~~~~~~~~ 124 | 125 | 接下来,假设你要为两个组织请求多个字段。重新填入每个组织的所有字段会使查询重复且冗长,因此我们可以使用**片段**来提取查询的可重用部分。当查询深度嵌套并使用大量共享字段时,片段尤其有用。 126 | 127 | {title="GitHub GraphQL Explorer",lang="json"} 128 | ~~~~~~~~ 129 | { 130 | book: organization(login: "the-road-to-learn-react") { 131 | # leanpub-start-insert 132 | ...sharedOrganizationFields 133 | # leanpub-end-insert 134 | } 135 | company: organization(login: "facebook") { 136 | # leanpub-start-insert 137 | ...sharedOrganizationFields 138 | # leanpub-end-insert 139 | } 140 | } 141 | 142 | # leanpub-start-insert 143 | fragment sharedOrganizationFields on Organization { 144 | name 145 | url 146 | } 147 | # leanpub-end-insert 148 | ~~~~~~~~ 149 | 150 | 如你所见,你必须指定该片段用在哪种**类型**的对象上。在这个例子中,应该是 `Organization` 类型,它是由 GitHub 的 GraphQL API 定义的自定义类型。以上为如何使用片段进行提取和重用你的部分查询的示例。关于这点,如果你在 GraphiQL 应用程序的右侧打开 “Docs” 面板。你可以看到 GraphQL 定义的 **schema**。schema 定义了 GraphiQL 如何使用某个 GraphQL API,在这个例子中,它是 Github 提供的 GraphQL API。它定义了 GraphQL **graph**,可以使用查询和变更对 GraphQL API 进行调用。由于它是一个图形结构,因此对象和字段可以深深地嵌套在其中,随着学习的深入,我们会对此有更深入的体会。 151 | 152 | 由于我们目前在探索查询相关的内容,所以可以在 “Docs” 侧边栏中选择 “Query” 标签来了解更多信息。对比 graph 中的对象和字段,浏览它们的可选参数。点击它们,你可以在文档中查看这些对象中的可访问字段。有些字段是常见的 GraphQL 类型,如 `String`,`Int` 和 `Boolean`,而其他一些类型是**自定义类型**,就像我们使用的 `Organization` 类型。此外,通过感叹号标记你可以查看在对象上的字段的参数是否为必填。例如,带有 `String!` 参数的字段要求你必须传入 `String` 参数,而带有 `String` 参数的字段则是可选的。 153 | 154 | 在之前的学习中,你在构建查询时,传入了用于向字段标识某个组织的参数,但是是通过**内联参数**的方式。如果将查询作为函数一样看待,为它提供动态参数就很有意义了。这就是 GraphQL 中**变量**,它允许使用参数动态地构建查询。以下示例展示了组织的 `login` 参数是如何使用变量的: 155 | 156 | {title="GitHub GraphQL Explorer",lang="json"} 157 | ~~~~~~~~ 158 | # leanpub-start-insert 159 | query ($organization: String!) { 160 | organization(login: $organization) { 161 | # leanpub-end-insert 162 | name 163 | url 164 | } 165 | } 166 | ~~~~~~~~ 167 | 168 | 使用 `$` 符号将 `organization` 参数定义为变量。此外,参数的类型被定义为 `String`。由于该参数是完成查询所必需的,因此 `String` 类型有一个感叹号。 169 | 170 | 在 “Query Variables” 面板中,需要像下面这样定义变量内容,用于提供 `organization` 变量作为查询的参数: 171 | 172 | {title="GitHub GraphQL Explorer",lang="json"} 173 | ~~~~~~~~ 174 | { 175 | "organization": "the-road-to-learn-react" 176 | } 177 | ~~~~~~~~ 178 | 179 | 一般来说,变量被用来创建动态查询。遵循 GraphQL 中的最佳实践,我们不需要手动插入字符串来构建动态查询。实际开发过程中,当我们使用变量构建查询时,可以让参数在请求被发送时动态绑定。稍后你将在 React 应用程序中看到这两种实现。 180 | 181 | 旁注:你还可以在 GraphQL 中定义**默认变量**。要求是非必需参数,否则会出现关于 **nullable variable** 或 **non-null variable** 的错误。要了解默认变量,我们将通过省略感叹号来使 “organization” 参数设为可选。之后,它可以作为默认变量传递。 182 | 183 | {title="GitHub GraphQL Explorer",lang="json"} 184 | ~~~~~~~~ 185 | # leanpub-start-insert 186 | query ($organization: String = "the-road-to-learn-react") { 187 | organization(login: $organization) { 188 | # leanpub-end-insert 189 | name 190 | url 191 | } 192 | } 193 | ~~~~~~~~ 194 | 195 | 尝试使用两组变量执行上一个查询:一次使用不同于默认变量的 `organization` 变量,一次不定义 `organization` 变量。 196 | 197 | 现在,让我们回过头来检查 GraphQL 查询的结构。在引入变量之后,第一次在查询结构中遇到了 `query` 语句。之前,实际上是省略 `query` 语句的**查询的简写版本**,但是现在使用变量后, `query` 语句就是必须的了。尝试不带变量的以下查询,但使用 `query` 语句,来验证查询的非简写版本是否有效。 198 | 199 | {title="GitHub GraphQL Explorer",lang="json"} 200 | ~~~~~~~~ 201 | # leanpub-start-insert 202 | query { 203 | # leanpub-end-insert 204 | organization(login: "the-road-to-learn-react") { 205 | name 206 | url 207 | } 208 | } 209 | ~~~~~~~~ 210 | 211 | 虽然使用了非简写版本,但仍然返回了与之前相同的数据,和我们设想的结果一样。查询语句在 GraphQL 语言中也称为**操作类型**。例如,它也可以是 `mutation` 语句。除了操作类型,你还可以定义**操作名称**。 212 | 213 | 214 | {title="GitHub GraphQL Explorer",lang="json"} 215 | ~~~~~~~~ 216 | # leanpub-start-insert 217 | query OrganizationForLearningReact { 218 | # leanpub-start-insert 219 | organization(login: "the-road-to-learn-react") { 220 | name 221 | url 222 | } 223 | } 224 | ~~~~~~~~ 225 | 226 | 将代码中的匿名和具名函数进行对比。**具名查询**更为清晰,表明你希望以声明方式实现查询,在调试多个查询时非常有帮助,推荐在真实的项目中这样操作。完成查询的最终版本(省略了变量表),应该像下面一样: 227 | 228 | 229 | {title="GitHub GraphQL Explorer",lang="json"} 230 | ~~~~~~~~ 231 | query OrganizationForLearningReact($organization: String!) { 232 | organization(login: $organization) { 233 | name 234 | url 235 | } 236 | } 237 | ~~~~~~~~ 238 | 239 | 到目前为止,你仅获取了包含一些简单字段的对象。 GraphQL 模式能实现一个完整的图形结构,所以让我们看看如何使用查询实现**嵌套对象**的获取。写法和之前基本一样: 240 | 241 | {title="GitHub GraphQL Explorer",lang="json"} 242 | ~~~~~~~~ 243 | query OrganizationForLearningReact( 244 | $organization: String!, 245 | # leanpub-start-insert 246 | $repository: String! 247 | # leanpub-end-insert 248 | ) { 249 | organization(login: $organization) { 250 | name 251 | url 252 | # leanpub-start-insert 253 | repository(name: $repository) { 254 | name 255 | } 256 | # leanpub-end-insert 257 | } 258 | } 259 | ~~~~~~~~ 260 | 261 | 使用第二个变量来获取组织中的特定代码库: 262 | 263 | 264 | {title="GitHub GraphQL Explorer",lang="json"} 265 | ~~~~~~~~ 266 | { 267 | "organization": "the-road-to-learn-react", 268 | # leanpub-start-insert 269 | "repository": "the-road-to-learn-react-chinese" 270 | # leanpub-end-insert 271 | } 272 | ~~~~~~~~ 273 | 274 | 包含 React 教程的一个组织含有翻译后的版本,其中一个仓库是简体中文版本。 GraphQL 中的字段可以是嵌套对象,并且你已从 graph 中查询了两个关联对象。通过 graph 可以构建出深层嵌套的查询。在之前探索 GraphiQL 中的 “Docs” 侧边栏时,你可能已经看到可以在对象跳转到另外一个对象的功能。 275 | 276 | **指令**可用于以更强大的方式查询 GraphQL API 中的数据,并且它们可以应用于字段和对象。下面,我们来尝试使用两种指令:**include 指令**,用来包含布尔类型设置为 true 的字段; 和 **skip 指令**,与之相反排除那些为 true 的字段。使用这些指令,你可以将条件结构应用于查询。以下查询展示了 include 指令,你也可以使用 skip 指令替换它实现相反的效果: 277 | 278 | 279 | {title="GitHub GraphQL Explorer",lang="json"} 280 | ~~~~~~~~ 281 | query OrganizationForLearningReact( 282 | $organization: String!, 283 | $repository: String!, 284 | # leanpub-start-insert 285 | $withFork: Boolean! 286 | # leanpub-end-insert 287 | ) { 288 | organization(login: $organization) { 289 | name 290 | url 291 | repository(name: $repository) { 292 | name 293 | # leanpub-start-insert 294 | forkCount @include(if: $withFork) 295 | # leanpub-end-insert 296 | } 297 | } 298 | } 299 | ~~~~~~~~ 300 | 301 | 现在你可以根据提供的变量决定是否包含 `forkCount` 字段的信息。 302 | 303 | {title="GitHub GraphQL Explorer",lang="json"} 304 | ~~~~~~~~ 305 | { 306 | "organization": "the-road-to-learn-react", 307 | "repository": "the-road-to-learn-react-chinese", 308 | # leanpub-start-insert 309 | "withFork": true 310 | # leanpub-end-insert 311 | } 312 | ~~~~~~~~ 313 | 314 | 315 | GraphQL 中的查询为你提供了从 GraphQL API 读取数据时的全部功能。不过最后一部分可能让人感到困惑,如果你依然没有掌握,下面提供了一些练习来帮助你。 316 | 317 | ### 练习: 318 | 319 | * 延伸阅读:[GraphQL 中的查询](http://graphql.org/learn/queries). 320 | * 使用 GraphiQL 中的 “Docs” 侧边栏探索 GitHub 的查询操作 321 | * 使用以下功能创建一些从 GitHub 的 GraphQL API 请求数据的查询: 322 | * 对象和字段 323 | * 嵌套对象 324 | * 片段 325 | * 参数和变量 326 | * 具名操作 327 | * 指令 328 | 329 | ## GraphQL 基础:变更 330 | 331 | 这部分将会介绍 GraphQL 变更操作。它作为 GraphQL 查询的补充,用于改写数据而不是读取。变更操作和查询操作拥有着同样的规则:拥有字段和对象、参数和变量、片段和操作名称、指令和返回结果中的嵌套对象。通过变更操作你可以指定在"更新"发生后所期望的返回数据的字段和对象。在你开始你的第一次变更操作之前,请注意你在使用真实的 GitHub 数据,也就是说如果你在尝试变更操作的时候关注了 GitHub 上的一个人,你就真的关注了这个人。幸运的是这种行为在 GitHub 上是受到鼓励的。 332 | 333 | 接下来你将会 star 一个 GitHub 上的代码库,而和你之前使用一个查询来请求一样,你将使用[来自 GitHub API](https://developer.github.com/v4/mutation/addstar) 的一个变更请求。你可以在 "Docs" 侧边栏中找到 `addStar` 这种变更操作。这是一个存放为开发者讲解 React 基础课程的代码库,所以 star 这个代码库能够证明它有用。 334 | 335 | 你可以访问[这个代码库](https://github.com/the-road-to-learn-react/the-road-to-learn-react)来查看你是否已经成功 star。我们想要一个尚未 star 过的代码库,这样我们可以通过一个变更操作来 star 它。在你 star 一个代码库前,你需要知道它的唯一标识。这个唯一标识你可以通过下面的查询获取: 336 | 337 | {title="GitHub GraphQL Explorer",lang="json"} 338 | ~~~~~~~~ 339 | query { 340 | organization(login: "the-road-to-learn-react") { 341 | name 342 | url 343 | # leanpub-start-insert 344 | repository(name: "the-road-to-learn-react") { 345 | # leanpub-end-insert 346 | id 347 | name 348 | } 349 | } 350 | } 351 | ~~~~~~~~ 352 | 353 | 在 GraphiQL 的该查询结果中,你应该能看到代码库的唯一标识: 354 | 355 | {title="Code Playground",lang="json"} 356 | ~~~~~~~~ 357 | MDEwOlJlcG9zaXRvcnk2MzM1MjkwNw== 358 | ~~~~~~~~ 359 | 360 | 在使用唯一标示作为变量之前,你可以在 GraphiQL 中使用以下结构的变更操作: 361 | 362 | {title="GitHub GraphQL Explorer",lang="json"} 363 | ~~~~~~~~ 364 | mutation AddStar($repositoryId: ID!) { 365 | addStar(input: { starrableId: $repositoryId }) { 366 | starrable { 367 | id 368 | viewerHasStarred 369 | } 370 | } 371 | } 372 | ~~~~~~~~ 373 | 374 | 这个变更操作的名称是由 GitHub API 起的:`addStar`。你需要传递 `starrableId` 作为 `input` 来指定代码库;否则 GitHub 服务器无从得知这次变更操作是要 star 哪个代码库。另外,这个变更是一个具名变更为: `AddStar` 。你也可以给它任意名称。最后但也同样重要的是,你可以再次通过对象和字段来定义这个变更的返回值,这和查询是相同的。最终,在变量区中提供你在上一次查询得到的变量将被用于这一次变更操作: 375 | 376 | {title="GitHub GraphQL Explorer",lang="json"} 377 | ~~~~~~~~ 378 | { 379 | "repositoryId": "MDEwOlJlcG9zaXRvcnk2MzM1MjkwNw==" 380 | } 381 | ~~~~~~~~ 382 | 383 | 一旦你执行了这个变更操作,结果应该类似如下内容。因为你使用了 `id` 和 `viewerHasStarred` 字段指定你的变更的返回值,所以你应该能在结果中找到它们。 384 | 385 | {title="GitHub GraphQL Explorer",lang="json"} 386 | ~~~~~~~~ 387 | { 388 | "data": { 389 | "addStar": { 390 | "starrable": { 391 | "id": "MDEwOlJlcG9zaXRvcnk2MzM1MjkwNw==", 392 | "viewerHasStarred": true 393 | } 394 | } 395 | } 396 | } 397 | ~~~~~~~~ 398 | 399 | 这个代码库现在已经被 star 了。在返回的结果中能够看到,但你也可以通过查看 [GitHub 上的代码库](https://github.com/the-road-to-learn-react/the-road-to-learn-react)来确认。恭喜,你完成了你的第一个变更操作。 400 | 401 | ### 练习: 402 | 403 | * 延伸阅读:[GraphQL 中的变更](http://graphql.org/learn/queries/#mutations) 404 | * 通过 GraphiQL 上的 "Docs" 侧边栏探索 GitHub 的更多变更操作 405 | * 在 GraphiQL 上的 "Docs" 侧边栏中找到 GitHub 的 `addStar` 变更 406 | * 检查它所有可以返回的字段 407 | * 对该代码库或另一个代码库构造一些其他变更操作,比如: 408 | * 取消 Star 代码库 409 | * Watch 代码库 410 | * 在 GraphiQL 面板上创建两个并列的命名变更,然后执行它们 411 | * 延伸阅读:[模式和类型](http://graphql.org/learn/schema) 412 | * 你可以只是大概了解一下,不要担心你有不理解的地方 413 | 414 | ## GraphQL 分页 415 | 416 | 这里我们回到了在第一节提到的**分页**的概念。试想你在你的 GitHub 组织下有一个代码库列表,但你只想获得它们中的一部分来展示在你的 UI 上。如果是从一个大型组织下获取一个代码库列表的话,那将花费大量的时间。在 GraphQL 中,你可以通过提供参数到一个**列举字段**来请求分页数据,比如一个表明你希望从列表中获得多少项的参数。 417 | 418 | {title="GitHub GraphQL Explorer",lang="json"} 419 | ~~~~~~~~ 420 | query OrganizationForLearningReact { 421 | organization(login: "the-road-to-learn-react") { 422 | name 423 | url 424 | # leanpub-start-insert 425 | repositories(first: 2) { 426 | edges { 427 | node { 428 | name 429 | } 430 | } 431 | } 432 | # leanpub-end-insert 433 | } 434 | } 435 | ~~~~~~~~ 436 | 437 | 这里一个 `first` 参数被传给了 `repositories` 的列举字段来指定希望从列表中获得多少项作为结果。这个查询没有被要求按照 `edges` 和 `node` 的结构编写,不过这也是仅有的几种 GraphQL 定义分页数据结构和列表的方案之一。实际上它是借鉴了 Facebook GraphQL 客户端 Relay 的接口描述方案。GitHub 按照它的方式并采纳到了自己的 GraphQL 分页 API 中。之后你将会在练习中了解到其他用 GraphQL 实现分页的方法。 438 | 439 | 在执行这个查询之后,你应该能在 repositories 字段的列表中看到两项。然而,我们仍然需要寻找如何拿到代码库列表中之后两项的办法。这个查询的第一次结果是分页列表的第一**页**,而第二次查询的结果应该是第二页。接下来,你会看到分页数据的查询结构是如何允许我们获得元信息来执行连续的查询。比如,每个 edge 有它自己的 游标字段来指明它在列表中的位置。 440 | 441 | {title="GitHub GraphQL Explorer",lang="json"} 442 | ~~~~~~~~ 443 | query OrganizationForLearningReact { 444 | organization(login: "the-road-to-learn-react") { 445 | name 446 | url 447 | repositories(first: 2) { 448 | edges { 449 | node { 450 | name 451 | } 452 | # leanpub-start-insert 453 | cursor 454 | # leanpub-end-insert 455 | } 456 | } 457 | } 458 | } 459 | ~~~~~~~~ 460 | 461 | 结果类似如下内容: 462 | 463 | {title="GitHub GraphQL Explorer",lang="json"} 464 | ~~~~~~~~ 465 | { 466 | "data": { 467 | "organization": { 468 | "name": "The Road to learn React", 469 | "url": "https://github.com/the-road-to-learn-react", 470 | "repositories": { 471 | "edges": [ 472 | { 473 | "node": { 474 | "name": "the-road-to-learn-react" 475 | }, 476 | "cursor": "Y3Vyc29yOnYyOpHOA8awSw==" 477 | }, 478 | { 479 | "node": { 480 | "name": "hackernews-client" 481 | }, 482 | "cursor": "Y3Vyc29yOnYyOpHOBGhimw==" 483 | } 484 | ] 485 | } 486 | } 487 | } 488 | } 489 | ~~~~~~~~ 490 | 491 | 现在你可以使用列表中第一个代码库的游标来执行第二个查询。通过给 `repositories` 的列举字段使用 `after` 参数,你可以指定获得下一页分页数据的起点。那么执行下面的查询后的结果会是什么样呢? 492 | 493 | {title="GitHub GraphQL Explorer",lang="json"} 494 | ~~~~~~~~ 495 | query OrganizationForLearningReact { 496 | organization(login: "the-road-to-learn-react") { 497 | name 498 | url 499 | # leanpub-start-insert 500 | repositories(first: 2, after: "Y3Vyc29yOnYyOpHOA8awSw==") { 501 | # leanpub-end-insert 502 | edges { 503 | node { 504 | name 505 | } 506 | cursor 507 | } 508 | } 509 | } 510 | } 511 | ~~~~~~~~ 512 | 513 | 在上一个结果中,只有第二项和一个新的第三项被取到了。第一项没有被取到因为你使用了它的游标作为 `after` 参数来取得它之后的所有元素。现在你可能已经想到如何来进行连续的分页列表查询了: 514 | 515 | * 不带游标参数执行初始查询 516 | 517 | * 执行接下来的查询的时候,使用上一次查询结果中**最后**一项的游标作为该次查询的游标 518 | 519 | 为了保持查询是动态的,我们可以把它的参数都抽成变量。然后你可以通过提供变量来使用拥有动态的 `cursor` 参数的查询。为了获取第一页,`after` 参数可以是 `undefined` 。最后,这就是所有关于你如何使用一个被称为分页的功能来从一个巨大的列表中获取很多页列表。你需要一个必需的参数来指明应该取得多少项以及一个可选参数,在这个例子中是 `after` 参数,来指明列表的起始点。 520 | 521 | 也有一些其他有用的方式通过使用元信息来为你的列表分页。当只使用最后一个代码库的 `cursor` 的时候,给每一个代码库都取得 `cursor` 字段可能显得很冗长,所以你可以给单独的 edge 去掉 `cursor` 字段,但给 `pageInfo` 对象加上它的 `endCursor` 和 `hasNextPage` 字段。你也可以请求列表的 `totalCount` 。 522 | 523 | {title="GitHub GraphQL Explorer",lang="json"} 524 | ~~~~~~~~ 525 | query OrganizationForLearningReact { 526 | organization(login: "the-road-to-learn-react") { 527 | name 528 | url 529 | repositories(first: 2, after: "Y3Vyc29yOnYyOpHOA8awSw==") { 530 | # leanpub-start-insert 531 | totalCount 532 | # leanpub-end-insert 533 | edges { 534 | node { 535 | name 536 | } 537 | } 538 | # leanpub-start-insert 539 | pageInfo { 540 | endCursor 541 | hasNextPage 542 | } 543 | # leanpub-end-insert 544 | } 545 | } 546 | } 547 | ~~~~~~~~ 548 | 549 | 这个 `totalCount` 字段表明了列表中元素的数量,而 `pageInfo` 字段给你提供了两个信息: 550 | 551 | * 和我们使用 `cursor` 字段一样,**`endCursor`** 也可以用来获取连续的列表,除了这一次我们只需要一个元字段来实现。但列表最后一项的 cursor 是足够用来请求列表中下一页的。 552 | 553 | * **`hasNextPage`** 告诉了你是否还能通过 GraphQL API 获得下一页。有时候你已经从你的服务端获得了最后一页。对于那些在滚动列表加载更多页的时候使用无限滚动的应用,你可以在没有下一页的情况下,停止获取页面信息。 554 | 555 | 通过元信息可以完整地实现分页。使用 GraphQL API 来实现[分页列表](https://www.robinwieruch.de/react-paginated-list/)和[无限滚动](https://www.robinwieruch.de/react-infinite-scroll/)使得信息更加方便。注意这包含了 GitHub 的 GraphQL API; 一个不同的分页的 GraphQL API 的字段可能使用了不同的命名方式,除了元信息或者采用完全不同的机制。 556 | 557 | ### 练习: 558 | * 把你的分页查询中的 `login` 和 `cursor` 抽为变量。 559 | * 把 `first` 参数替换为 `last` 参数。 560 | * 在 GraphiQL 的 "Docs" 侧边栏搜索 `repositories` 字段会看到:"A list of repositories that the ... owns." 561 | * 探索其他可以传给这个列举字段的参数。 562 | * 使用 `orderBy` 参数来获取一个递增或者递减的列表。 563 | * 延伸阅读:[GraphQL 中的分页](http://graphql.org/learn/pagination)。 564 | * 使用游标是 GitHub 唯一使用的解决方案。 565 | * 请确保同样理解其他的解决方案。 566 | 567 | | | 568 | 569 | 通过 GraphiQL 或者 GitHub 提供的 GraphQL Explorer 来与 GitHub 的 GraphQL API 交互仅仅只是开始。你现在应该已经掌握了 GraphQL 的基本概念。但是仍然有很多更加有趣的概念可以探索。在接下来的章节中,你将会实现一个完整运行的使用 React 与 GitHub 的 API 进行交互的 GraphQL 客户端应用。 570 | -------------------------------------------------------------------------------- /manuscript/05-graphql-react/index.md: -------------------------------------------------------------------------------- 1 | # React 和 GraphQL 的结合 2 | 3 | 我们将一起开发一个 GraphQL 的客户端程序,从中你将了解如何将 React 与 GraphQL 结合起来。目前我们不会使用 [Apollo 客户端](https://github.com/apollographql/apollo-client)或 [Relay](https://github.com/facebook/relay) 这样强大的工具来帮助你快速上手,而是使用基本的 HTTP 请求执行 GraphQL 查询和变更。在之后应用程序中,我们将引入 Apollo 作为你的 React.js 应用的 GraphQL 客户端。现阶段开发的应用只展示如何在 React 中基于 HTTP 使用 GraphQL。 4 | 5 | 在此过程中,你将构建一个类似 GitHub 的问题跟踪器,通过执行 GraphQL 的查询和变更来读写数据,基于 [GitHub's GraphQL API](https://developer.github.com/v4/) 的简单 GitHub 客户端。最终实现一个能够在 React 示例中展示 GraphQL的案例,也可以将其作为其他开发人员的学习工具,最终实现的应用程序可以参考 [GitHub 的代码库](https://github.com/rwieruch/react-graphql-github-vanilla)。 6 | 7 | ## 编写你的第一个 React GraphQL 客户端 8 | 9 | 在上一节之后,你应该准备好了如何在 React 应用程序中使用查询和变更。在本节中,你将创建一个使用 GitHub GraphQL API 的 React 应用程序。它是一个简单的问题跟踪器,需要显示在 GitHub 代码库中的一些 open issues,如果你对 React 缺乏经验,可以阅读 [React 学习之道](https://www.robinwieruch.de/the-road-to-learn-react),了解更多相关知识,为接下来的部分做好充分的准备。 10 | 11 | 现阶段的应用程序,不需要复杂的 React 配置。你只需使用 [create-react-app](https://github.com/facebook/create-react-app) 就可以创建无需额外配置的 React 应用程序。在命令行中输入以下指令,用npm安装它:`npm install -g create-react-app`。如果你想学习详细的 React 设置,请参考 [React 的 Webpack 设置指南](https://www.robinwieruch.de/minimal-react-webpack-babel-setup/)。 12 | 13 | 现在,让我们使用 create-react-app 创建应用程序。进入你常规的项目文件夹中,输入如下命令: 14 | 15 | {title="Command Line",lang="json"} 16 | ~~~~~~~~ 17 | create-react-app react-graphql-github-vanilla 18 | cd react-graphql-github-vanilla 19 | ~~~~~~~~ 20 | 21 | 创建应用程序之后,可以使用 `npm start` 和 `npm test` 对其进行测试。在学习了简单的 React 之后,你应该熟悉 npm,create-react-app 和 React。 22 | 23 | 接下来我们将主要关注在 src/App.js 文件上,你也可以将组件、配置或函数拆分到它们自己的文件夹和文件中。我们从上述文件中的 App 组件开始,为了简化它,你可以将其更改为如下内容: 24 | 25 | {title="src/App.js",lang="javascript"} 26 | ~~~~~~~~ 27 | import React, { Component } from 'react'; 28 | 29 | const TITLE = 'React GraphQL GitHub Client'; 30 | 31 | class App extends Component { 32 | render() { 33 | return ( 34 |
35 |

{TITLE}

36 |
37 | ); 38 | } 39 | } 40 | 41 | export default App; 42 | ~~~~~~~~ 43 | 44 | 现阶段组件只会渲染一个 `title` 作为标题。在实现其他的 React 组件之前,我们先安装一个使用 HTTP POST 方法执行查询和变更的第三方库来处理 GraphQL 请求。推荐使用 [axios](https://github.com/axios/axios)。在命令行中,输入如下命令在项目文件夹中安装 axios: 45 | 46 | {title="Command Line",lang="json"} 47 | ~~~~~~~~ 48 | npm install axios --save 49 | ~~~~~~~~ 50 | 51 | 接下来,可以在你的 App 组件中导入并配置它。它可以简化后续的开发步骤,因为在某种程度上,你只需要用你的 access token 和 GitHub 的 GraphQL API 配置它一次。 52 | 53 | 首先,在从 axios 创建配置实例时,为它定义一个基本的 URL。如前所述,你不需要在每次发出请求时都定义 GitHub 的 URL 端点,因为所有查询和变更都指向 GraphQL 中的相同 URL 端点。你可以使用对象和字段灵活地进行查询和变更结构。 54 | 55 | {title="src/App.js",lang="javascript"} 56 | ~~~~~~~~ 57 | import React, { Component } from 'react'; 58 | # leanpub-start-insert 59 | import axios from 'axios'; 60 | 61 | const axiosGitHubGraphQL = axios.create({ 62 | baseURL: 'https://api.github.com/graphql', 63 | }); 64 | # leanpub-end-insert 65 | 66 | ... 67 | 68 | export default App; 69 | ~~~~~~~~ 70 | 71 | 接下来,将 access token 作为 header 传递到配置中。使用这个 axios 实例发出的每个请求都会使用这个 header。 72 | 73 | {title="src/App.js",lang="javascript"} 74 | ~~~~~~~~ 75 | ... 76 | 77 | const axiosGitHubGraphQL = axios.create({ 78 | baseURL: 'https://api.github.com/graphql', 79 | # leanpub-start-insert 80 | headers: { 81 | Authorization: 'bearer YOUR_GITHUB_PERSONAL_ACCESS_TOKEN', 82 | }, 83 | # leanpub-end-insert 84 | }); 85 | 86 | ... 87 | ~~~~~~~~ 88 | 89 | 用你的 access token 替换 `YOUR_GITHUB_PERSONAL_ACCESS_TOKEN` 字符串。为了避免将访问令牌直接剪切和粘贴到源代码中,你可以创建一个 *.env* 文件来保存项目文件夹中命令行上的所有环境变量。如果不想在公开的 GitHub 代码库中共享个人令牌,可以将该文件添加到 .gitignore。 90 | 91 | {title="Command Line",lang="json"} 92 | ~~~~~~~~ 93 | touch .env 94 | ~~~~~~~~ 95 | 96 | 将环境变量定义在这个 *.env* 文件中。在使用 create-react-app 时,确保遵循正确的命名约束,它使用 `REACT_APP` 作为每个 key 的前缀。将下面的键值对粘贴在你的 *.env* 文件中。密钥必须有 `REACT_APP` 前缀,并且值必须是你 GitHub 的 access token。 97 | 98 | {title=".env",lang="javascript"} 99 | ~~~~~~~~ 100 | REACT_APP_GITHUB_PERSONAL_ACCESS_TOKEN=xxxXXX 101 | ~~~~~~~~ 102 | 103 | 现在,你可以将 access token 作为环境变量传递给 axios 配置,并使用字符串插值([模板字符串](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals)创建一个配置好的 axios 实例。 104 | 105 | {title="src/App.js",lang="javascript"} 106 | ~~~~~~~~ 107 | ... 108 | 109 | const axiosGitHubGraphQL = axios.create({ 110 | baseURL: 'https://api.github.com/graphql', 111 | headers: { 112 | # leanpub-start-insert 113 | Authorization: `bearer ${ 114 | process.env.REACT_APP_GITHUB_PERSONAL_ACCESS_TOKEN 115 | }`, 116 | # leanpub-end-insert 117 | }, 118 | }); 119 | 120 | ... 121 | ~~~~~~~~ 122 | 123 | axios 的初始设置基本上与我们之前使用 GraphiQL 应用程序访问 GitHub 的 GraphQL API 时所完成的设置相同,那时你还必须设置带有 access token 和端点 URL 的头文件。 124 | 125 | 接下来需要添加一个表单,用于从用户处获得关于 GitHub 组织和代码库的详细信息。这个表单包含一个输入栏,可以为特定的 GitHub 代码库请求一个可以分页的 issues 列表。首先,需要一个带有输入栏的表单来输入组织和代码库。其次,表单需要一个提交按钮来请求用户在输入字段中提供的组织和代码库的数据,并且将这些数据存放在于组件的本地状态中。接下来,当组件第一次挂载时,组织和代码库有一个初始的本地状态来请求初始数据。 126 | 127 | 我们可以分两步来实现这个场景。render 方法需要渲染一个带有输入栏的表单。表单必须有一个 `onSubmit` 处理方法,而输入栏需要一个 `onChange` 处理方法。输入栏使用组件中 state 的 `path` 作为值成为一个受控组件。 128 | 129 | {title="src/App.js",lang="javascript"} 130 | ~~~~~~~~ 131 | class App extends Component { 132 | render() { 133 | return ( 134 |
135 |

{TITLE}

136 | 137 |
138 | 141 | 147 | 148 |
149 | 150 |
151 | 152 | {/* Here comes the result! */} 153 |
154 | ); 155 | } 156 | } 157 | ~~~~~~~~ 158 | 159 | 声明要在 render 方法中使用的类方法。`componentDidMount()` 生命周期方法可用于在应用程序组件挂载时发出初始请求。在这个生命周期方法中,需要发出初始化请求为输入栏设置一个初始值。 160 | 161 | {title="src/App.js",lang="javascript"} 162 | ~~~~~~~~ 163 | class App extends Component { 164 | state = { 165 | path: 'the-road-to-learn-react/the-road-to-learn-react', 166 | }; 167 | 168 | componentDidMount() { 169 | // fetch data 170 | } 171 | 172 | onChange = event => { 173 | this.setState({ path: event.target.value }); 174 | }; 175 | 176 | onSubmit = event => { 177 | // fetch data 178 | 179 | event.preventDefault(); 180 | }; 181 | 182 | render() { 183 | ... 184 | } 185 | } 186 | ~~~~~~~~ 187 | 188 | 你可能在前面的实现中使用了以前未使用过的 React 类组件语法。如果你不熟悉它,请查看这个 [GitHub 代码库](https://github.com/the-road-to-learn-react/react-alternative-class-component-syntax)以获得更多的理解。使用**类字段声明**可以省略初始化本地状态的构造函数语句,而且不需要绑定类方法。相反,箭头函数将处理所有绑定行为。 189 | 190 | 依照 React 的最佳实践,将输入栏设置为受控组件。输入元素不应使用原生的 HTML 行为处理其内部状态;而应该是自然反应。 191 | 192 | {title="src/App.js",lang="javascript"} 193 | ~~~~~~~~ 194 | class App extends Component { 195 | ... 196 | 197 | render() { 198 | # leanpub-start-insert 199 | const { path } = this.state; 200 | # leanpub-end-insert 201 | 202 | return ( 203 |
204 |

{TITLE}

205 | 206 |
207 | 210 | 219 | 220 |
221 | 222 |
223 | 224 | {/* Here comes the result! */} 225 |
226 | ); 227 | } 228 | } 229 | ~~~~~~~~ 230 | 231 | 为表单提前设置——可使用的输入栏、一个提交按钮、`onChange()` 和 `onSubmit()` 类方法——是 React 中实现表单的一种常见方法。唯一增加的是 `componentDidMount()` 生命周期方法中的初始数据获取,通过为查询提供一个初始状态来从后端请求数据,从而改进用户体验。它是 [React 获取第三方 API 数据](https://www.robinwieruch.de/react-fetching-data/)的基础。 232 | 233 | When you start the application on the command line, you should see the initial state for the `path` in the input field. You should be able to change the state by entering something else in the input field, but nothing happens with `componentDidMount()` and submitting the form yet. 234 | 235 | 当你在命令行上启动应用程序时,应该会在输入栏中看到 `path` 的初始状态。并且能够通过在输入栏中输入其他内容来更改状态,但是在 `componentDidMount()` 方法中以及提交表单时什么都不会发生。 236 | 237 | You might wonder why there is only one input field to grab the information about the organization and repository. When opening up a repository on GitHub, you can see that the organization and repository are encoded in the URL, so it becomes a convenient way to show the same URL pattern for the input field. You can also split the `organization/repository` later at the `/` to get these values and perform the GraphQL query request. 238 | 239 | 你可能想知道为什么只需要一个输入字段来获取关于组织和仓库的信息。在 GitHub 上打开代码库时,你可以看到组织和代码库都是在 URL 中编码的,因此可以方便地用输入的字段来匹配相似的 URL 模式。你之后也可以以 `/` 分割 `organization/repository`,以获取这些值并执行 GraphQL 查询请求。 240 | 241 | ### 练习: 242 | 243 | * 查看[本节源码](https://github.com/the-road-to-graphql/react-graphql-github-vanilla/tree/ca7b278b8f602c46dfac64a1304d39a8e8e0006b) 244 | 245 | * 如果你不熟悉 React,可以阅读 *React 学习之道* 246 | 247 | ## 在 React 中执行 GraphQL 查询 248 | 249 | 在本章节中,你将在 React 中实现第一个 GraphQL 查询,从组织的一个代码库中获取 issues,但不是一次获取所有问题,先获取一个组织。我们先将查询定义为 App 组件上面的一个变量。 250 | 251 | {title="src/App.js",lang="javascript"} 252 | ~~~~~~~~ 253 | const GET_ORGANIZATION = ` 254 | { 255 | organization(login: "the-road-to-learn-react") { 256 | name 257 | url 258 | } 259 | } 260 | `; 261 | ~~~~~~~~ 262 | 263 | 使用 JavaScript 中的模板字符串将查询语句定义为多行的字符串。与之前在 GraphiQL 或 GitHub Explorer 中使用的查询相同。现在你就可以使用 axios 向 GitHub 的 GraphiQL API 发出 POST 请求。axios 的配置已经指向正确的 API 端点,并使用的是你的 access token。唯一剩下的事情是在 POST 请求期间将查询作为有效负载传递给它。端点的参数可以是空字符串,因为你在配置中定义了端点。当 App 组件生命周期执行到 `componentDidMount()` 时,就会执行请求。在 axios 的 promise 被解析之后,结果将会保留在控制台日志中。 264 | 265 | {title="src/App.js",lang="javascript"} 266 | ~~~~~~~~ 267 | ... 268 | 269 | const axiosGitHubGraphQL = axios.create({ 270 | baseURL: 'https://api.github.com/graphql', 271 | headers: { 272 | Authorization: `bearer ${ 273 | process.env.REACT_APP_GITHUB_PERSONAL_ACCESS_TOKEN 274 | }`, 275 | }, 276 | }); 277 | 278 | const GET_ORGANIZATION = ` 279 | { 280 | organization(login: "the-road-to-learn-react") { 281 | name 282 | url 283 | } 284 | } 285 | `; 286 | 287 | class App extends Component { 288 | ... 289 | 290 | componentDidMount() { 291 | # leanpub-start-insert 292 | this.onFetchFromGitHub(); 293 | # leanpub-end-insert 294 | } 295 | 296 | onSubmit = event => { 297 | // fetch data 298 | 299 | event.preventDefault(); 300 | }; 301 | 302 | # leanpub-start-insert 303 | onFetchFromGitHub = () => { 304 | axiosGitHubGraphQL 305 | .post('', { query: GET_ORGANIZATION }) 306 | .then(result => console.log(result)); 307 | }; 308 | # leanpub-end-insert 309 | 310 | ... 311 | } 312 | ~~~~~~~~ 313 | 314 | 你只需要使用 axios 执行一个 HTTP POST 请求,并将 GraphQL 查询作为有效负载。因为 axios 使用 promises,promise 最终会被解析后,你就掌握了 GraphQL API 的结果。这没什么惊讶的。它只是使用普通 JavaScript 实现的,用 axios 作为 HTTP 客户端,使用普通的 HTTP 执行 GraphQL 请求。 315 | 316 | 再次启动你的应用程序,并确认你已经在开发者模式下的控制台日志中得到了结果。如果得到了 [401 HTTP 状态码](https://en.wikipedia.org/wiki/List_of_HTTP_status_codes),说明你没有正确设置你的 access token。否则,你应该会在开发人员控制台日志中看到类似的结果。 317 | 318 | {title="Developer Tools",lang="json"} 319 | ~~~~~~~~ 320 | { 321 | "config": ..., 322 | "data":{ 323 | "data":{ 324 | "organization":{ 325 | "name":"The Road to learn React", 326 | "url":"https://github.com/the-road-to-learn-react" 327 | } 328 | } 329 | }, 330 | "headers": ..., 331 | "request": ..., 332 | "status": ..., 333 | "statusText": ... 334 | } 335 | ~~~~~~~~ 336 | 337 | 最外层信息是 axios 的元信息返回给你所发请求的所有内容。它都是 axios,与 GraphQL 还没有任何关系,这就是为什么它的大部分被占位符所替代。Axios 有一个 `data` 属性,代表 Axios 请求的结果。里面包裹了一个反映 GraphQL 结果的 `data` 属性。首先,`data` 属性在第一个结果中看起来是冗余的,但是一旦理解了它,你就会知道一个 `data` 属性来自 axios,而另一个来自 GraphQL 数据结构。最后,在第二个 `data` 属性中找到 GraphQL 查询的结果。在里面也可以找到包含已解析的名称和 url 字段作为字符串属性的组织。 338 | 339 | 下一步,你将把包含组织信息的结果存储在 React 的本地状态中。如果发生任何错误,还需要将潜在的错误存储在状态中。 340 | 341 | {title="src/App.js",lang="javascript"} 342 | ~~~~~~~~ 343 | class App extends Component { 344 | state = { 345 | path: 'the-road-to-learn-react/the-road-to-learn-react', 346 | # leanpub-start-insert 347 | organization: null, 348 | errors: null, 349 | # leanpub-end-insert 350 | }; 351 | 352 | ... 353 | 354 | onFetchFromGitHub = () => { 355 | axiosGitHubGraphQL 356 | .post('', { query: GET_ORGANIZATION }) 357 | .then(result => 358 | # leanpub-start-insert 359 | this.setState(() => ({ 360 | organization: result.data.data.organization, 361 | errors: result.data.errors, 362 | })), 363 | # leanpub-end-insert 364 | ); 365 | } 366 | 367 | ... 368 | 369 | } 370 | ~~~~~~~~ 371 | 372 | 接下来,你可以在 App 组件的 `render()` 方法中展示 Organization 的信息: 373 | 374 | {title="src/App.js",lang="javascript"} 375 | ~~~~~~~~ 376 | class App extends Component { 377 | ... 378 | 379 | render() { 380 | # leanpub-start-insert 381 | const { path, organization } = this.state; 382 | # leanpub-end-insert 383 | 384 | return ( 385 |
386 |

{TITLE}

387 | 388 |
389 | ... 390 |
391 | 392 |
393 | 394 | # leanpub-start-insert 395 | 396 | # leanpub-end-insert 397 |
398 | ); 399 | } 400 | } 401 | ~~~~~~~~ 402 | 403 | 将组织组件作为一个新的功能无状态组件引入,保持 App 组件的渲染方法简洁。因为这个应用程序只是一个简单的 GitHub 问题跟踪器,你已经可以在一小段话里提到它了。 404 | 405 | {title="src/App.js",lang="javascript"} 406 | ~~~~~~~~ 407 | class App extends Component { 408 | ... 409 | } 410 | 411 | # leanpub-start-insert 412 | const Organization = ({ organization }) => ( 413 |
414 |

415 | Issues from Organization: 416 | {organization.name} 417 |

418 |
419 | ); 420 | # leanpub-end-insert 421 | ~~~~~~~~ 422 | 423 | 在最后一步中,你需要决定在还没有获取任何东西时界面应该渲染什么,以及在发生错误时应该渲染什么。可以在 React 中使用[条件呈现](https://www.robinwieruch.de/conditional-rendering-react/)解决这些边界情况。对于第一个边缘情况,只需检查 `organization` 是否存在。 424 | 425 | {title="src/App.js",lang="javascript"} 426 | ~~~~~~~~ 427 | class App extends Component { 428 | ... 429 | 430 | render() { 431 | # leanpub-start-insert 432 | const { path, organization, errors } = this.state; 433 | # leanpub-end-insert 434 | 435 | return ( 436 |
437 | ... 438 | 439 |
440 | 441 | # leanpub-start-insert 442 | {organization ? ( 443 | 444 | ) : ( 445 |

No information yet ...

446 | )} 447 | # leanpub-end-insert 448 |
449 | ); 450 | } 451 | } 452 | ~~~~~~~~ 453 | 454 | 对于第二种边缘情况,你已经将错误传递给了 Organization 组件。如果出现了错误,它应该简单地将每个错误的错误消息渲染出来。否则,它应该渲染组织信息。对于 GraphQL 中的不同字段和环境,可能会有多个错误。 455 | 456 | {title="src/App.js",lang="javascript"} 457 | ~~~~~~~~ 458 | # leanpub-start-insert 459 | const Organization = ({ organization, errors }) => { 460 | if (errors) { 461 | return ( 462 |

463 | Something went wrong: 464 | {errors.map(error => error.message).join(' ')} 465 |

466 | ); 467 | } 468 | 469 | return ( 470 |
471 |

472 | Issues from Organization: 473 | {organization.name} 474 |

475 |
476 | ); 477 | }; 478 | # leanpub-end-insert 479 | ~~~~~~~~ 480 | 481 | 你已经在 React 应用程序中执行了第一个 GraphQL 查询,虽然这只是一个以查询语句作为有效负载的普通 HTTP POST 请求。使用了配置好的 axios 客户端实例。之后,你可以将结果存储在 React 的本地状态中,以便稍后显示。 482 | 483 | ### React 中 GraphQL 嵌套对象的使用 484 | 485 | 接下来,我们需要为组织添加一个嵌套对象。由于应用程序最终会在代码库中显示 issue,因此下一步你应该获取组织的代码库。请记住,查询操作会进入 GraphQL 图,因此我们可以在架构定义这两个实体之间的关系时将 `repository` 字段嵌套在 `organization` 中。 486 | 487 | {title="src/App.js",lang="javascript"} 488 | ~~~~~~~~ 489 | # leanpub-start-insert 490 | const GET_REPOSITORY_OF_ORGANIZATION = ` 491 | # leanpub-end-insert 492 | { 493 | organization(login: "the-road-to-learn-react") { 494 | name 495 | url 496 | # leanpub-start-insert 497 | repository(name: "the-road-to-learn-react") { 498 | name 499 | url 500 | } 501 | # leanpub-end-insert 502 | } 503 | } 504 | `; 505 | 506 | class App extends Component { 507 | ... 508 | 509 | onFetchFromGitHub = () => { 510 | axiosGitHubGraphQL 511 | # leanpub-start-insert 512 | .post('', { query: GET_REPOSITORY_OF_ORGANIZATION }) 513 | # leanpub-end-insert 514 | .then(result => 515 | ... 516 | ); 517 | }; 518 | 519 | ... 520 | } 521 | ~~~~~~~~ 522 | 523 | 虽然现在代码库与组织的名称相同,但是没关系,因为稍后你可以动态地定义组织和代码库。接下来,你可以使用另一个代码库组件作为子组件扩展组织组件。现在,查询操作的结果应该在组织对象中具有一个嵌套的代码库对象。 524 | 525 | {title="src/App.js",lang="javascript"} 526 | ~~~~~~~~ 527 | const Organization = ({ organization, errors }) => { 528 | if (errors) { 529 | ... 530 | } 531 | 532 | return ( 533 |
534 |

535 | Issues from Organization: 536 | {organization.name} 537 |

538 | # leanpub-start-insert 539 | 540 | # leanpub-end-insert 541 |
542 | ); 543 | }; 544 | 545 | # leanpub-start-insert 546 | const Repository = ({ repository }) => ( 547 |
548 |

549 | In Repository: 550 | {repository.name} 551 |

552 |
553 | ); 554 | # leanpub-end-insert 555 | ~~~~~~~~ 556 | 557 | GraphQL 查询结构与组件树完全一致。它通过将其他对象嵌套到查询中,并沿着 GraphQL 查询的结构扩展组件树,这样就可以继续拓展查询结构。由于应用程序是一个问题跟踪器,我们需要向查询添加 issue 列表字段。 558 | 559 | 如果你想更详细地了解查询结构,请打开 GraphiQL 中的 "Docs" 侧栏以了解 `Organization`, `Repository`, `Issue`,也可以在那里找到分页 issue 列表字段。对图表结构有一个大概的了解总是好的。 560 | 561 | 现在让我们用 issue 列表字段扩展查询操作。这些 issue 最终是一个分页列表,我们稍后会介绍这些内容;现在,将它嵌套在 `repository` 字段中,并使用 `last` 参数来获取列表的最后一项。 562 | 563 | {title="src/App.js",lang="javascript"} 564 | ~~~~~~~~ 565 | # leanpub-start-insert 566 | const GET_ISSUES_OF_REPOSITORY = ` 567 | # leanpub-end-insert 568 | { 569 | organization(login: "the-road-to-learn-react") { 570 | name 571 | url 572 | repository(name: "the-road-to-learn-react") { 573 | name 574 | url 575 | # leanpub-start-insert 576 | issues(last: 5) { 577 | edges { 578 | node { 579 | id 580 | title 581 | url 582 | } 583 | } 584 | } 585 | # leanpub-end-insert 586 | } 587 | } 588 | } 589 | `; 590 | ~~~~~~~~ 591 | 592 | 你还可以使用问题的 `node` 字段上的 `id` 字段为每个 issue 请求一个 id,以便于在组件中使用在列表中呈现的 `key` 属性,这是在 React 中的最佳实践。记得要在执行请求时调整查询变量的名称。 593 | 594 | {title="src/App.js",lang="javascript"} 595 | ~~~~~~~~ 596 | class App extends Component { 597 | ... 598 | 599 | onFetchFromGitHub = () => { 600 | axiosGitHubGraphQL 601 | # leanpub-start-insert 602 | .post('', { query: GET_ISSUES_OF_REPOSITORY }) 603 | # leanpub-end-insert 604 | .then(result => 605 | ... 606 | ); 607 | }; 608 | 609 | ... 610 | } 611 | ~~~~~~~~ 612 | 613 | 组件结构非常自然地遵循了查询结构。你可以向代码库组件中添加已呈现的 issue 列表。你也可以将其作为重构提取到自己的组件中,让你的组件变得更加简洁,增加可读性和可维护性。 614 | 615 | {title="src/App.js",lang="javascript"} 616 | ~~~~~~~~ 617 | const Repository = ({ repository }) => ( 618 |
619 |

620 | In Repository: 621 | {repository.name} 622 |

623 | 624 | # leanpub-start-insert 625 | 632 | # leanpub-end-insert 633 |
634 | ); 635 | ~~~~~~~~ 636 | 637 | 这就是查询操作中的嵌套对象,字段和列表字段。当你再次运行你的应用程序,你会在浏览器中看到指定代码库的最后提的 issue。 638 | 639 | ### React-GraphQL 变量和参数的使用 640 | 641 | 接下来我们将使用表单和输入元素。当用户填写内容并提交内容时,它们应该用于从 Github GraphQL API 请求数据。该内容还用于 App 组件的 `componentDidMount()` 中的初始请求。目前,组织 login 和代码库 name 应该是查询操作中的内联参数。现在,你应该能够通过从本地状态到查询的 path 来动态定义组织和代码库。这就是 GraphQL 查询中的变量发挥作用的地方,还记得吗? 642 | 643 | 首先,我们先使用一种简单的方法,通过 JavaScript 来执行字符串插值,注意不是 GraphQL 变量。为此,请将查询操作从模板文字变量重构为返回模板文字变量的函数。通过使用该函数,你可以动态地传入组织和代码库。 644 | 645 | {title="src/App.js",lang="javascript"} 646 | ~~~~~~~~ 647 | # leanpub-start-insert 648 | const getIssuesOfRepositoryQuery = (organization, repository) => ` 649 | # leanpub-end-insert 650 | { 651 | # leanpub-start-insert 652 | organization(login: "${organization}") { 653 | # leanpub-end-insert 654 | name 655 | url 656 | # leanpub-start-insert 657 | repository(name: "${repository}") { 658 | # leanpub-end-insert 659 | name 660 | url 661 | issues(last: 5) { 662 | edges { 663 | node { 664 | id 665 | title 666 | url 667 | } 668 | } 669 | } 670 | } 671 | } 672 | } 673 | `; 674 | ~~~~~~~~ 675 | 676 | 接下来,可以在提交句柄中调用 `onFetchFromGitHub()` 这个方法,也可以在组件挂载 `componentDidMount()` 时, `path` 属性的本地状态初始化后调用。这些是在初始渲染时从 GraphQL API 获取数据的两个基本位置,以及从按钮单击获取每个手动提交的数据。 677 | 678 | {title="src/App.js",lang="javascript"} 679 | ~~~~~~~~ 680 | class App extends Component { 681 | state = { 682 | path: 'the-road-to-learn-react/the-road-to-learn-react', 683 | organization: null, 684 | errors: null, 685 | }; 686 | 687 | componentDidMount() { 688 | # leanpub-start-insert 689 | this.onFetchFromGitHub(this.state.path); 690 | # leanpub-end-insert 691 | } 692 | 693 | onChange = event => { 694 | this.setState({ path: event.target.value }); 695 | }; 696 | 697 | onSubmit = event => { 698 | # leanpub-start-insert 699 | this.onFetchFromGitHub(this.state.path); 700 | # leanpub-end-insert 701 | 702 | event.preventDefault(); 703 | }; 704 | 705 | onFetchFromGitHub = () => { 706 | ... 707 | } 708 | 709 | render() { 710 | ... 711 | } 712 | } 713 | ~~~~~~~~ 714 | 715 | 最后,调用返回查询的函数,而不是直接将查询字符串作为有效负载传递。对字符串使用 JavaScript 的 [split](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/split) 方法从路径变量中检索 `/` 字符的前缀和后缀,其中前缀是组织,后缀是代码库。 716 | 717 | {title="src/App.js",lang="javascript"} 718 | ~~~~~~~~ 719 | class App extends Component { 720 | ... 721 | 722 | # leanpub-start-insert 723 | onFetchFromGitHub = path => { 724 | const [organization, repository] = path.split('/'); 725 | # leanpub-end-insert 726 | 727 | axiosGitHubGraphQL 728 | .post('', { 729 | # leanpub-start-insert 730 | query: getIssuesOfRepositoryQuery(organization, repository), 731 | # leanpub-end-insert 732 | }) 733 | .then(result => 734 | this.setState(() => ({ 735 | organization: result.data.data.organization, 736 | errors: result.data.errors, 737 | })), 738 | ); 739 | }; 740 | 741 | ... 742 | } 743 | ~~~~~~~~ 744 | 745 | 由于 split 方法的返回值是一个数组,假设路径中只有一个斜杠,那么该数组应包含两个值:组织和代码库。所以,使用 JavaScript 数组解构从同一行中的数组中提取两个值很方便。 746 | 747 | 注意,此程序仅作为学习体验,未考虑其健壮性。任何人都不可能要求用户使用与 组织/代码库 不同的模式输入组织和代码库,所以这里没有包含任何验证。尽管如此,这也能为你学习概念打好良好的基础。 748 | 749 | 如果你想更进一步,可以将类方法的第一部分提取到它自己的函数中,该函数使用 axios 发送带有查询的请求并返回一个 promise,可用于将结果解析为本地状态,然后你可以在 `then()` 函数中使用 `this.setState()` 方法来解析程序块中的 promise. 750 | 751 | {title="src/App.js",lang="javascript"} 752 | ~~~~~~~~ 753 | # leanpub-start-insert 754 | const getIssuesOfRepository = path => { 755 | const [organization, repository] = path.split('/'); 756 | 757 | return axiosGitHubGraphQL.post('', { 758 | query: getIssuesOfRepositoryQuery(organization, repository), 759 | }); 760 | }; 761 | # leanpub-end-insert 762 | 763 | class App extends Component { 764 | ... 765 | 766 | onFetchFromGitHub = path => { 767 | # leanpub-start-insert 768 | getIssuesOfRepository(path).then(result => 769 | # leanpub-end-insert 770 | this.setState(() => ({ 771 | organization: result.data.data.organization, 772 | errors: result.data.errors, 773 | })), 774 | ); 775 | }; 776 | 777 | ... 778 | } 779 | ~~~~~~~~ 780 | 781 | 你也可以将应用程序的功能或者组件拆分为多个部分,使它们简洁,可读,可重用和[可测试](https://www.robinwieruch.de/react-testing-tutorial/)。传递给 `this.setState()` 的函数可以作为高阶函数提取。它必须是高阶函数,因为你需要传递 promise 的结果,但也为 `this.setState()` 方法提供函数。 782 | 783 | {title="src/App.js",lang="javascript"} 784 | ~~~~~~~~ 785 | # leanpub-start-insert 786 | const resolveIssuesQuery = queryResult => () => ({ 787 | organization: queryResult.data.data.organization, 788 | errors: queryResult.data.errors, 789 | }); 790 | # leanpub-start-insert 791 | 792 | class App extends Component { 793 | ... 794 | 795 | onFetchFromGitHub = path => { 796 | # leanpub-start-insert 797 | getIssuesOfRepository(path).then(queryResult => 798 | this.setState(resolveIssuesQuery(queryResult)), 799 | # leanpub-end-insert 800 | ); 801 | }; 802 | 803 | ... 804 | } 805 | ~~~~~~~~ 806 | 807 | 现在,你已经通过在查询中提供动态参数灵活的进行查询操作。在终端尝试启动你的程序,在特定代码库(例如 facebook/create-react-app)中填充不同的组织。 808 | 809 | 这是一个很好的设置,但还没有什么可见的变量。你只需使用一个函数和带有模板文字的字符串插值将参数传递给查询操作。现在我们将使用 GraphQL 变量,将查询变量重新调整到定义内联变量的模板文本中。 810 | 811 | {title="src/App.js",lang="javascript"} 812 | ~~~~~~~~ 813 | # leanpub-start-insert 814 | const GET_ISSUES_OF_REPOSITORY = ` 815 | query ($organization: String!, $repository: String!) { 816 | organization(login: $organization) { 817 | # leanpub-end-insert 818 | name 819 | url 820 | # leanpub-start-insert 821 | repository(name: $repository) { 822 | # leanpub-end-insert 823 | name 824 | url 825 | issues(last: 5) { 826 | edges { 827 | node { 828 | id 829 | title 830 | url 831 | } 832 | } 833 | } 834 | } 835 | } 836 | } 837 | `; 838 | ~~~~~~~~ 839 | 840 | 现在,你可以将这些变量作为 HTTP POST 请求的查询参数: 841 | 842 | {title="src/App.js",lang="javascript"} 843 | ~~~~~~~~ 844 | const getIssuesOfRepository = path => { 845 | const [organization, repository] = path.split('/'); 846 | 847 | return axiosGitHubGraphQL.post('', { 848 | # leanpub-start-insert 849 | query: GET_ISSUES_OF_REPOSITORY, 850 | variables: { organization, repository }, 851 | # leanpub-end-insert 852 | }); 853 | }; 854 | ~~~~~~~~ 855 | 856 | 最后,查询操作将变量纳入考虑,而不用字符串插值的方法去检测函数。我强烈建议在继续下一节之前先练习下面的练习。我们还没有讨论像碎片或操作名称这样的特性,但是我们很快就会使用 Apollo 代替普通的带 axios 的 HTTP 来覆盖它们。 857 | 858 | ### 练习: 859 | 860 | * 查看[本节源码](https://github.com/the-road-to-graphql/react-graphql-github-vanilla/tree/c08126a9ec91dde4198ae85bb2f194fa7767c683) 861 | * 探索你的组织、代码库和 issues 中并尝试添加字段 862 | * 扩展你的组件以显示附加信息 863 | * 延伸阅读:[通过 HTTP 提供 GraphQL API 服务](http://graphql.org/learn/serving-over-http/) 864 | 865 | ## React 中的 GraphQL 分页 866 | 867 | 上一节中,你在 React 应用上实现了一个用于 GraphQL 查询的列表字段,它能得到一个包含部分查询结果的列表,其中的元素满足指定的嵌套结构。 868 | 869 | 本节将继续在使用 React 的基础上,更为深入地探索基于 GraphQL 列表字段实现的分页功能。你将首先了解列表字段的参数使用,而后在查询中添加另一个嵌套列表字段,从而能够在查询中实现获取下一页 `issues` 的功能。 870 | 871 | 我们首先为这个 `issues` 列表字段添加一个额外参数: 872 | 873 | {title="src/App.js",lang="javascript"} 874 | ~~~~~~~~ 875 | const GET_ISSUES_OF_REPOSITORY = ` 876 | query ($organization: String!, $repository: String!) { 877 | organization(login: $organization) { 878 | name 879 | url 880 | repository(name: $repository) { 881 | name 882 | url 883 | # leanpub-start-insert 884 | issues(last: 5, states: [OPEN]) { 885 | # leanpub-end-insert 886 | edges { 887 | node { 888 | id 889 | title 890 | url 891 | } 892 | } 893 | } 894 | } 895 | } 896 | } 897 | `; 898 | ~~~~~~~~ 899 | 900 | 如果你在 GraphiQL 工具中通过 "Docs" 边栏查看过这个 `issues` 列表字段的参数信息,就能找到可供使用的参数信息。其中的一个参数便是 `states`,用于决定获取处在开启状态或关闭状态的 issues,亦或两者同时。如果仅需要展示处于开启状态的 issues,那么可以进一步提炼这个列表字段,方法如上述代码所示。通过 GitHub 的 API 文档,你能够查看关于这个 `issues` 列表字段的更多参数信息,其它的列表字段也是一样。 901 | 902 | 现在我们将实现另一个列表字段,以便用于完善分页功能。我们知道,仓库中的每个 issue 都可能具有 reactions,也就是像笑脸或者大拇指之类的表情。这些 reactions 可以被视作另一项分页内容,为了展示它们,首先在查询中添加一个嵌套的 reactions 列表字段: 903 | 904 | {title="src/App.js",lang="javascript"} 905 | ~~~~~~~~ 906 | const GET_ISSUES_OF_REPOSITORY = ` 907 | query ($organization: String!, $repository: String!) { 908 | organization(login: $organization) { 909 | name 910 | url 911 | repository(name: $repository) { 912 | name 913 | url 914 | issues(last: 5, states: [OPEN]) { 915 | edges { 916 | node { 917 | id 918 | title 919 | url 920 | # leanpub-start-insert 921 | reactions(last: 3) { 922 | edges { 923 | node { 924 | id 925 | content 926 | } 927 | } 928 | } 929 | # leanpub-end-insert 930 | } 931 | } 932 | } 933 | } 934 | } 935 | } 936 | `; 937 | ~~~~~~~~ 938 | 939 | 之后,我们继续通过 React 组件来渲染 reactions 列表。这里本应当为列表和条目定义专门的组件,例如叫做 ReactionsList 和 ReactionItem,可以将此当作课后练习,从而提升应用代码的可读性和可维护性。 940 | 941 | {title="src/App.js",lang="javascript"} 942 | ~~~~~~~~ 943 | const Repository = ({ repository }) => ( 944 |
945 | ... 946 | 947 | 962 |
963 | ); 964 | ~~~~~~~~ 965 | 966 | 通过扩展查询结构以及 React 组件内容,你已经成功渲染出相应结果。由于作为数据源的 GraphQL API 为其字段定义了清晰的结构和关联性,这里的实现方式直截了当。 967 | 968 | 最后,为了做出完整的应用,你需要实现一个对 `issues` 列表字段的实际分页功能,通过点击一个按钮从 GraphQL API 获取更多的 issues。以下是按钮部分的实现代码: 969 | 970 | {title="src/App.js",lang="javascript"} 971 | ~~~~~~~~ 972 | const Repository = ({ 973 | repository, 974 | # leanpub-start-insert 975 | onFetchMoreIssues, 976 | # leanpub-end-insert 977 | }) => ( 978 |
979 | ... 980 | 981 | 984 | 985 | # leanpub-start-insert 986 |
987 | # leanpub-end-insert 988 | 989 | # leanpub-start-insert 990 | 991 | # leanpub-end-insert 992 |
993 | ); 994 | ~~~~~~~~ 995 | 996 | 该按钮的处理函数经由更外层传入 Repository 组件: 997 | 998 | {title="src/App.js",lang="javascript"} 999 | ~~~~~~~~ 1000 | const Organization = ({ 1001 | organization, 1002 | errors, 1003 | # leanpub-start-insert 1004 | onFetchMoreIssues, 1005 | # leanpub-end-insert 1006 | }) => { 1007 | ... 1008 | 1009 | return ( 1010 |
1011 |

1012 | Issues from Organization: 1013 | {organization.name} 1014 |

1015 | 1021 |
1022 | ); 1023 | }; 1024 | ~~~~~~~~ 1025 | 1026 | 该函数在 App 组件中作为类方法实现,同样也作为属性传入 Organization 组件。 1027 | 1028 | {title="src/App.js",lang="javascript"} 1029 | ~~~~~~~~ 1030 | class App extends Component { 1031 | ... 1032 | 1033 | # leanpub-start-insert 1034 | onFetchMoreIssues = () => { 1035 | ... 1036 | }; 1037 | # leanpub-end-insert 1038 | 1039 | render() { 1040 | const { path, organization, errors } = this.state; 1041 | 1042 | return ( 1043 |
1044 | ... 1045 | 1046 | {organization ? ( 1047 | 1054 | ) : ( 1055 |

No information yet ...

1056 | )} 1057 |
1058 | ); 1059 | } 1060 | } 1061 | ~~~~~~~~ 1062 | 1063 | 开始实现功能之前,首先需要考虑如何标明这个分页列表中的下一页。在列表字段中,可以通过添加额外的内部字段来获取相关元信息,例如 `pageInfo` 和 `totalCount`,前者可用于按钮点击的事件处理中定义下一页的相关信息。此外,`totalCount` 可辅助判断下一页中的条目数量: 1064 | 1065 | {title="src/App.js",lang="javascript"} 1066 | ~~~~~~~~ 1067 | const GET_ISSUES_OF_REPOSITORY = ` 1068 | query ($organization: String!, $repository: String!) { 1069 | organization(login: $organization) { 1070 | name 1071 | url 1072 | repository(name: $repository) { 1073 | ... 1074 | issues(last: 5, states: [OPEN]) { 1075 | edges { 1076 | ... 1077 | } 1078 | # leanpub-start-insert 1079 | totalCount 1080 | pageInfo { 1081 | endCursor 1082 | hasNextPage 1083 | } 1084 | # leanpub-end-insert 1085 | } 1086 | } 1087 | } 1088 | } 1089 | `; 1090 | ~~~~~~~~ 1091 | 1092 | 通过这些信息,现在你能将这个游标作为变量传入查询中,从而获取下一页的 issues 内容。这个游标,也就是 `after` 参数,在分页列表中定义了获取更多条目所需的起始位置。 1093 | 1094 | {title="src/App.js",lang="javascript"} 1095 | ~~~~~~~~ 1096 | class App extends Component { 1097 | ... 1098 | 1099 | onFetchMoreIssues = () => { 1100 | # leanpub-start-insert 1101 | const { 1102 | endCursor, 1103 | } = this.state.organization.repository.issues.pageInfo; 1104 | # leanpub-end-insert 1105 | 1106 | # leanpub-start-insert 1107 | this.onFetchFromGitHub(this.state.path, endCursor); 1108 | # leanpub-end-insert 1109 | }; 1110 | 1111 | ... 1112 | } 1113 | ~~~~~~~~ 1114 | 1115 | 名为 `onFetchFromGitHub` 的类方法尚未定义第二个参数,现在让我们来定义它。 1116 | 1117 | {title="src/App.js",lang="javascript"} 1118 | ~~~~~~~~ 1119 | # leanpub-start-insert 1120 | const getIssuesOfRepository = (path, cursor) => { 1121 | # leanpub-end-insert 1122 | const [organization, repository] = path.split('/'); 1123 | 1124 | return axiosGitHubGraphQL.post('', { 1125 | query: GET_ISSUES_OF_REPOSITORY, 1126 | # leanpub-start-insert 1127 | variables: { organization, repository, cursor }, 1128 | # leanpub-end-insert 1129 | }); 1130 | }; 1131 | 1132 | class App extends Component { 1133 | ... 1134 | 1135 | # leanpub-start-insert 1136 | onFetchFromGitHub = (path, cursor) => { 1137 | getIssuesOfRepository(path, cursor).then(queryResult => 1138 | this.setState(resolveIssuesQuery(queryResult, cursor)), 1139 | # leanpub-end-insert 1140 | ); 1141 | }; 1142 | 1143 | ... 1144 | } 1145 | ~~~~~~~~ 1146 | 1147 | 新的参数被直接传入 `getIssuesOfRepository()` 函数,后者用于构造 GraphQL 的 API 请求,并返回对应查询结果的 Promise。通过观察其它对 `onFetchFromGitHub()` 类方法的调用,能够注意到它们并未使用第二个参数,因此实际传入 GraphQL API 调用中的参数是 `undefined`。最终,这个查询要么使用了传入的游标信息作为参数来获取下一页的列表,要么由于游标未定义获取到第一页的列表。 1148 | 1149 | {title="src/App.js",lang="javascript"} 1150 | ~~~~~~~~ 1151 | const GET_ISSUES_OF_REPOSITORY = ` 1152 | query ( 1153 | $organization: String!, 1154 | $repository: String!, 1155 | # leanpub-start-insert 1156 | $cursor: String 1157 | # leanpub-end-insert 1158 | ) { 1159 | organization(login: $organization) { 1160 | name 1161 | url 1162 | repository(name: $repository) { 1163 | ... 1164 | # leanpub-start-insert 1165 | issues(first: 5, after: $cursor, states: [OPEN]) { 1166 | # leanpub-end-insert 1167 | edges { 1168 | ... 1169 | } 1170 | totalCount 1171 | pageInfo { 1172 | endCursor 1173 | hasNextPage 1174 | } 1175 | } 1176 | } 1177 | } 1178 | } 1179 | `; 1180 | ~~~~~~~~ 1181 | 1182 | 上面的模板字符串中,`cursor` 作为变量传入到该列表字段的 `after` 参数中。因为没有后置的的感叹号声明强制要求,所以可以是 `undefined`,这种场景出现在分页列表中获取第一页的情况下。此外,`issues` 列表字段中的 `last` 参数改成了 `first`,因为获取到最后一个条目之后就不存在下一页了,因此必须从第一个条目开始不断获取更多内容,直到到达列表中的最后一个条目为止。 1183 | 1184 | 以上就基本是在 React 应用中使用 GraphQL 获取分页列表所需的过程,不过还差最后一步。目前为止,App 组件中关于 issues 的状态并未得到更新,因此仅显示第一个请求中的结果。为了在保持组织和仓库基本信息不变的情况下合并 issues 的分页列表,最佳的操作时间就是查询得到的 Promise 完成时。由于这个过程已经被提取为 App 组件之外的独立函数,因此可以在该函数中对输入数据进行必要的数据处理。需要注意的是,这里的输入数据既可以是应用挂载时的第一次请求结果,也可以是点击 "More" 按钮时获取的后续结果。 1185 | 1186 | {title="src/App.js",lang="javascript"} 1187 | ~~~~~~~~ 1188 | # leanpub-start-insert 1189 | const resolveIssuesQuery = (queryResult, cursor) => state => { 1190 | const { data, errors } = queryResult.data; 1191 | 1192 | if (!cursor) { 1193 | return { 1194 | organization: data.organization, 1195 | errors, 1196 | }; 1197 | } 1198 | 1199 | const { edges: oldIssues } = state.organization.repository.issues; 1200 | const { edges: newIssues } = data.organization.repository.issues; 1201 | const updatedIssues = [...oldIssues, ...newIssues]; 1202 | 1203 | return { 1204 | organization: { 1205 | ...data.organization, 1206 | repository: { 1207 | ...data.organization.repository, 1208 | issues: { 1209 | ...data.organization.repository.issues, 1210 | edges: updatedIssues, 1211 | }, 1212 | }, 1213 | }, 1214 | errors, 1215 | }; 1216 | }; 1217 | # leanpub-end-insert 1218 | ~~~~~~~~ 1219 | 1220 | 由于更新机制变得更复杂,该函数也经历了全面重写。首先,`cursor` 被作为函数参数传入,用于确定是首次查询还是获取下一页内容的查询;随后,当 `cursor` 是 `undefined` 时函数会提前结束并将查询结果简单包装后直接返回,与原先的逻辑相同,这里对应于 App 组件挂载或者用户提交新的输入的场景,应当覆盖之前的状态;其次,当 `cursor` 存在也就是加载更多的查询时,查询结果中的 issues 列表会与已有的列表进行合并,为了显得更为直观而使用了 JavaScript 解构别名语法;最后,该函数返回了更新后的对象,由于存在多层嵌套而使用了 JavaScript 扩展(Spread)语法逐层更新,不过只有 `edges` 属性需要更新为合并后的 issues 列表。 1221 | 1222 | 之后,根据 `pageInfo` 对象中的 `hasNextPage` 来决定是否显示 "More" 按钮,当没有更多页面时按钮消失。 1223 | 1224 | {title="src/App.js",lang="javascript"} 1225 | ~~~~~~~~ 1226 | const Repository = ({ repository, onFetchMoreIssues }) => ( 1227 |
1228 | ... 1229 | 1230 |
1231 | 1232 | # leanpub-start-insert 1233 | {repository.issues.pageInfo.hasNextPage && ( 1234 | # leanpub-end-insert 1235 | 1236 | # leanpub-start-insert 1237 | )} 1238 | # leanpub-end-insert 1239 |
1240 | ); 1241 | ~~~~~~~~ 1242 | 1243 | 至此你已经成功地在 React 应用中实现了基于 GraphQL 的分页功能,如果需要,可以自行练习更多关于 issues 和 reactions 的参数使用。通过 GraphiQL 中地 "Docs" 边栏可以查看列表字段的参数信息,有些参数是公共的,而有些是部分列表所特有的,这些参数将帮助你实现精致的 GraphQL 查询。 1244 | 1245 | ### 练习: 1246 | 1247 | * 确认[本节源代码](https://github.com/the-road-to-graphql/react-graphql-github-vanilla/tree/060677346e8955fb1a6c7579859ce92e62e1f406) 1248 | * 探索更多关于 `issues` 和 `reactions` 列表字段的参数信息,包括公共和类型独有的参数 1249 | * 思考对深度嵌套状态对象的更新机制的优化方案,并[在此贡献你的想法](https://github.com/rwieruch/react-graphql-github-apollo/pull/14) 1250 | 1251 | ## React 中的 GraphQL 变更操作 1252 | 1253 | 你已经尝试了在 React 应用中通过 GraphQL 获取大量数据,这也是 GraphQL 的主要应用场景。不过,这种接口永远存在两个方向的操作:读取和写入。在 GraphQL 中写入操作被称为变更。此前,你已经在没有 React 的情况下通过 GraphiQL 使用过 GraphQL 变更操作,本节中你将在 React 应用中实现 GraphQL 变更操作。 1254 | 1255 | 你已经在 GraphiQL 执行过 GitHub 的 `addStar` 变更操作了,现在将通过 React 实现同样的操作。开始实现 star 某仓库这个变更操作之前,你需要查询关于这个仓库所需的部分额外信息。 1256 | 1257 | {title="src/App.js",lang="javascript"} 1258 | ~~~~~~~~ 1259 | const GET_ISSUES_OF_REPOSITORY = ` 1260 | query ( 1261 | $organization: String!, 1262 | $repository: String!, 1263 | $cursor: String 1264 | ) { 1265 | organization(login: $organization) { 1266 | name 1267 | url 1268 | repository(name: $repository) { 1269 | # leanpub-start-insert 1270 | id 1271 | # leanpub-end-insert 1272 | name 1273 | url 1274 | # leanpub-start-insert 1275 | viewerHasStarred 1276 | # leanpub-end-insert 1277 | issues(first: 5, after: $cursor, states: [OPEN]) { 1278 | ... 1279 | } 1280 | } 1281 | } 1282 | } 1283 | `; 1284 | ~~~~~~~~ 1285 | 1286 | 通过 `viewerHasStarred` 字段可以知晓当前用户是否已经 star 了这个仓库,从而帮助确定下一步的变更操作是 `addStar` 还是 `removeStar`。现在你只需要实现 `addStar` 变更操作,而 `removeStar` 部分将作为本节的练习。另外,查询操作中返回的 `id` 字段返回了该仓库的标识符,能够用于在后续变更操作中指明目标。 1287 | 1288 | 触发这个变更操作的最佳场景,莫过于一个对仓库 star 或取消 star 的按钮,通过 `viewerHasStarred` 这个布尔值进行条件渲染,得到一个显示内容为 "Star" 或 "Unstar" 的按钮。鉴于 star 的目标是一个仓库,因此在 Repository 组件中触发该变更操作最合适不过。 1289 | 1290 | {title="src/App.js",lang="javascript"} 1291 | ~~~~~~~~ 1292 | const Repository = ({ 1293 | repository, 1294 | onFetchMoreIssues, 1295 | # leanpub-start-insert 1296 | onStarRepository, 1297 | # leanpub-end-insert 1298 | }) => ( 1299 |
1300 | ... 1301 | 1302 | # leanpub-start-insert 1303 | 1309 | # leanpub-end-insert 1310 | 1311 | 1314 |
1315 | ); 1316 | ~~~~~~~~ 1317 | 1318 | 为了确定用户 star 的是哪一个仓库,变更操作必须知晓该仓库的 `id` 信息,这里通过 `viewerHasStarred` 属性作为参数传入,以便后续能够区分 star 或取消 star 的场景。 1319 | 1320 | {title="src/App.js",lang="javascript"} 1321 | ~~~~~~~~ 1322 | const Repository = ({ repository, onStarRepository }) => ( 1323 |
1324 | ... 1325 | 1326 | 1336 | 1337 | ... 1338 |
1339 | ); 1340 | ~~~~~~~~ 1341 | 1342 | 对应的事件处理函数应该定义在 App 组件中,向下传递至 Repository 组件,中途也经过了 Organization 组件。 1343 | 1344 | {title="src/App.js",lang="javascript"} 1345 | ~~~~~~~~ 1346 | const Organization = ({ 1347 | organization, 1348 | errors, 1349 | onFetchMoreIssues, 1350 | # leanpub-start-insert 1351 | onStarRepository, 1352 | # leanpub-end-insert 1353 | }) => { 1354 | ... 1355 | 1356 | return ( 1357 |
1358 | ... 1359 | 1366 |
1367 | ); 1368 | }; 1369 | ~~~~~~~~ 1370 | 1371 | 现在可以尝试在 App 组件中定义这个方法,很容易发现 `id` 和 `viewerHasStarred` 的信息可以从应用的本地状态中解构得到,因此无需在函数中传入便可直接使用。不过,鉴于 Repository 已经知晓这些信息,作为函数参数中传入仍然是合理的选择,并且能够使得数据流更加直观。特别是考虑到之后有通过多个 Repository 组件处理多个仓库的需求,这种场景下函数需要具备更加充分的信息。 1372 | 1373 | {title="src/App.js",lang="javascript"} 1374 | ~~~~~~~~ 1375 | class App extends Component { 1376 | ... 1377 | 1378 | # leanpub-start-insert 1379 | onStarRepository = (repositoryId, viewerHasStarred) => { 1380 | ... 1381 | }; 1382 | # leanpub-end-insert 1383 | 1384 | render() { 1385 | const { path, organization, errors } = this.state; 1386 | 1387 | return ( 1388 |
1389 | ... 1390 | 1391 | {organization ? ( 1392 | 1400 | ) : ( 1401 |

No information yet ...

1402 | )} 1403 |
1404 | ); 1405 | } 1406 | } 1407 | ~~~~~~~~ 1408 | 1409 | 现在便可以开始实现这个事件处理函数,整个变更操作的执行过程可以从组件外包出去作为独立函数,之后就可以通过 `viewerHasStarred` 这个布尔值判断是执行 `addStar` 还是 `removeStar` 变更操作。GraphQL 中变更操作的执行十分类似于之前使用过的查询操作,API 地址的配置已经在最初的章节通过 axios 完成,并不需要重复进行。变更的内容可以通过 `query` 属性传输,这点我们会在之后详述,虽然 `variables` 属性在 API 中是可选的,但当前需要用于传入仓库标识符。 1410 | 1411 | {title="src/App.js",lang="javascript"} 1412 | ~~~~~~~~ 1413 | # leanpub-start-insert 1414 | const addStarToRepository = repositoryId => { 1415 | return axiosGitHubGraphQL.post('', { 1416 | query: ADD_STAR, 1417 | variables: { repositoryId }, 1418 | }); 1419 | }; 1420 | # leanpub-end-insert 1421 | 1422 | class App extends Component { 1423 | ... 1424 | 1425 | onStarRepository = (repositoryId, viewerHasStarred) => { 1426 | # leanpub-start-insert 1427 | addStarToRepository(repositoryId); 1428 | # leanpub-end-insert 1429 | }; 1430 | 1431 | ... 1432 | } 1433 | ~~~~~~~~ 1434 | 1435 | 定义 `addStar` 变更操作之前,先再次检查 GitHub 的 GraphQL API,从中你可以找到它的所有结构信息以及必须参数,还有可用的结果字段。例如,你可以在返回结果中包含 `viewerHasStarred` 字段来取得是否 star 该仓库的更新后结果。 1436 | 1437 | {title="src/App.js",lang="javascript"} 1438 | ~~~~~~~~ 1439 | const ADD_STAR = ` 1440 | mutation ($repositoryId: ID!) { 1441 | addStar(input:{starrableId:$repositoryId}) { 1442 | starrable { 1443 | viewerHasStarred 1444 | } 1445 | } 1446 | } 1447 | `; 1448 | ~~~~~~~~ 1449 | 1450 | 现在已经能够通过在浏览器中点击该按钮来执行变更操作,如果之前你没有 star 过这个仓库,在点击按钮之后就会进行 star。虽然可以在 GitHub 网站上得到可视化的反馈,不过在应用中并不会产生任何反应,即便已经进行 star 之后该按钮仍然会显示 "Star" 标签,这是由于 App 组件中的 `viewerHasStarred` 状态在变更操作后并未得到更新。这也是你下一步的工作,鉴于 axios 返回了一个 Promise,你可以在 `then()` 方法的回调中添加相关实现细节。 1451 | 1452 | {title="src/App.js",lang="javascript"} 1453 | ~~~~~~~~ 1454 | # leanpub-start-insert 1455 | const resolveAddStarMutation = mutationResult => state => { 1456 | ... 1457 | }; 1458 | # leanpub-end-insert 1459 | 1460 | class App extends Component { 1461 | ... 1462 | 1463 | onStarRepository = (repositoryId, viewerHasStarred) => { 1464 | # leanpub-start-insert 1465 | addStarToRepository(repositoryId).then(mutationResult => 1466 | this.setState(resolveAddStarMutation(mutationResult)), 1467 | ); 1468 | # leanpub-end-insert 1469 | }; 1470 | 1471 | ... 1472 | } 1473 | ~~~~~~~~ 1474 | 1475 | 在变更操作的 Promise 结果中,你能看到一个 `viewerHasStarred` 属性,这是由于它被定义为变更操作中的一个字段。调用 `resolveAddStarMutation` 函数后产生了一个新的状态对象,用于 `this.setState()` 方法中。这里同样使用了扩展语法来更新深度嵌套结构,这个函数中除了 `viewerHasStarred` 属性之外的内容都不会发生变化,因为它是从响应中唯一能够得到的内容。 1476 | 1477 | {title="src/App.js",lang="javascript"} 1478 | ~~~~~~~~ 1479 | const resolveAddStarMutation = mutationResult => state => { 1480 | # leanpub-start-insert 1481 | const { 1482 | viewerHasStarred, 1483 | } = mutationResult.data.data.addStar.starrable; 1484 | 1485 | return { 1486 | ...state, 1487 | organization: { 1488 | ...state.organization, 1489 | repository: { 1490 | ...state.organization.repository, 1491 | viewerHasStarred, 1492 | }, 1493 | }, 1494 | }; 1495 | # leanpub-end-insert 1496 | }; 1497 | ~~~~~~~~ 1498 | 1499 | 现在可以重新尝试 star 这个仓库,虽然可能需要先到 GitHub 页面上取消 star。按钮中的文本将根据本地状态中 `viewerHasStarred` 更新后的值来决定显示 "Star" 或是 "Unstar" 标签,你可以根据目前习得的成果实现出 `removeStar` 变更操作。 1500 | 1501 | 我们想要更进一步显示出 star 了该仓库的当前人数,并在 `addStar` 或 `removeStar` 操作后更新,为此,首先需要在查询操作中添加 stargazers 字段来获取总人数: 1502 | 1503 | {title="src/App.js",lang="javascript"} 1504 | ~~~~~~~~ 1505 | const GET_ISSUES_OF_REPOSITORY = ` 1506 | query ( 1507 | $organization: String!, 1508 | $repository: String!, 1509 | $cursor: String 1510 | ) { 1511 | organization(login: $organization) { 1512 | name 1513 | url 1514 | repository(name: $repository) { 1515 | id 1516 | name 1517 | url 1518 | # leanpub-start-insert 1519 | stargazers { 1520 | totalCount 1521 | } 1522 | # leanpub-end-insert 1523 | viewerHasStarred 1524 | issues(first: 5, after: $cursor, states: [OPEN]) { 1525 | ... 1526 | } 1527 | } 1528 | } 1529 | } 1530 | `; 1531 | ~~~~~~~~ 1532 | 1533 | 随后,你可以将人数作为按钮标签的一部分: 1534 | 1535 | {title="src/App.js",lang="javascript"} 1536 | ~~~~~~~~ 1537 | const Repository = ({ 1538 | repository, 1539 | onFetchMoreIssues, 1540 | onStarRepository, 1541 | }) => ( 1542 |
1543 | ... 1544 | 1545 | 1556 | 1557 | 1560 |
1561 | ); 1562 | ~~~~~~~~ 1563 | 1564 | 现在我们希望这个数量随着 star(或取消 star)一个仓库而更新,而目前的问题就和在没有更新 `addStar` 变更操作成功后没有更新 `viewerHasStarred` 属性一样,为此在回调函数中一同更新本地的 stargazers 总数。虽然 stargazer 对象并没有作为变更结果返回,但仍然可以在 `addStar` 变更操作成功后人工增加或者减少计数。 1565 | 1566 | {title="src/App.js",lang="javascript"} 1567 | ~~~~~~~~ 1568 | const resolveAddStarMutation = mutationResult => state => { 1569 | const { 1570 | viewerHasStarred, 1571 | } = mutationResult.data.data.addStar.starrable; 1572 | 1573 | # leanpub-start-insert 1574 | const { totalCount } = state.organization.repository.stargazers; 1575 | # leanpub-end-insert 1576 | 1577 | return { 1578 | ...state, 1579 | organization: { 1580 | ...state.organization, 1581 | repository: { 1582 | ...state.organization.repository, 1583 | viewerHasStarred, 1584 | # leanpub-start-insert 1585 | stargazers: { 1586 | totalCount: totalCount + 1, 1587 | }, 1588 | # leanpub-end-insert 1589 | }, 1590 | }, 1591 | }; 1592 | }; 1593 | ~~~~~~~~ 1594 | 1595 | 你已经在 React 应用中实现了首个 GraphQL 的变更操作,虽然目前为止,只有 `addStar` 一个操作。尽管按钮仍然能够通过显示 "Star" 或 "Unstar" 标签来反映 `viewerHasStarred` 的值,但在按钮显示为 "Unstar" 时仍然执行的是 `addStar` 操作,而用来取消 star 某仓库的 `removeStar` 变更操作就作为下面的练习之一。 1596 | 1597 | ### 练习: 1598 | 1599 | * 确认[本节源代码](https://github.com/the-road-to-graphql/react-graphql-github-vanilla/tree/3dcd95e32ef24d9e716a1e8ac144b62c0f41ca3c) 1600 | * 实现 `removeStar` 变更操作,应当与 `addStar` 操作类似。 1601 | * 仍然通过 `onStarRepository` 类方法访问 `viewerHasStarred` 属性。 1602 | * 在类方法中根据条件执行 `addStar` 或 `removeStar` 变更操作。 1603 | * 取消 star 一个仓库后更新本地状态。 1604 | * 保持你的最终思路与[该实现](https://github.com/rwieruch/react-graphql-github-vanilla)一致。 1605 | * 为特定 issue 实现 `addReaction` 变更操作 1606 | * 将组件进一步细分(例如 IssueList、IssueItem、ReactionList 和 ReactionItem) 1607 | 1608 | ## React 中不借助 Apollo 使用 GraphQL 的缺点 1609 | 1610 | 我们基于 React 和 GraphQL 实现了一个简单的 GitHub issue 追踪器,并未使用任何专用的 GraphQL 封装库,而是仅仅通过 axios 发送 HTTP POST 请求实现与 GraphQL API 的通信。我认为掌握底层技术是十分重要的,也就是能够在不引入额外抽象的情况下,使用普通 HTTP 请求实现 GraphQL 交互。不过,Apollo 库的抽象能够极大简化在 React 中使用 GraphQL 的成本,因此在下一个应用将使用 Apollo 进行开发。引入 Apollo 之前,使用 HTTP 与 GraphQL 的过程体现出了两项重要内容: 1611 | 1612 | * GraphQL 如何结合 HTTP 这样简洁的接口工作。 1613 | * 缺乏成熟的 GraphQL 客户端工具情况下的缺点,因为你要将一切从头做起。 1614 | 1615 | 在继续下一章内容之前,我希望总结一下现有实现存在的缺点,即 React 应用中,直接使用 HTTP 方法来实现对 GraphQL API 读写数据的方式: 1616 | 1617 | * **互补性:**为了从客户端应用中通过 HTTP 请求调用 GraphQL API,有很多优质的库可供选择,其中之一就是 axios,这也是为什么在上述应用中使用它的原因。然而,使用 axios(或者任何其它 HTTP 客户端)并不能很好填补 GraphQL 接口的需求。例如,GraphQL 并不需要使用全部 HTTP 功能,仅需要自动应用 POST 方法和唯一 API 地址即可。由于不需要 RESTful 接口中那样的资源路径和方法定义,因此对每个请求重复输入 HTTP 方法和 API 地址是毫无意义的,而是应当作为一劳永逸的配置。GraphQL 有着自己的约束,可以将其视作 HTTP 的上层抽象,因此底层的 HTTP 实现对于开发人员并不是必要的。 1618 | 1619 | * **声明式:**每当需要使用 HTTP 请求实现一个查询或变更,你都需要使用 axios 之类的库对专门的 API 地址进行调用,这样是命令式地对后端读取或写入数据。然而,要是存在声明式的手段来构造查询和变更呢?要是可以在视图层组件的协同定义查询和变更呢?上述应用中,你亲身体验了组件的层次结构与查询的结构高度是如何得高度相似,要是查询和变更也同样能够达到此等一致呢?这就是协同定义数据层与视图层带来的强大力量,通过使用一个专用的 GraphQL 客户端库,你将发现更多的简化方式。 1620 | 1621 | * **功能支持:**使用普通 HTTP 请求与 GraphQL API 交互时,你将无法发掘 GraphQL 的全部可能性。设想你希望把上述应用中的查询拆分,并且协同定义于实际使用数据的相应组件中,这时 GraphQL 将在视图层中以声明式的方式出现。但是当你缺少工具支持,只能人工处理多个查询,追踪各个查询的关系并且在状态层中合并查询结果。以上述应用为例,拆分查询内容会极大提升应用复杂度,而一个 GraphQL 客户端工具可以高效地实现查询间的级联。 1622 | 1623 | * **数据处理:**使用基础 HTTP 请求时的原生数据处理可以认为是缺少 GraphQL 功能支持的子集,没有专用工具的情况下,没有其他人会帮你进行数据规范化或者缓存相同请求。如果没有在第一时间对数据进行规范化,那么整个数据层的更新都将变成噩梦,你将不得不面对深层的嵌套对象,导致大量不必要的使用 JavaScript 的展开语法。当你在 GitHub 仓库中查看该应用的实现,你会发现在变更或者查询后 React 本地状态的更新过程并不美观,可以使用像 [normalizr](https://github.com/paularmstrong/normalizr) 这样强大的规范化工具来帮助你改善本地状态结构,在 [Taming the State in React](https://roadtoreact.com) 中可以了解更多关于规范化状态的信息。除了缺乏缓存于规范化过程之外,缺少工具意味着缺乏像分页和乐观更新这样的功能,而一个专用的 GraphQL 工具能够保证所有这些功能可用。 1624 | 1625 | * **GraphQL 订阅:** GraphQL 中读写数据的概念除了查询和变更外,还存在第三个概念——**订阅**——用于在客户端应用中接收实时数据。当你依赖于基础 HTTP 请求进行读写时,如果需要实现订阅的等效功能,还需要依赖于 [WebSockets](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API) 来接收实时数据。后者引入了长链接机制来持续接收结果。总而言之,使用 GraphQL 订阅能够为你的应用再添加一个工具,通过在客户端使用 GraphQL 工具,订阅功能可能已经被工具所自动实现。 1626 | 1627 | 我十分期待能够为这个 React 应用引入 Apollo 作为 GraphQL 客户端工具库,它会帮助解决上述提到的所有缺点。不过,我仍然坚信在不使用工具库的情况下入门 GraphQL 是一个很好的学习方式。 1628 | 1629 | | | 1630 | 1631 | 你能在[这个 GitHub 仓库](https://github.com/rwieruch/react-graphql-github-vanilla)中找到应用的最终版本,它还包含了绝大多数练习的答案。鉴于仍有大量边界场景未覆盖,同时缺少样式,这个应用并不能算开发完成。不过,我希望在 React 中通过 HTTP 手动实现能够帮助你了解客户端的 GraphQL 概念,我认为在使用一个像 Apollo 和 Relay 这样成熟的 GraphQL 客户端工具之前这个步骤是十分重要的。 1632 | 1633 | 我已经演示了如何在 React 应用中通过普通的 HTTP 请求实现 GraphQL 交互,并未用到 Apollo 等工具库。之后,你将继续学习在 React 应用中通过 Apollo 来使用 GraphQL,而非基于 axios 的基础 HTTP 请求。借助 Apollo GraphQL 客户端能够零成本地实现数据缓存、数据规范化、乐观更新以及分页,甚至远不止这些,使用希望你能为后续 GraphQL 应用的开发做好准备。 1634 | --------------------------------------------------------------------------------