GraphQL

Максим Воронцов

REST


                        /notes/:name

                        /notes/:name/comments

                        ? /notes/:name/comments/authors

                        ? /users/:id

                        ? /notes/:name/comments_with_authors
                    

REST

  • Большое число запросов
  • Сложности в проектировании при росте зависимостей между сущностями
  • Лишние данные в ответе от сервера
  • Всегда необходимо помнить об обратной совместимости
  • Нет удобных инструментов для разработки

GraphQL

Язык запросов к API, а так же среда исполнения для этих запросов

GraphQL

  • Строгая типизация
  • Получаем только то, что действительно необходимо
  • Возможность получить все необходимые данные за один запрос
  • Отсутствие проблем с обратной совместимостью и расширением
  • Удобные инструменты для разработки
  • Реализации на всех популярных языках

GraphQL

GraphQL

GraphQL

GraphQL

GraphQL

GraphQL


                        query {
                          note(name: "Books") {
                            name
                            text
                            comments {
                              text
                              author {
                                name
                              }
                            }
                          }
                        }
                    

GraphiQL

Types

ID, Int, Float, String, Boolean

                                    type User {
                                      id: ID
                                      name: String
                                    }
                                

                                    type Comment {
                                      text: String
                                      author: User
                                    }
                                

                                    type Note {
                                      name: String
                                      text: String
                                      comments: [Comment]
                                    }
                                

                                    type Query {
                                        note(name: String!): Note
                                        notes: [Note]
                                    }
                                

Unions


                        type Admin {
                          id: ID
                          name: String
                          accessLevel: String
                        }

                        type NormalUser {
                          id: ID
                          name: String
                          age: Int
                        }

                        union User = Admin | NormalUser
                    

Queries


                                query {
                                  note(name: "Books") {
                                    name
                                    text
                                    comments {
                                      text
                                    }
                                  }
                                }
                                

                                    {
                                      "data": {
                                        "note": {
                                          "name": "Books",
                                          "text": "Books to read",
                                          "comments": [
                                            { "text": "Очень круто!" },
                                            { "text": "А мне не очень понравилось :(" },
                                            { "text": "Peter, объяснишь почему?" },
                                          ]
                                        }
                                      }
                                    }
                                

Queries


                                    query {
                                      note(name: "Books") {
                                        text
                                      }

                                      user(id: 1) {
                                        name
                                      }
                                    }
                                

                                    {
                                      "data": {
                                        "note": {
                                          "text": "Books to read"
                                        },
                                        "user": {
                                          "name": "Max"
                                        }
                                      }
                                    }
                                

Aliases


                                    query {
                                      firstNote: note(name: "Books") {
                                        text
                                      }

                                      secondNote: note(name: "Films") {
                                        text
                                      }
                                    }
                                

                                    {
                                      "data": {
                                        "firstNote": {
                                          "text": "Books to read"
                                        },
                                        "secondNote": {
                                          "text": "Films to watch"
                                        },
                                      }
                                    }
                                

Named Queries


                        query NotesQuery {
                          firstNote: note(name: "Books") {
                            text
                          }

                          secondNote: note(name: "Films") {
                            text
                          }
                        }
                    

Fragments


                        fragment NoteFields on Note {
                            id
                            name
                            text
                        }
                    

Fragments


                        query {
                            firstNote: note(name: "Books") {
                                ...NoteFields
                            }

                            secondNote: note(name: "Films") {
                                ...NoteFields
                            }
                        }
                    

Inline fragments


                        query {
                          users {
                            __typename

                            ... on Admin {
                              name
                              accessLevel
                            }

                            ... on NormalUser {
                              name
                              age
                            }
                          }
                        }
                    

Variables


                        query NoteQuery($name: String!) {
                          note(name: $name) {
                            name
                            text
                          }
                        }
                    

                        // Variables

                        { "name": "Books" }
                    

Directives


                        query NoteQuery($name: String!, $withComments: Boolean!) {
                          note(name: $name) {
                            name
                            text
                            comments @include(if: $withComments) {
                              text
                            }
                          }
                        }
                    

Directives


                        query NoteQuery($name: String!, $withoutComments: Boolean!) {
                          note(name: $name) {
                            name
                            text
                            comments @skip(if: $withoutComments) {
                              text
                            }
                          }
                        }
                    

Mutations


                        mutation CreateNote($name: String!, $text: String!) {
                          createNote(name: $name, text: $text) {
                            name
                            text
                          }
                        }
                    

GraphQL + Node.js

graphql.js

Делаем простой запрос


                        $ npm install --save graphql
                    

                        const { graphql, buildSchema } = require('graphql');
                        const notes = [
                            { name: 'Films', text: 'Films to watch' },
                            { name: 'Books', text: 'Books to read' }
                        ];
                        const schema = buildSchema(`
                            type Note {
                              name: String
                              text: String
                            }

                            type Query {
                              note(name: String!): Note
                            }
                        `);
                    

Делаем простой запрос


                        const resolvers = {
                            note: args => notes.find(note => note.name === args.name)
                        };

                        const query = `
                            query {
                              note(name: "Books") {
                                text
                              }
                            }
                        `;

                        graphql(schema, query, resolvers).then(response => {
                          // { data: { note: { text: "Books to read" } } }
                        });
            

Mutations


                        const schema = buildSchema(`
                            type Note {
                              name: String
                              text: String
                            }

                            type Mutation {
                              createNote(name: String!, text: String!): Note
                            }
                        `);
                    

Mutations


                        const resolvers = {
                            createNote: args => {
                                const note = { name: args.name, text: args.text };

                                notes.push(note);

                                return note;
                            }
                        };
                    

Mutations


                        const mutation = `
                            mutation {
                              createNote(name: "Music", text: "Music to listen") {
                                name
                                text
                              }
                            }
                        `;

                        graphql(schema, mutation, resolvers).then(response => {
                            // {"data": { "createNote": { "name": "Music", "text": "Music to listen" }}}
                        });
                    

GraphQL Server

GraphQL Server

$ npm install --save express express-graphql

                        const express = require('express');
                        const graphql = require('express-graphql');

                        const schema = require('./schema');

                        const server = express();

                        server.use('/graphql', graphql({ schema, graphiql: true }));

                        server.listen(4000, () => {
                          console.log('Listening on http://localhost:4000/graphql');
                        });
                    

Schema


                        const { GraphQLID, GraphQLString, GraphQLObjectType } = require('graphql');

                        const UserType = new GraphQLObjectType({
                            name: 'User',
                            fields: {
                                id: { type: GraphQLID },
                                name: { type: GraphQLString }
                            }
                        });
                    

Schema


                        const { GraphQLString, GraphQLObjectType } = require('graphql');

                        const CommentType = new GraphQLObjectType({
                            name: 'Comment',
                            fields: {
                                text: { type: GraphQLString },
                                author: { type: UserType }
                            }
                        });
                    

Schema


                        const {
                            GraphQLList, GraphQLID, GraphQLString, GraphQLObjectType
                        } = require('graphql');

                        const NoteType = new GraphQLObjectType({
                            name: 'Note',
                            fields: {
                                id: { type: GraphQLID },
                                name: { type: GraphQLString },
                                text: { type: GraphQLString },
                                comments: {
                                    type: new GraphQLList(CommentType),
                                    resolve: parentValue =>
                                        comments.filter(comment => comment.noteId === parentValue.id)
                                }
                            }
                        });
                    

Schema


                        const Query = new GraphQLObjectType({
                            name: 'Query',
                            fields: {
                                note: {
                                    type: NoteType,
                                    args: {
                                        name: {
                                            type: new GraphQLNonNull(GraphQLString)
                                        }
                                    },
                                    resolve: (parentValue, { name }) =>
                                        notes.find(note => note.name === name)
                                }
                            }
                        });
                    

Schema


                        const Mutation = new GraphQLObjectType({
                            name: 'Mutation',
                            fields: {
                                createNote: {
                                    type: NoteType,
                                    args: {
                                        name: new GraphQLNonNull(GraphQLString),
                                        text: new GraphQLNonNull(GraphQLString)
                                    },
                                    resolve: (parentValue, { name, text }) => {
                                        const note = { name, text };
                                        notes.push(note);
                                        return note;
                                    }
                                }
                            }
                        });
                    

Schema


                        module.export = new GraphQLSchema({
                            query: Query,
                            mutation: Mutation
                        });
                    

На практике

Errors


                                    query {
                                      note(name: "Books") {
                                        name
                                        createdAt
                                      }
                                    }
                                

                                    {
                                      "errors": [
                                        {
                                          "message": "Cannot query field \"createdAt\" on type \"Note\".",
                                          "locations": [
                                            {
                                              "line": 4,
                                              "column": 5
                                            }
                                          ]
                                        }
                                      ]
                                    }
                                

Errors


                        {
                          "data": null,
                          "errors": [
                            {
                              "code": "UNQ",
                              "message": "Fields must be unique",
                              "details": {
                                "fields": [
                                  "name"
                                ]
                              }
                            }
                          ]
                        }
                    

GraphQL

  • Новая технология
  • Мало паттернов
  • Сложности при работе с SQL базами данных
  • Сложности при работе с реактивными данными

GraphQL Клиенты

Lokka Максимально простой в использовании. Базовая поддержка запросов и мутаций. Простейшее кэширование
Apollo Более гибкий. Хороший баланс между функциональностью и сложностью использования
Relay Наиболее функциональный, из-за чего наиболее сложный в использовании. Много внимания уделено производительности (особенно на мобильных).

Создаем Apollo Client


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

                        import React from 'react';
                        import ReactDOM from 'react-dom';
                        import { ApolloProvider } from 'react-apollo';
                        import ApolloClient from 'apollo-client-preset';

                        const client = new ApolloClient();

                        ReactDOM.render(
                            <ApolloProvider client={client}>
                                <Note name="Books" />
                            </ApolloProvider>,
                            document.getElementById('root')
                        );
                    

Queries


                        import gql from 'graphql-tag';

                        const query = gql`
                            query Note($name: String!) {
                              note(name: $name) {
                                name
                                text
                              }
                            }
                        `;
                    

Higher-Order Components


                        function logProps(WrappedComponent) {
                            return class extends React.Component {
                                static getDerivedStateFromProps(nextProps) {
                                    console.log('Current props: ', this.props);
                                    console.log('Next props: ', nextProps);

                                    return null;
                                }

                                render() {
                                  return <WrappedComponent {...this.props} />;
                                }
                            }
                        }
                    

Queries


                        import React from 'react';
                        import { graphql } from 'react-apollo';

                        function Note({ data }) {
                            return (
                                <React.Fragment>
                                    <h1>{data.name}</h1>
                                    <p>{data.text}</p>
                                </React.Fragment>
                            );
                        }

                        export default graphql(query, {
                            options: props => ({ variables: { name: props.name } })
                        })(Note);
                    

Uncaught TypeError: Cannot read property name of undefined

Queries


                        function Note({ data }) {
                            if (data.loading) {
                                return 'Loading...';
                            }

                            return (
                                <React.Fragment>
                                    <h1>{data.name}</h1>
                                    <p>{data.text}</p>
                                </React.Fragment>
                            );
                        }
                    

Mutations


                        class Form extends React.Component {
                            state = { name: '', text: '' }

                            handleSubmit = () => this.makeSomeApiRequest(this.state.name, this.state.text)
                            handleNameChange = event => this.setState({ name: event.target.value })
                            handleTextChange = event => this.setState({ text: event.target.value })

                            render() {
                                const { name, text } = this.state;

                                return (
                                    <div>
                                        <input value={name} onChange={this.handleNameChange} />
                                        <textarea value={text} onChange={this.handleTextChange} />
                                        <button onClick={this.handleSubmit}>
                                            Отправить
                                        <button>
                                    </div>
                                );
                            }
                        }
                    

Mutations


                        const mutation = gql`
                            mutation CreateNote($name: String!, $text: String!) {
                              createNote(name: $name, text: $text) {
                                id
                              }
                            }
                        `;

                        export default graphql(mutation)(Form);
                    

Mutations


                        class Form extends React.Component {
                            ...

                            handleSubmit = () => {
                                const { mutate } = this.props;
                                const { name, text } = this.state;

                                mutate({ variables: { name, text } })
                                    .then(() => { ... })
                                    .catch(() => { ... })
                            }

                            ...
                        }
                    

Cache

  1. Получили список всех заметок
  2. Создали новую заметку
  3. Перешли на страницу со всеми заметками
  1. Создали заметку
  2. Перешли на страницу со всеми заметками

Cache

  1. Получили список всех заметок
  2. Создали новую заметку
  3. Перешли на страницу со всеми заметками
  1. Создали заметку
  2. Перешли на страницу со всеми заметками

Cache


                        class Form extends React.Component {
                            ...

                            handleSubmit = () => {
                                const { mutate } = this.props;
                                const { name, text } = this.state;

                                mutate({
                                    variables: { name, text },
                                    refetchQueries: ['Notes']
                                })
                                    .then(() => { ... })
                                    .catch(() => { ... })
                            }

                            ...
                        }
                    

compose


                        export default graphql(createNoteMutation)(graphql(notesQuery))(IndexPage));

                                                            ):

                        import { compose } from 'react-apollo';

                        // compose(f, g, h)(x) === f(g(h(x)))

                        compose(
                            graphql(createNoteMutation),
                            graphql(notesQuery)
                        )(IndexPage);
                                                            (:
                    

React + Apollo

  • Простота использования
  • Активная поддержка и развитие
  • Гибкая настройка кэширования
  • Поддержка fragments
  • Optimistic Updates
  • Polling

GraphQL

GraphQL Specification
GraphQL.js
GraphQL Best Practices

GraphQL Clients

Lokka
Apollo
Relay