您可以捐助,支持我们的公益事业。

1元 10元 50元





认证码:  验证码,看不清楚?请点击刷新验证码 必填



  求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Model Center   Code  
会员   
   
 
     
   
 订阅
  捐助
用GraphQL增强React
 
来源: InfoQ 发布于2017-8-17
  2725  次浏览      19
 

要点

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,你都将获得很多乐趣。

   
2725 次浏览       19
相关文章

深度解析:清理烂代码
如何编写出拥抱变化的代码
重构-使代码更简洁优美
团队项目开发"编码规范"系列文章
相关文档

重构-改善既有代码的设计
软件重构v2
代码整洁之道
高质量编程规范
相关课程

基于HTML5客户端、Web端的应用开发
HTML 5+CSS 开发
嵌入式C高质量编程
C++高级编程