Клиент-Сервер

GraphQL

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

REST

REST

REST

students/42
students/42/friends
?


REST

/students/42/groups/
/students/42/departments/

/students/42/friends/groups/
/students/42/friends/departments/

/students/42/friends_with_groups_and_departments/
            

REST

Сложности в проектировании при росте зависимостей между сущностями

Большое число запросов

Лишние данные в ответе от сервера

Всегда необходимо помнить об обратной совместимости

Нет удобных инструментов для разработки

GraphQL

GraphQL

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

GraphQL

Строгая типизация

Получаем только то, что действительно необходимо

Возможность получить все необходимые данные за один запрос

Отсутствие проблем с обратной совместимостью и расширением

Удобные инструменты для разработки

Реализации на всех популярных языках

GraphQL

GraphQL

GraphQL

GraphQL

GraphQL

GraphQL


query {
  student(id: 42) {
    friends {
      group {
        name
      }
      department {
        name
      }
    }
  }
}
            

GraphiQL

Types

ID, Int, Float, String, Boolean

type Group {
  id: ID
  name: String
}

type Student {
  id: ID
  name: String
  age: Int
  group: Group
}
            

type Query {
  group(id: ID!): Group
  groups: [Group]

  student(id: ID!): Student
  students: [Student]
}
                        

Unions

type Student {
  id: ID
  name: String
  age: Int
  group: Group
}

type Lecturer {
  id: ID
  name: String
  degree: String
}

union Person = Student | Lecturer

Queries

query {
  student(id: 1) {
    id
    name
    age
    group {
      name
    }
  }
}
            
{
  "data": {
    "student": {
      "id": 1,
      "name": "Максим",
      "age": 22,
      "group": {
        "name": "МГКН-1"
      }
    }
  }
}
            

Queries

query {
  student(id: 1) {
    name
    group {
      name
    }
  }

  group(id: 1) {
    name
  }
}
            
{
  "data": {
    "student": {
      "name": "Максим",
      "group": {
        "id": 1,
        "name": "МГКН-1"
      }
    },
    "group": {
      "name": "МГКН-1"
    }
  }
}

Aliases

query {
  first: student(id: 1) {
    id
    name
    age
  }

  second: student(id: 2) {
    id
    name
    age
  }
}
            
{
  "data": {
    "first": {
      "id": 1,
      "name": "Максим",
      "age": 22
    },
    "second": {
      "id": 2,
      "name": "Иван",
      "age": 19
    }
  }
}
            

Named Queries

query StudentsQuery {
  first: student(id: 1) {
    id
    name
    age
  }

  second: student(id: 2) {
    id
    name
    age
  }
}
            

Fragments

fragment StudentFields on Student {
    id
    name
    age
    group {
        id
        name
    }
}
            

Fragments

query {
    first: student(id: 1) {
        ...StudentFields
    }

    second: student(id: 2) {
        ...StudentFields
    }
}
            

Inline fragments

query {
  persons {
    __typename # Meta field
    ... on Student {
      name
      group {
        name
      }
    }

    ... on Lecturer {
      name
      degree
    }
  }
}

Variables

query StudentQuery($id: ID!) {
  student(id: $id) {
    id
    name
    age
  }
}

// Variables

{
  "id": "1"
}

Directives

query StudentQuery($id: ID!, $withGroup: Boolean!) {
  student(id: $id) {
    id
    name
    age
    group @include(if: $withGroup) {
      id
      name
    }
  }
}

Directives

query StudentQuery($id: ID!, $withoutAge: Boolean!) {
  student(id: $id) {
    id
    name
    age @skip(if: $withoutAge)
    group {
      id
      name
    }
  }
}

Mutations

mutation CreateStudent($name: String!, $age: Int!, $groupId: ID!) {
  createStudent(name: $name, age: $age, groupId: $groupId) {
    id
    name
    age
    group {
      id
      name
    }
  }
}
GraphQL.js

Queries

$ npm install --save graphql

const { graphql, buildSchema } = require('graphql');
const students = [
    { id: '1', name: 'Maxim' },
    { id: '2', name: 'Ivan' }
];
const schema = buildSchema(`
  type Student {
    id: ID
    name: String
  }

  type Query {
    student(id: ID!): Student
  }
`);

Queries

const root = {
  student: args => {
    return students.find(student => student.id === args.id);
  }
};

const query = `
  query {
    student(id: 1) {
      name
    }
  }
`;

graphql(schema, query, root).then(response => {
  // { data: { student: { name: "Максим" } } }
});
            

Mutations

const schema = buildSchema(`
  type Student {
    id: ID
    name: String
  }

  type Mutation {
    createStudent(name: String!): Student
  }
`);

Mutations

const root = {
  createStudent: args => {
    const newStudent = {
      id: newId,
      name: args.name
    };

    students.push(newStudent);

    return newStudent;
  }
};

Mutations

const mutation = `
  mutation {
    createStudent(name: "Peter") {
      id
      name
    }
  }
`;

graphql(schema, mutation, root).then(response => {
  console.log(response);
});

// {"data": { "createStudent": { "id": 4, "name": "Peter" }}}

GraphQL Server

$ npm install --save express express-graphql

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

const app = express();

app.use('/graphql', graphqlHTTP({
  schema: schema,
  rootValue: root,
  graphiql: true
}));

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

GraphQL Server

Schema

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

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

Schema

Schema

const StudentType = new GraphQLObjectType({
  name: 'Student',
  fields: {
    id: { type: GraphQLID },
    name: { type: GraphQLString },
    age: { type: GraphQLInt },
    group: {
      type: GroupType,
      resolve: parentValue => {
        return groups.find(group => {
          return group.id === parentValue.groupId
        });
      }
    }
  }
});
            

Schema

const Query = new GraphQLObjectType({
  name: 'Query',
  fields: {
    student: {
      type: StudentType,
      args: {
        id: { type: new GraphQLNonNull(GraphQLID) }
      },
      resolve(parentValue, { id }) {
        return students.find(student => {
          return student.id === id;
        });
      }
    }
  }
});
            

Schema

const Query = new GraphQLObjectType({
  name: 'Query',
  fields: {
    // ...

    students: {
      type: new GraphQLList(StudentType),
      resolve() {
        return students;
      }
    }
  }
});
            

Schema

module.exports = new GraphQLSchema({
  query: Query
});
            

Schema

const Mutation = new GraphQLObjectType({
  name: 'Mutation',
  fields: {
    createStudent: {
      type: StudentType,
      args: {
        name: new GraphQLNonNull(GraphQLString),
        age: new GraphQLNonNull(GraphQLInt),
        groupId: new GraphQLNonNull(GraphQLID)
      },
      resolve(parentValue, args) {
        return StudentModel.create(args);
      }
    }
  }
});
            

Schema

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

Practical example

Errors


query {
  student(id: 1) {
    name
    wrongField
  }
}
            

Errors

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

Errors

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

GraphQL

Новая технология

Мало паттернов

Сложности при работе с SQL базами данных

Сложности при работе с реактивными данными

GraphQL Clients

GraphQL Clients

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

React + Apollo Client

React + Apollo Client


React + Apollo Client

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

Apollo Client Setup

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

const client = new ApolloClient({});

const Root = () => (
  <ApolloProvider client={client}>
    <div>Root Component</div>
  </ApolloProvider>
);

ReactDOM.render(
  <Root />,
  document.getElementById('root')
);
            

Queries

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

const query = gql`
  query Student($id: ID!) {
    student(id: $id) {
      name
      group {
        name
      }
    }
  }
`;
            

Queries

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

const Student = ({ data }) => (
  <div>
    <div>Имя: {data.student.name}</div>
    <div>Группа: {data.student.group.name}</div>
  </div>
);

export default graphql(query, {
  options: ({ id }) => ({ variables: { id } })
})(Student);
            
Uncaught TypeError: Cannot read property 'name' of undefined

Queries

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

const Student = ({ data }) => {
  if (data.loading) {
      return <div>Loading...</div>;
  }

  // ...
};

export default graphql(query, {
    options: ({ id }) => ({ variables: { id } })
})(Student);
            

Mutations

const Student = ({ data }) => {
  // ...

  return (
    <div>
      <div>Имя: {data.student.name}</div>
      <div>Группа: {data.student.group.name}</div>
      <button>Удалить</button>
    </div>
  );
}
            

Mutations

const mutation = gql`
  mutation RemoveStudent($id: ID!) {
    removeStudent(id: $id)
  }
`;
            

Mutations

export default graphql(mutation)(graphql(query, {
  options: ({ id }) => ({ variables: { id } })
})(Student));
            

Mutations

import { compose } from 'react-apollo';

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

export default compose(
  graphql(query, {
    options: ({ id }) => ({ variables: { id } })
  }),
  graphql(mutation)
)(Student);
            

Mutations

class Student extends React.Component {
  handleRemoveClick() {
    this.props.mutate({
      variables: { id: this.props.id }
    })
      .then(() => { ... })
      .catch(response => {
        console.log(response.graphQLErrors);
      });
  }

  // ...
}
            

Mutations

class Student extends React.Component {
  render() {
    // ...

    return (
      <div>
        <div>Имя: {data.student.name}</div>
        <div>Группа: {data.student.group.name}</div>
        <button onClick={this.handleRemoveClick}>
          Удалить
        </button>
      </div>
    );
  }
}
            

Cache

Students: Петя, Вася, Маша
  1. Зашли на страницу со списком всех студентов
  2. Перешли на страницу Васи
  3. Удалили
  4. Вернулись на страницу со списком всех студентов
  1. Зашли на страницу Васи по прямой ссылке
  2. Удалили
  3. Перешли на страницу со списком всех студентов

Cache


class Student extends React.Component {
  handleRemoveClick() {
    this.props.mutate({
      variables: { id: this.props.id },
      refetchQueries: [
        'Students'
      ]
    })
      .then(() => { ... })
      .catch(() => { ... });
  }

  // ...
}
            

Apollo Client

Простота использования

Гибкая настройка кэширования

Fragments

Optimistic Updates

Polling

GraphQL

GraphQL Specification

GraphQL.js

GraphQL Best Practices

GraphQL Clients

Lokka

Apollo

Relay

Examples

GraphQL Server

React + Apollo