BT

你的观点很重要! 快来参与InfoQ调研吧!

用GraphQL增强React

| 作者 Shane Stillwell 关注 0 他的粉丝 ,译者 李梦 关注 0 他的粉丝 发布于 2017年8月3日. 估计阅读时间: 37 分钟 | ArchSummit社交架构图谱:Facebook、Snapchat、Tumblr等背后的核心技术

要点

  • 把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)关注我们。

评价本文

专业度
风格

您好,朋友!

您需要 注册一个InfoQ账号 或者 才能进行评论。在您完成注册后还需要进行一些设置。

获得来自InfoQ的更多体验。

告诉我们您的想法

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我
社区评论

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

讨论

登陆InfoQ,与你最关心的话题互动。


找回密码....

Follow

关注你最喜爱的话题和作者

快速浏览网站内你所感兴趣话题的精选内容。

Like

内容自由定制

选择想要阅读的主题和喜爱的作者定制自己的新闻源。

Notifications

获取更新

设置通知机制以获取内容更新对您而言是否重要

BT