写点什么

用 GraphQL 增强 React

2017 年 8 月 02 日

要点

  • 把 GraphQL 和 React 放在一起就如同巧克力配花生酱,味道好极了。
  • GraphQL 可以帮你编写出强表达性的查询来从 API 精确拉取数据。
  • GraphQL 类型系统是非常强大的,可以为 API 进行验证并集成灵活的查询。
  • 有时你可能仍然需要 REST,没关系,它们是可以和平共处的。
  • GraphQL 可以在任何后端软件中实现,那么要如何将 GraphQL 集成到后端服务中呢?

在波士顿的一间办公室里,我、PacMan 女士还有坐在我对面的客户 Greg 正围着乒乓球桌喝啤酒。Greg 在相关业务上浸淫已久,是个令人钦佩的开发者。他直截了当地问我:“GraphQL 有没有为生产环境做好准备?

这么问也很合理,因为他从来没用过 GraphQL。而事实上,GraphQL 在 2015 年才开源,2016 年才真正作为标准实施。除了 Facebook,还有没有人真正在使用 GraphQL 呢?Greg 和他的团队都非常熟悉 REST,他们在过去几年里用 REST 构建过好几个应用。他们还使用 Swagger 来进行验证和文档生成,而且也用的很顺手。所以,他才会质疑 GraphQL 是否真的是最好的应用程序通信管道。

在回答 Greg 的问题之前,我们先回到 GraphQL 本身。

开门见山,GraphQL 是什么?

GraphQL 内涵丰富,它是一个流行词。酷小孩们用它,所以有人认为它只是昙花一现,就像今天的 techno babel、shiny、new hotness 等等酷酷的形容词。但是我保证绝非如此。

首先,GraphQL 不是什么?

在继续之前,先来消除一些关于 GraphQL 的常见误解。

  • 误解:客户端可以任何方式请求任何数据。例如某一客户端想要所有的用户和他们最爱的冰淇淋类型。只要服务器端的模式定义了这个关系是就能实现。

    真相:客户端将受限于 GraphQL 服务器端定义的数据关系。

  • 误解:GraaphQL 是否兼容 MS SQL?不兼容,它也不兼容 MongoDB、Oracle、MySQL、PostgreSQL、Redis、CouchDB 和 Elasticsearch。

    真相:GraphQL 并不直接对接数据库。它接收来自客户端的请求,然后由后端软件通过请求数据来查询数据存储,并返回与 GraphQL 模式格式相容的数据。

  • 误解:GraphQL 和 REST 你必须二选一。胡说。
    真相:可以轻松地在服务器端同时提供它们。

GraphQL 是一种强类型语言

说真的,GraphQL 是一种语言?那当然!先来看看下面这些简单的定义,这是一个缩略图的定义。

复制代码
type Thumbnail {
# 图片的 URL,! 表示必需
uri: String!
# 宽度(像素)
width: Int
# 高度(像素)
height: Int
# 作为图片 title 标签的字符串
title: String
}

如你所见,以上定义了一个名为Thumbnail的类型或对象。这个对象有几个属性,其中 url 是一个 string,width 和 height 是整数,title 也是一个 string。

这里有一个很棒的 GraphQL 语言参考手册

GraphQL 是关于关系的

GraphQL 的强大之处不只在于其定义的类型,还涉及这些类型是如何关联的。来看一个Person类型,我们可以将它关联到另一个类型——Address。这个关联由定义建立,现在客户端可以请求一个 person 并视情况来接收他们的地址列表。

复制代码
type Address {
street: String
city: String
state: String
zip: String
country: String
}
type Person {
# 名
fname: String!
# 姓
lname: String
# 年龄
age: Int
# 地址列表
addresses: [Address]
}

GraphQL 是一种查询语言

GraphQL 这部分符合大多数开发者的理解——一种查询语言,作为 REST 的一个替代品。那么是什么让它比 REST 更好?

可以这么认为,REST 是二维的,而 GraphQL 是三维的。

  • 在资源交互时 REST 严重依赖 URL,而 GraphQL 却可以方便地与多级资源进行交互。例如,一个 GraphQL 客户端可以通过 ID 请求一个 Person,并在将这个 Person 的 Friend 列表(一个 Person 数组)嵌套在响应中。在每个 Friend 中又可以请求他们的地址(一个 Address 数组)。下面是一个嵌套查询的例子。
复制代码
query {
person(id: 123) {
id
friends {
id
addresses: {
street
city
state
zip
}
}
}
}
  • REST 与 HTTP 状态代码高度耦合,如 200 和 404。而 GraphQL 不使用 HTTP 状态代码,而是在响应中使用一个错误数组。
  • 在为多级资源制定 ID 时,比如 post > comment > author > email,REST 中的 GET 会变得很笨重。而 GraphQL 可以轻易地利用类型定义和关系来处理。
  • GraphQL 自动验证输入数据。例如,如下定义了一个 input。如果客户端提交了一个 string 作为age,GraphQL 会抛出一个错误;如果fname为空,它也会抛出一个错误。
复制代码
input Person {
# 名
fname: String!
# 姓
lname: String
# 年龄
age: Int
}

在 API 返回数据时,也会进行验证和格式化来匹配定义的模式。当你从数据库查询一个 person 记录,而它却意外将 password 字段也发送给客户端时,这就很容易处理。

由于 GraphQL 的定义中没有 password,它会默默地把 password 从响应中删掉。

GraphQL 是可扩展的

假设你开始编写自己的 GraphQL 模式,并且打算改变日期处理的方式。比如你可能更喜欢以时间戳的形式返回给客户端,而不是 ISO 字符串。你可以定义一个自己的Scalar类型交给 GraphQL,然后只需要定义这个 Scslar 如何解析和序列化数据。下面是一个自定义的Data scalar,它返回整数形式的日期。你会注意到有一个处理来自客户端数据的parseValue函数,还有一个在发送给客户端之前处理数据的serialize函数。

复制代码
const { GraphQLScalarType } = require('graphql')
const { Kind } = require('graphql/language')
exports.Date = new GraphQLScalarType({
name: 'Date',
description: 'Date custom scalar type',
parseValue (value) {
return new Date(value) // 来自客户端的值
},
serialize (value) {
if (typeof value === 'object') {
return value.getTime() // 发送给客户端的值
}
return value
},
parseLiteral (ast) {
if (ast.kind === Kind.INT) {
return parseInt(ast.value, 10) // ast value is always in string format
}
return null
}
})

当然,GraphQL 在 2015 年才被 Facebook 开源,2016 年才开始成为标准。它很年轻,但也有优势。

  • 孵化历史长:Facebook 是才 2015 年才将它开源,但是其实在 2012 年就已经开发出来,而且在公布之前已经在内部广泛使用。要知道世界上最大的科技公司之一已经把它放在了应用的核心位置。
  • 工具:后面会看到,围绕 GraphQL 的工具发展迅猛,GraphQL 已经拥有了许多成熟的工具和库。详见 GraphQL 资源库
  • 标准化:许多公司都会发布开源软件,但是 GraphQL 已经更进一步成为一项标准(草案阶段)。可以深入阅读下标准
    成为标准更可能会被其他公司或者整个行业所采纳。

我确信 GraphQL 已经为生产环境做好了准备。那么下面做什么呢?

现在我们已经对 GraphQL 的构成有了一点认识,对于它为重度生产环境中使用所做的准备也有了更好的了解。下面让我们来构建一个 React/Node.js 应用来实际运用 GraphQL。

不需要特殊武器

先说清楚,在客户端你完全不需要特别的库来发送 GraphQL 请求。GraphQL 不过就是将一个特定的 JSON 对象 POST 到终端并接受返回的 JSON。如下 GraphQL 会 POST 到终端的示例。

复制代码
{
"query": "query tag($id:ID) { tag(id:$id) { id title }}",
"variables": {
"id": "6d726d65-fb99-4fa7-9463-b79bad7f16a7"
}
}

可以看到两条属性。

  • query:一个表示 GraphQL 查询的字符串。
  • variables:一个 GraphQL 所需变量的 JSON 对象。注意在查询字符串中它们要前缀一个 $(如 $id)。

就这样,它将按照 query 中的请求来生成响应。

休斯顿,这里是阿波罗,这里没有问题。

介绍下我最喜欢的 GraphQL 工具套件——Apollo

Apollo 的开发者们创建了一套神奇的工具,可以用它构建 React 前端和 Node.js 后端。事实上,他们不只提供 React 和 Node.js 的 GraphQL 工具,Angular、Vanilla JS、Swift(iOS)和 Android 也都有。

今天我们会在用到的几个 Apollo 工具:

  • react-apollo:为 React 应用集成 GraphQL 的工具
  • graphql-server-express:一个为 GraphQL 服务器处理请求和响应的 Node.js/ExpressJS 中间件
  • graphql-tools:用来将 GraphQL 模式语言转换为 ExpressJS 服务器可理解的函数的工具库

App 发射倒计时

不浪费时间,让我们构建一个简单的应用来演示 React、Node.js 和 GraphQL。现在,创建一个简单的通讯录应用,它可以添加联系人并列出所有联系人。

首先

在开动之前要先行规划,我们通过创建 GraphQL 模式来实现。这将定义 GraphQL 服务器接受请求和返回响应的形式。下面是 Person 的 GraphQL 模式。

复制代码
type Person {
# person 的内部,必需
id: ID!
# 名,必需
firstName: String!
# 姓,必需
lastName: String!
# 年龄
age: Int
# person 的电话号码
phone: String
# 电话号码是否手机号
isMobile: Boolean
# person 的好友
bestFriend: Person
}

这是为 person 设定的一个简单模式,我们可以用它来记录一个朋友的联系信息。现在 GraphQL 模式还需要定义另外两项:用来创建或更新 person 的 input 和客户端调用的操作。先来看下 Person 的 input。

复制代码
input PersonInput {
# person 的内部 ID
id: ID
# 名,必需
firstName: String!
# 姓,必需
lastName: String!
# 年龄
age: Int
# person 的电话号码
phone: String
# 电话号码是否手机号
isMobile: Boolean
# person 的好友的 ID
bestFriend: ID
}

嘿,到底发生了什么?
看起来只是把 Person 类型用在了input中,没错。GraphQL 将输入和输出做了一些区别对待。例如id,对于类型和输出就需要它,而输入则不需要。思考下,当创建一个新 person 时,并不知道数据库会指派给它哪个 ID。如果给出了一个id,那就应该知道这不是新建,而是更新。另外,bestFriend只是输入的一个 ID,但是类型和响应的却是一个完整的 Person 类型。

最后要在模式中定义的是客户端调用的实际方法和操作,用来创建、更新和列出联系人。

复制代码
type Query {
# 通过 id 获取单独的 Person
person (id: ID): Person
# 获取所有的 Person
people: [Person!]!
}
type Mutation {
# 创建或更新一个 Person
person (input: PersonInput): Person
}
schema {
query: Query
mutation: Mutation
}

从定义中可以看到有两个查询操作和一个变更操作。两个查询,person 和 people 分别用来获取单独的 person 和一个 person 数组。变更操作则用来创建或更新一个 person。

现在将这三个模式定义保存在一个名为“schema.gql”的文件中。随后将在设置服务时导入它。

稳固的平台

现在已经定义了我们的模式,到了设置 Node.js/Express 服务的时候。之前提过 Apollo 提供一个实用的中间件来配合 Express,不过那只是最简单的部分。在设置应用之前,需要先来讨论 Apollo GraphQL 的一个重要概念。

解析器

什么是解析器?还记得之前提过 GraphQL 并不知道如何跟数据库对话吧?确实如此。每个查询、变更和类型都需要知道如何将 GraphQL 请求解析成为一个可接受的响应。为此,Apollo 需要创建一个了解如何返回数据请求的对象。

来看看我们模式的解析器是什么样的。

简单起见,把数据保存在内存中的一个‘people’数组中。不过对于实际的应用,你需要用某种类型的数据存储。

复制代码
// 将就一下,用内存里的数组作为数据库
const people = [ ];
const resolvers = {
Query: {
// 获取一个 person
person (_, { id }) {
return people[id];
},
// 获取所有的 person
people () {
return people;
}
},
Mutation: {
person (_, { input }) {
// 如果该 person 已存在则进行更新
if (input.id in people) {
people[input.id] = input;
return input;
}
// 默认添加(或创建)该 person
// 将 id 设为记录的索引
input.id = people.length
people.push(input)
return input
},
},
Person: {
// 将好友 Id 解析成一条 person 记录
bestFriend (person) {
return people[person.bestFriend];
}
}
};
module.exports = resolvers;

看起来很熟悉吧。其实解析器就是一个 JavaScript 对象,它的关键字与我们的模式相匹配。由于只用了一个简单的 JavaScript 数据作为数据存储,我们就用索引来作为 person 的 id。

Apollo 将定义的解析器与我们的模式相匹配。现在它就知道如何处理每个类型的请求了。虽然只涉及皮毛,也足够你了解查询、变更、解析器和类型的工作方式。

请注意Person的解析器。默认情况下,Apollo 只会原样返回对象的属性,但有时需要做一些改变。来看bestFriend解析器,由于它要返回一个 Person 类型,我们使用bestFriend的 id 在 people 数组中查找并返回整个 person。

记住,如果客户端只请求了bestFriend属性,那么 Apollo 将只触发bestFriend函数。

集合时间

现在已经在 schema.gql 中定义了模式,并在 resolvers.js 中定义了解析器,然后就需要把一切都集合在一起启动 GraphQL。这里定义了一个简单的 Express 应用,可以放在程序的 index.js 中。

复制代码
const fs = require('fs');
const path = require('path');
const express = require('express');
const bodyParser = require('body-parser');
const { graphqlExpress, graphiqlExpress } = require('graphql-server-express');
const { makeExecutableSchema } = require('graphql-tools');
const typeDefs = fs.readFileSync(path.join(__dirname, './schema.gql'), 'utf8')
const resolvers = require('./resolvers');
const myGraphQLSchema = makeExecutableSchema({
typeDefs,
resolvers
});
var app = express();
// POST 需要用到 bodyParser
app.use('/graphql',
bodyParser.json(),
graphqlExpress({ schema: myGraphQLSchema })
);
app.use('/graphiql',
graphiqlExpress({ endpointUrl: '/graphql'})
);
app.listen(3000);

表面上看起来挺复杂,其实只是整合了模式定义(typeDefs),用makeExecutableSchema让它与解析器相配,最后将 GraphQL 添加到 URL 路径 /graphql。可以用以下命令启动服务 ;

node index.js

Espress 服务将被启动并在 http://localhost:3000/graphql 监听 GraphQL POST。

另外还导入了GraphiQL,可以在 http://localhost:3000/graphiql 查看 GraphiQL 浏览器和文档。

现在 API 服务已经运行起来了,你可以点击链接 http://localhost:3000/graphiql..... 进行变更操作添加一个 person。

很酷吧?

继续再试试其他操作,后端运行起来的效果很爽不是么?我们再来看一些简单的 React 组件以及它们如何与 GraphQL 后端通信。

前端控制中心

现在我们通过展示一些 React 组件来运用之前定义的联系人 API。使用 Webpack、React Router、Redux 和其他元素组建完整的前端超出了本文的范围,所以只展示 Apollo 将如何融入。

首先,先看一些顶层代码,需要用到组件中 Apollo 的 React 库。这个 npm 模块叫做react-apollo

复制代码
import { ApolloClient, ApolloProvider } from 'react-apollo';
// 创建一个上面提到的客户端
const client = new ApolloClient();
ReactDOM.render(
<ApolloProvider client={client}>
<MyAppComponent />
</ApolloProvider>,
document.getElementById('root')
)

这是一个简单示例,它用 ApolloProvider 高阶组件包装了 APP,可以在客户端和 GraphQL 服务器之间建立所需的通信。

我们来看它将如何展示 ID 为 10 的 Person。下面的例子将在组件装配后自动触发 GraphQL 查询。查询按照const query = gql….; 模板来定义。查询和PersonView组件通过使用这里看到的graphql库来进行整合。

这是 Person 组件的一个高阶组件。就是说 Apollo 将于 GraphQL 服务器保持联系,当它接到一个应答时,Apollo 会将这些属性作为props.data.person注入到你的组件。

复制代码
import React from 'react'
import { gql, graphql } from 'react-apollo'
function Person ({ data: { person = {} } }) {
return (
<PersonView data={person} />
);
}
const query = gql`
query person($id: ID) {
person(id: $id) {
id
firstName
lastName
age
phone
isMobile
bestFriend {
id
firstName
}
}
}
`;
export default graphql(query, {
options: () => ({
variables: {
id: 10 // 你可能会使用 URL 参数而非硬编码
}
})
})(Person);

接下来看看变更,它不太一样。事实上,查询和变更可以依赖同样的 React 组件,所以我们对之前的例子做些扩展来让它可以更新 person。

复制代码
import React from 'react'
import { gql, graphql, compose } from 'react-apollo'
function Person ({ submit, data: { person = {} } }) {
return (
<PersonView data={person} submit={submit} />
);
}
const query = gql`
… omitted …
`;
const update = gql`
mutation person($input: PersonInput) {
person(input: $input) {
id
firstName
lastName
age
phone
isMobile
bestFriend {
id
firstName
}
}
}
`;
export default compose(
graphql(query, {
options: () => ({
variables: {
id: 10 // 你可能会使用 URL 参数而非硬编码
}
})
}),
graphql(update, {
props: ({ mutate }) => ({
submit: (input) = mutate({ variables: { input } })
})
})
)(Person);

仔细观察这段代码,我们推出了compose工具,可以用它在一个单独的组件中组合多种 GraphQL 操作。

我们还定义了一个update查询来使用在模式中定义的 person 更新。在代码的尾部可以看到创建了一个名为submit的包装函数。它作为一个属性传递到 Person 组件中,并从这里传递给 PersonView 组件。

PersonView 组件可以像下面的例子中这样简单调用submit函数来触发一个 person 更新。

复制代码
props.submit({
firstName: “Neil”,
lastName: “Armstrong”,
isMobile: true
})

还想再来点激动人心的?

当触发 Person 类型更新时,Apollo 会自动更新你的本地缓存。所以应用中任何用到 Person 记录的地方,都将被自动更新。

最后,来看看在一个表格中展示所有 people 的代码。在下面的例子中,用一个简单的 HTML 表格展示 perple 清单。特别要注意loading属性,这是 Apollo 在获取数据时设置的一个属性,你可以设置一个下拉列表组件或者其他 UI 来提示访问者。

还像之前那样定义 React 组件。然后query使用gql工具将模板文字转换为一个有效的 GraphQL 请求。最终,用graphql工具将他们绑在一起。现在这个组件装配后,自动触发查询并加载后端存储的 people。

复制代码
import React from 'react'
import { gql, graphql } from 'react-apollo'
function People ({ data: { loading, people = [] } }) {
// 当还在从 GraphQL 获取数据时,Apollo 将设置 loading = true
if (loading) return <Spinner />
return (
<table className='table table-hover table-striped'>
<tbody>
{people.map((person, i) =>
<tr key={i}>
<td>{person.firstName}</td>
<td>{person.lastName}</td>
<td>{person.age}</td>
<td>{person.phone}</td>
<td>{person.isMobile}</td>
<td>{person.bestFriend && person.bestFriend.firstName}</td>
</tr>
)}
</tbody>
</table>
);
}
const query = gql`
query people {
people {
id
firstName
lastName
age
phone
isMobile
bestFriend {
id
firstName
}
}
}
`;
export default graphql(query)(People);

总结

如你所见,GraphQL 是一套强大的工具,你可以将它整合到 React 应用中来增强 API 交互。而使用 Apollo 可以更容易地将 GraphQL 添加到 React 前端和 Node.js 后端。现在正是测试在 GraphQL 中发现的新技能的好时机。你可以运用这门技术编写一个小应用,或者悄悄地将 GraphQL 包含到已有的 API 服务中。无论选择如何在应用中运用 GraphQL,你都将获得很多乐趣。

关于作者

Shane Stillwell,作家、演讲者、住在寒冷的明尼苏达州的德卢斯北部。过去十五年里,不铲雪的时候,他会为 Under Armour、BrightCove、Meijer 和其他顶级组织提供咨询服务。Shane 专注于使用 React、 Node.js 和 Docker 的定制 web 应用开发领域。他努力磨砺自己的技能并乐于分享,是一个享受户外的一切的居家男人,你可以在很多地方通过 @shanestillwell 找到他。

查看英文原文: Turbocharge React with GraphQL


感谢冬雨对本文的审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ @丁晓昀),微信(微信号: InfoQChina )关注我们。

2017 年 8 月 02 日 17:514165

评论

发布
暂无评论
发现更多内容

还有比二分查找更快的算法,面向接口编程Protocol,John 易筋 ARTS 打卡 Week 05

John(易筋)

swift ARTS 打卡计划 二分查找 binary search protocol

架构师训练营 - 第 02 周学习总结

Eric

三周作业

飞雪

ARTS-WEEK3

Allen

故障演练利器之ChaosBlade介绍

心平气和

故障演练 故障注入

架构师训练营 - 第三周作业

teslə

每周学习总结 - 架构师培训 3 期

Damon

第三周学习总结

iHai

极客大学架构师训练营

springboot整合Quartz实现定时任务(api使用篇)

北漂码农有话说

ARTS-week-4

youngitachi

ARTS 打卡计划 arts

架构师训练营 - 第三周总结

teslə

组合设计模式

Karl

week3.课后作业

个人练习生niki

单例模式 组合模式

Prometheus 2.19.0 新特性

耳东

Prometheus

week3 学习总结

任小龙

Go:使用Delve和Core Dump来调试

陈思敏捷

go golang debug gdb

极客时间架构师训练营 - week3 - 作业 2

jjn0703

极客大学架构师训练营

极客时间 - 架构师培训 -3 期作业

Damon

第三周课后作业

iHai

极客大学架构师训练营

代码重构-学习总结

飞雪

架构师训练营第三周作业

CATTY

架构师训练营 - 学习笔记 - 第三周

心在飞

极客大学架构师训练营

架构师训练营 - 第三周命题作业

牛牛

极客大学架构师训练营 命题作业

第三周总结

Karl

week3.学习总结

个人练习生niki

游戏夜读 | 《FPS关卡设计》

game1night

架构师训练营 -week3- 作业

晓-Michelle

极客大学架构师训练营

ARTS-WEEK4

一周思进

ARTS 打卡计划

程序员的晚餐 | 6 月 21 日 自制小火锅

清远

美食

Open-Falcon安装注意事项

wong

Open-Falcon Nightingale Monitor

手写单例

Karl

演讲经验交流会|ArchSummit 上海站

演讲经验交流会|ArchSummit 上海站

用GraphQL增强React-InfoQ