显示导航

用 React 和 Apollo 构建一个 GORM/GraphQL 应用程序

构建一个 Grails 应用程序,并使用 GORM 的 GraphQL 支持来服务使用 Apollo 的 React 应用程序

作者:Zachary Klein

Grails 版本 4.0.0

1 培训

Grails 培训 - 由创建和积极维护 Grails 框架的工作人员开发和提供!

2 入门

在本指南中,你将学习如何利用 GORM 对 GraphQL 的支持。你将配置 Grails 充当 GraphQL 服务器,

initial 示例项目已经使用 @Resource 注释 配置了一个简单的与会议相关的 REST API(包含两个域类,SpeakerTalk)。该项目使用 react 配置文件,并且提供了一个简单的 React 应用程序,该应用程序将列出演讲者以及属于每个演讲者的演讲。

该 React 应用程序非常简单,并且朴素地使用了 Grails 提供的默认 REST API - 例如,该应用程序进行一个 REST 调用来列出演讲者,然后遍历每个演讲者的演讲 id,进行单独的 REST 调用来获取每个演讲的详细信息。这意味着仅为了获取呈现应用程序所需的数据,加载一次应用程序就需要许多 REST 调用。

http://localhost:8080/speaker	200	fetch	1.6 KB	851 ms
http://localhost:8080/talk/1	200	fetch	260 B	262 ms
http://localhost:8080/talk/2	200	fetch	273 B	310 ms
http://localhost:8080/talk/3	200	fetch	254 B	299 ms
http://localhost:8080/talk/4	200	fetch	261 B	297 ms
http://localhost:8080/talk/5	200	fetch	293 B	284 ms

当然,我们可以使用标准 REST 技术(包括超媒体或自定义端点(例如使用JSON 视图))解决这个问题,但对于更复杂的应用程序,这可能会导致提供不同数据组合的自定义端点激增。此外,一些端点可能提供比应用程序实际需要更多的数据——在典型的 REST API 中,无法表示所需数据的“部分”。

这是 GraphQL 的优势所在——它允许我们 API 的使用者(无论是私有客户端(例如我们自己的单页应用程序)还是普通用户)以声明性方式指定他们需要的精确数据部分。GraphQL 查询甚至可以编成 Flow 类型,使组件的数据需求明确且可测试。

GraphQL

请访问官方 GraphQL 项目网站,参阅优秀的文档和其他资源,了解有关详情。

2.1 需要准备什么

要完成此指南,您需要以下内容

  • 一些空闲时间

  • 不错的文本编辑器或 IDE

  • 已安装且正确配置了 JAVA_HOME 的 JDK 8 或更高版本

2.2 如何完成此指南

按照以下步骤开始操作

或者

Grails 指南代码库包含两个文件夹

  • initial 初始项目。通常是简单的 Grails 应用程序,带有部分附加代码,帮助您抢先开始。

  • complete 完成的示例。它是由按指南执行步骤并在 initial 文件夹中应用这些更改生成的。

要完成此指南,请转到 initial 文件夹

  • cdgrails-guides/gorm-graphql-with-react-and-apollo/initial

并按照下一部分中的说明操作。

如果您 cdgrails-guides/gorm-graphql-with-react-and-apollo/complete,则可以直接转到完成的示例

3 为 GraphQL 配置 GORM

通过添加以下行到 build.gradle 中的 dependencies 部分,安装GORM GraphQL 插件

build.gradle
compile "org.grails:gorm-graphql:1.0.2"
compile "org.grails.plugins:gorm-graphql:1.0.2"

现在,要为您的领域类生成 GraphQL 模式,所有需要做的就是在相关领域类中添加 static graphql 属性。

grails-app/domain/com/objectcomputing/Talk.groovy
package com.objectcomputing

import grails.rest.Resource

@Resource(uri='/talk')
class Talk {

    String title
    int duration

    static graphql = true (1)

    static belongsTo = [speaker: Speaker]
}
1 这会根据领域类中的属性生成 GraphQL 模式,该模式遵循 constraints 块中指定约束。

如果您需要自定义 GraphQL 架构的行为,您可以使用由该插件提供的 DSL,将 graphql 属性分配给自定义 GraphQLMapping。这并不是本指南的先决条件,但在 Speaker 域类中显示了几个受支持的选项。

grails-app/domain/com/objectcomputing/Speaker.groovy
package com.objectcomputing

import grails.rest.Resource
import org.grails.gorm.graphql.entity.dsl.GraphQLMapping
import java.time.LocalDate
import java.time.Period

@Resource(uri='/speaker')
class Speaker {

    String firstName
    String lastName
    String name
    String email
    String bio
    LocalDate birthday

    static hasMany = [talks: Talk]

    static graphql = GraphQLMapping.build {

        property 'lastName', order: 1 (1)
        property 'firstName', order: 2
        property 'email', order: 3

        exclude 'birthday' (2)

        property 'name', deprecationReason: 'To be removed August 1st, 2020' (3)

        property('bio') { (4)
            order 4
            dataFetcher { Speaker speaker ->
                speaker.bio ?: "No biography provided"
            }
        }

        add('age', Integer) { (5)
            dataFetcher { Speaker speaker ->
                Period.between(speaker.birthday, LocalDate.now()).years
            }
            input false
        }
    }

    static constraints = {
        email nullable: true, email: true
        birthday nullable: true
        bio nullable: true
    }

    static mapping = {
        bio type: 'text'
        name formula: 'concat(FIRST_NAME,\' \',LAST_NAME)'
        talks sort: 'id'
    }

}
1 设置属性顺序
2 从该架构中排除属性
3 弃用属性(也可以设置为 deprecated: true)
4 自定义数据访问器,用于在检索属性时提供自定义逻辑
5 将(非持久性)属性添加到 GraphQL 架构
您可以通过 插件文档,了解 GORM GraphQL 插件的所有可用功能。

4 玩转 GraphQL 浏览器

GORM GraphQL 插件提供了一个交互式 GraphQL 浏览器 (GraphiQL),使用户可以轻松浏览 GraphQL 架构。该浏览器在启动 Grails 应用时可用,其 URL 为 /graphql/browser。浏览器功能包括基于您的架构的自动完成,这使得轻松浏览可用查询类型和突变(GraphQL 术语,用来表示创建/更新/从架构中删除数据)变得更加简单。

使用 grails run-app./gradlew bootRun 启动该应用,并浏览到 http://localhost:8080/graphql/browser

现在您可以对新的 GraphQL 架构以及突变运行查询。以下提供了一些查询和突变以便您上手 - 请参阅 GORM GraphQL 插件文档和 GraphQL 项目网站,了解有关 GraphQL 语法和功能的更多信息。

查询

//List speaker ids
query {
  speakerList(max: 10) {
    id
  }
}
//Return total speakers
query {
	speakerCount
}
//Retrieve speaker details by id
query {
  speaker(id: 1) {
    firstName
    lastName
    bio
  }
}
//Return list of talks with speaker details embedded
query {
	talkList(max:10) {
    title
    speaker {
      firstName
      lastName
    }
  }
}

突变

//Update speaker by id, return any error messages (if any)
mutation {
  speakerUpdate(id: 1, speaker: {
    bio: "Updated bio!"
  }) {
    id
    bio
    errors {
      field
      message
    }
  }
}'
//Create speaker - this mutation will return an error due to missing property
mutation {
  speakerCreate(speaker: {
    firstName: "James"
    lastName: "Kleeh"
  }) {
    id
    firstName
    lastName
    errors {
      field
      message
    }
  }
}
//Delete speaker by id
mutation {
  speakerDelete(id: 2) {
    error
  }
}

5 安装和配置 Apollo

现在您拥有了 GraphQL 架构,该使用 GraphQL 替换 React 应用中现有的 RESTful 集成了。当然,我们可以手动编写 GraphQL 查询,但在大多数情况下,使用一个库抽象出这些请求的制作细节会更好。其中一个选择是 Relay 框架,该框架由 Facebook (React 的创建者)开发。另一个流行的库是 Apollo,它为许多 JavaScript 框架(包括 Angular 和 React)提供了绑定。本指南将在后面的步骤中使用 Apollo。

使用 npmyarn 安装必要的软件包

~ npm install apollo-client-preset react-apollo graphql-tag graphql --save

使用 Apollo 的第一个先决条件是配置一个提供程序,它将封装实际对 GraphQL 架构进行请求所需的所有详细信息。如下所示编辑文件 src/main/webapp/index.js

client/src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import './css/bootstrap.css';
import './css/App.css';
import './css/grails.css';
import './css/main.css';
import {ApolloProvider} from 'react-apollo';
import {ApolloClient} from 'apollo-client';
import {createHttpLink} from 'apollo-link-http';
import {InMemoryCache} from 'apollo-cache-inmemory';
import { SERVER_URL} from "./config";

(1)
const client = new ApolloClient({
  link: createHttpLink({ uri: `${SERVER_URL}/graphql` }), (2)
  cache: new InMemoryCache()
});

(3)
ReactDOM.render(< ApolloProvider client={client}>
  <App />
</ApolloProvider>, document.getElementById('root'));
1 我们创建 ApolloClient 的一个实例,它需要两个对象 - 一个 HttpLink(它展示 GraphQL 架构的 URL)和一个缓存存储器。
2 此客户端将向 ${SERVER_URL}/graphql 端点发送查询,凑巧的是,GORM GraphQL 插件也会在该端点为 GraphQL 架构公开
3 最后,我们将 App 组件和 ApolloProvider 组件一起封包,将 ApolloClient 实例作为道具传递给后者。现在,所有子组件都能够查询/更改 client 指向的 GraphQL 架构。

6 查询 GraphQL

我们的 React 应用非常简单,由 3 个主要组件(SpeakerListSpeakerTalk)组成。前两个组件调用 REST 载入数据。我们用一个 GraphQL 查询替换这些调用。

编辑 src/main/webapp/SpeakerList.js

/src/main/webapp/SpeakerList.js
import React, {Component} from 'react'
import Speaker from "./Speaker";
import {graphql} from 'react-apollo'; (1)
import gql from 'graphql-tag';  (2)

class SpeakerList extends Component {

(3)
  render() {
    const speakers = this.props.data.speakerList; (4)
    return speakers ? [speakers.map(s => <Speaker key={s.id} speaker={s}/>)] : <p>Loading...</p>
  }
}

const SPEAKER_QUERY = gql`query {        speakerList(max: 10) { (5)
  id, firstName, lastName,
    talks { id }
  }
}`;

(6)
const SpeakerListWithData = graphql(SPEAKER_QUERY,
    {options: {pollInterval: 5000}} (7)
)(SpeakerList);
export default SpeakerListWithData; (8)
1 graphql高阶组件,这意味着它将一个现有的组件封包并返回一个具有其他行为的新组件。
2 gql 是 JavaScript 模板文字标签,它将解析我们的 GraphQL 查询
3 请注意,我们可以完全删除我们的 componentDidMount 方法(REST 调用已被执行)和我们的构造函数,因为我们不再需要使用 this.state
4 我们的 GraphQL 查询返回的数据将通过 this.props.data.[query 的名称] 提供,在本例中是 speakerList
5 此常量表示我们的查询,请注意,语法与您可能在 GraphiQL 浏览器中使用的一样。
6 我们不会直接导出 SpeakerList,而是先使用 graphql 高阶组件对其进行“封包”,它将我们的 SpeakerList 组件与我们的 GraphQL 架构“连接”起来,传递我们的数据要求,并通过 SpeakerList 中的 data 道具提供数据。
7 我们指定了一个 pollInterval,这是 graphql 可用的几个选项之一。此选项将使我们的 GraphQL 查询每 10 秒运行一次,并使用任何新数据自动更新我们的组件。
8 最后,我们将“已连接”的 SpeakerList 版本导出为默认模块。

此时,如果您重新启动应用程序(或在另一个终端中运行 ./gradlew webpack 以重新载入 JavaScript),您将看到 React 应用现通过 GraphQL 架构检索演讲者。

Speaker 组件中,我们可以再次删除 componentDidMount 方法,并且可以删除 this.state.talks 属性。而是在 render 方法中,我们将直接从 speaker 道具访问每个演讲者的演讲。

/src/main/webapp/Speaker.js
import React, {Component} from 'react'
import {Well} from 'react-bootstrap'
import Talk from "./Talks";

class Speaker extends Component {

  constructor() {
    super();

    this.state = {
      title: '',
      duration: ''
    }
  }

//... other methods ommitted for space

  render() {
    const {speaker} = this.props;
    const {title, duration} = this.state;

    return <Well>
      <h3>{speaker.firstName} {speaker.lastName}</h3>
      <hr/>

      <ul>
        {speaker.talks.map(t => <Talk key={t.id} talk={t} delete={() => this.deleteTalk(t.id)}/>)}
        <li>
          <label>Title</label>
          <input type='text' name='title' value={title}
                 onChange={this.handleNewTalkChange}/>

          <label>Duration</label>
          <input type="number" name="duration" value={duration}
                 onChange={this.handleNewTalkChange}/>
          <button onClick={this.addNewTalk}>Add Talk</button>
        </li>
      </ul>

    </Well>
  }
}

export default Speaker;

<1> <2>

但是,如果我们现在尝试运行应用程序,我们会发现我们只能访问每个演讲的 ID,我们缺少 titleduration。得益于 GraphQL,我们只需将这些属性添加到查询中,即可将它们轻松添加到我们检索的数据中。

编辑 src/main/webapp/SpeakerList.js,并将 titleduration 添加到 SPEAKER_QUERY

/src/main/webapp/SpeakerList.js
const SPEAKER_QUERY = gql`query {
speakerList(max: 10) {
  id, firstName, lastName,
    talks { id, title, duration } (1)
  }
}`;

这样,我们的多个 REST 调用已替换为一个 GraphQL 查询。你可以使用 Apollo 的 开发者工具浏览器扩展 检查此查询和响应。

Apollo Dev Tools Extension

7 变更

如前所述,变更是通过 GraphQL 模式更改数据的一种 GraphQL 机制。可用的变更在模式中定义。GORM GraphQL 插件会生成一些基本变更,文档中进行了说明。例如,对于我们的 Speaker 域类,该插件提供了以下变更

  • speakerCreate(book: {})

  • speaker(id: .., book: {})

  • speaker(id: ..)

当然也可以定义自定义变更,插件文档 介绍了具体方法。

我们的 React 应用已使用 REST 调用为指定的演讲者创建和删除谈话。让我们用 GraphQL 变更替换这些 REST 调用。

编辑 /client/src/Speaker.js

/client/src/Speaker.js
const talkCreate = gql`
    mutation talkCreate($talk: TalkCreate!) {
        talkCreate(talk: $talk) {
            id
            title
            duration
        }
    }
`;

const talkDelete = gql`
    mutation talkDelete($id: Long!) {
        talkDelete(id: $id) {
            error
        }
    }
`;

我们已为两个变更(talkCreatetalkDelete)定义了 GraphQL 片段。这些片段指定了每个变更所需的输入及其类型。你可以将这些变更键入到 GraphiQL 浏览器中,查看其所需的输入。

现在我们需要将这些变更绑定到我们的组件属性,以便我们从组件中调用它们。我们再次使用高级 graphql 组件执行此任务。由于我们有多个变更,因此我们导入 Apollo 的 compose 函数来简化这些变更的“链接”。

首先,在 Speaker.js 顶部导入 compose 函数

import {graphql, compose} from 'react-apollo'
import gql from 'graphql-tag'

现在我们可以使用 compose 将这些变更绑定到我们的 Speaker 组件。

client/src/Speaker.js
const SpeakerWithMutations = compose(
  graphql(talkCreate, {
      props: ({mutate}) => ({
          talkCreate: ({talk}) =>
            mutate({
                variables: {talk},
            })
      })
  }),
  graphql(talkDelete, {
      props: ({mutate}) => ({
          talkDelete: ({id}) =>
            mutate({
                variables: {id},
            })
      })
  })
)(Speaker)

export default SpeakerWithMutations;
有关使用变更和 compose 的更多信息,请参阅 Apollo 文档

最后一步是用 REST 调用代替调用我们的变更。在 Speaker.js 中查找 addNewTalkdeleteTalk 函数,并按照以下所示的内容编辑它们

client/src/Speaker.js
addNewTalk = () => {
    const {title, duration} = this.state;
    const {speaker} = this.props;

    this.setState({title: '', duration: ''});

    this.props.talkCreate({talk: {title, duration, speaker: {id: speaker.id}}})
      .then(({data}) => console.log('create response:', data))
      .catch((error) => console.log('there was an error sending the query', error));
};

deleteTalk = (id) => {
    this.props.talkDelete({id: id})
      .then(({data}) => console.log('delete response: ', data))
      .catch((error) => console.log('there was an error sending the query', error));
};

我们已绑定到组件的每个变更作为this.props的函数:talkCreate()talkDelete()。每个函数接受一个参数,该参数是一个 JavaScript 对象,其中包含我们的变更所需的所有参数。当变更解析后,将使用变更返回的数据调用then()函数。对于此示例,我们只是记录数据,但你还可以使用它来更新 Apollo 缓存,如 文档中所述

为简单起见,本指南依赖于我们之前设置的 pollingInterval 来刷新数据并从缓存中删除/添加谈话。在实际应用程序中,需要使用变更结果更新内存中的数据。

8 结论

在此指南中,我们展示了使用 Grails 创建全功能 GraphQL 模式以及在单页应用中使用 Apollo 和 Grails 的 React Profile 与该模式交互是多么轻松。请参阅GraphQL 的 GORM 文档和相关的Grails 和 React 指南以了解详情。

9 Grails 帮助

Object Computing, Inc. (OCI) 赞助了本指南的创建。提供各种咨询和支持服务。

OCI 是 Grails 的家

结识团队