要点
1.把GraphQL和React放在一起就如同巧克力配花生酱,味道好极了。
2.GraphQL可以帮你编写出强表达性的查询来从API精确拉取数据。
3.GraphQL类型系统是非常强大的,可以为API进行验证并集成灵活的查询。
4.有时你可能仍然需要REST,没关系,它们是可以和平共处的。
5.GraphQL可以在任何后端软件中实现,那么要如何将GraphQL集成到后端服务中呢?
在波士顿的一间办公室里,我、PacMan女士还有坐在我对面的客户Greg正围着乒乓球桌喝啤酒。Greg在相关业务上浸淫已久,是个令人钦佩的开发者。他直截了当地问我:“GraphQL有没有为生产环境做好准备?”
这么问也很合理,因为他从来没用过GraphQL。而事实上,GraphQL在2015年才开源,2016年才真正作为标准实施。除了Facebook,还有没有人真正在使用GraphQL呢?Greg和他的团队都非常熟悉REST,他们在过去几年里用REST构建过好几个应用。他们还使用Swagger来进行验证和文档生成,而且也用的很顺手。所以,他才会质疑GraphQL是否真的是最好的应用程序通信管道。
开门见山,GraphQL是什么?
GraphQL内涵丰富,它是一个流行词。酷小孩们用它,所以有人认为它只是昙花一现,就像今天的techno
babel、shiny、new hotness等等酷酷的形容词。但是我保证绝非如此。
首先,GraphQL不是什么?
在继续之前,先来消除一些关于GraphQL的常见误解。
1.误解:客户端可以任何方式请求任何数据。例如某一客户端想要所有的用户和他们最爱的冰淇淋类型。只要服务器端的模式定义了这个关系是就能实现。
真相:客户端将受限于GraphQL服务器端定义的数据关系。
2.误解:GraaphQL是否兼容MS SQL?不兼容,它也不兼容MongoDB、Oracle、MySQL、PostgreSQL、Redis、CouchDB和Elasticsearch。
真相:GraphQL并不直接对接数据库。它接收来自客户端的请求,然后由后端软件通过请求数据来查询数据存储,并返回与GraphQL模式格式相容的数据。
3.误解: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年才开始成为标准。它很年轻,但也有优势。
1.孵化历史长:Facebook是才2015年才将它开源,但是其实在2012年就已经开发出来,而且在公布之前已经在内部广泛使用。要知道世界上最大的科技公司之一已经把它放在了应用的核心位置。
2.工具:后面会看到,围绕GraphQL的工具发展迅猛,GraphQL已经拥有了许多成熟的工具和库。详见
GraphQL资源库。
3.标准化:许多公司都会发布开源软件,但是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"
}
} |
可以看到两条属性。
1.query:一个表示GraphQL查询的字符串。
2.variables:一个GraphQL所需变量的JSON对象。注意在查询字符串中它们要前缀一个$(如$id)。
就这样,它将按照query中的请求来生成响应。
休斯顿,这里是阿波罗,这里没有问题。
介绍下我最喜欢的GraphQL工具套件——Apollo。
Apollo的开发者们创建了一套神奇的工具,可以用它构建React前端和Node.js后端。事实上,他们不只提供React和Node.js的GraphQL工具,Angular、Vanilla
JS、Swift(iOS)和Android也都有。
今天我们会在用到的几个Apollo工具:
1.react-apollo:为React应用集成GraphQL的工具
2.graphql-server-express:一个为GraphQL服务器处理请求和响应的Node.js/ExpressJS中间件
3.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,你都将获得很多乐趣。
|