GraphQL

 

Caio Almeida, GDG Meetup, 2020

  • Caio Almeida
  • @caiosba
  • https://ca.ios.ba
  • Diretor de Engenharia de Software do Meedan (https://meedan.com)
  • Bacharel / Mestre em Ciência da Computação (UFBA)

graphql

react.js

relay

backend

graphql

react.js

relay

ruby on rails

BACKEND

FRONTEND

GraphQL

Linguagem declarativa para requisição de dados que retorna apenas os dados solicitados pelo cliente

 

Descrição completa do modelo de dados da sua API

 

REST

Media

Comment

Tag

1

*

User

*

*

*

1

GET /api/v1/medias/1
GET /api/v1/medias/1/comments
GET /api/v1/medias/1/tags
GET /api/v1/medias/1/comments/1
GET /api/v1/users/1?fields=avatar,name
GET /api/v1/users/2?fields=avatar,name
GET /api/v1/users/3?fields=avatar,name
...

Endpoints reutilizáveis

GET /api/v1/medias/1?include=comments&count=5

GET /api/v1/medias/1?include=comments,tags
    &comments_count=5&tags_count=5

GET /api/v1/medias/1?fields=comments(text,date)
    &tags(tag)


...


GET /api/v1/media_and_comments/1
GET /api/v1/media_comments_and_tags/1
GET /api/v1/media_comments_tags_and_users/1
GET /api/v1/medias/1?include=comments&count=5

GET /api/v1/medias/1?include=comments,tags
    &comments_count=5&tags_count=5

GET /api/v1/medias/1?fields=comments(text,date)
    &tags(tag)


...


GET /api/v1/media_and_comments/1
GET /api/v1/media_comments_and_tags/1
GET /api/v1/media_comments_tags_and_users/1

Muitas requisições!

GraphQL

Playground

Um endpoint para todos governar

POST /graphql

POST /api/graphql?query=
{
  media(id: 1) {
    title
    embed
    tags(first: 3) {
      tag
    }
    comments(first: 5) {
      created_at
      text
      user {
        name,
        avatar
      }
    }
  }
}
POST /api/graphql?query=
{
  media(id: 1) {
    title
    embed
    tags(first: 3) {
      tag
    }
    comments(first: 5) {
      created_at
      text
      user {
        name,
        avatar
      }
    }
  }
}

Media

Comment

Tag

1

*

User

*

*

*

1

~

POST /api/graphql?query=
{
  media(id: 1) {
    title
    embed
    tags(first: 3) {
      tag
    }
    comments(first: 5) {
      created_at
      text
      user {
        name,
        avatar
      }
    }
  }
}
{
  "media": {
    "title": "Avangers Hulk Smash",
    "embed": "<iframe src=\"...\"></iframe>",
    "tags": [
      { "tag": "avengers" },
      { "tag": "operation" }
    ],
    "comments": [
      {
        "text": "This is true",
        "created_at": "2016-09-18 15:04:39",
        "user": {
          "name": "Ironman",
          "avatar": "http://[...].png"
        }
      },
      ...
    ]
  }
}

Sistema de Tipos

POST /api/graphql?query=
{
  media(id: 1) {
    title
    embed
    tags(first: 3) {
      tag
    }
    comments(first: 5) {
      created_at
      text
      user {
        name,
        avatar
      }
    }
  }
}

Media

Comment

Tag

1

*

User

*

*

*

1

type QueryType {
  media(id: Int): Media
  comment(id: Int): Comment
  user(id: Int): User
  me: User
}
type Media {
  title: String
  embed: String
  comments: [Comment]
  tags:[Tag]
}
type Comment {
  created_at: String
  text: String
  user: User
}
type User {
  name: String
  avatar: String
}

Mutações

mutation { 
  createMedia(
    input: {
      url: "http://youtu.be/7a_insd29fk"
      clientMutationId: "1"
    }
   )
   {
     media {
       id
     }
   }
}

Mutações provocam alterações na aplicação.


CRUD:

Queries: Read

Mutations:

  • Create
  • Update
  • Delete
# mutation { 
     createMedia(
#     input: {
        url: "http://youtu.be/7a_insd29fk"
#       clientMutationId: "1"
#     }
#    )
     {
       media {
         id
       }
     }
# }

Nome da mutação

Parâmetros de entrada

Formato de saída

React.js

Playground

  • Desenvolvido pelo Facebook
  • Biblioteca para a camada de visualização
    • Não é um framework
  • React.js não é auto-suficiente

Mais um framework JavaScript?

  • Complexidade de 2-way data binding
  • Alterações frequentes nos dados
  • Complexidade da interface do Facebook
  • Desvio da lógica do MVC

Controllers

Directives

Templates

Global Event Listeners

Models

Apenas Componentes

REGRA #1:

TUDO EM REACT É UM COMPONENTE

var MediaController = new Controller({
  addComment: function(comment) {
    // Requisição REST
  },

  deleteComment: function(commentId) {
    // Requisição REST
  },

  addTag: function(tag) {
    // Requisição REST
  }
});
var MediaController = new Controller({
  addComment: function(comment) {
    // Requisição REST
  },

  deleteComment: function(commentId) {
    // Requisição REST
  },

  addTag: function(tag) {
    // Requisição REST
  }
});
<div class="media">
  <h2>{{media.title}}</h2>
  <ul class="tags">
    <li ng-repeat="tag in tags">{{tag.tag}}</li>
  <ul>
  <ul class="comments">
    <li ng-repeat="comment in comments">
      <div ng-include="partials/comment.html">
    </li>
  <ul>
</div>
var MediaController = new Controller({
  addComment: function(comment) {
    // Requisição REST
  },

  deleteComment: function(commentId) {
    // Requisição REST
  },

  addTag: function(tag) {
    // Requisição REST
  }
});
<div class="media">
  <h2>{{media.title}}</h2>
  <ul class="tags">
    <li ng-repeat="tag in tags">{{tag.tag}}</li>
  <ul>
  <ul class="comments">
    <li ng-repeat="comment in comments">
      <div ng-include="partials/comment.html">
    </li>
  <ul>
</div>
this.$el.find('#delete-comment')
.on('click', function() {
  // chama deleteComment
});

MediaComponent

CommentsComponent

TagsComponent

TagComponent

CommentComponent

CommentComponent

CommentComponent

Separação de Concerns


Separação de Componentes

x

Componentes auto-contidos:

  • Testáveis
  • Compostos
  • Reutilizáveis
  • Mantidos
import React, { Component, PropTypes } from 'react';
import Relay from 'react-relay';
import DeleteTagMutation from './DeleteTagMutation';

class Tag extends Component {
  handleDelete() {
    const id = this.props.tag.id;
    Relay.Store.commitUpdate(
      new DeleteTagMutation({
        id: id
      })
    );
  }

  render() {
    const tag = this.props.tag;

    return (
      <div className="tag">
        <span>{tag.tag}</span>
        <span onClick={this.delete.bind(this)}>
          x
        </span>
      </div>
    );
  }
}

export default Tag;

JSX

  • Linguagem de marcação parecida com HTML
  • Descrição declarativa da interface
  • Combina a facilidade dos templates com o poder do JavaScript
  • Pré-processador traduz JSX para JavaScript plano

Boas práticas de performance

  • Evitar operações custosas de DOM
  • Minimizar o acesso ao DOM
  • Atualizar elementos offline antes de re-inserir no DOM
  • Evitar ajustar layouts em JavaScript

    O desenvolvedor deve se preocupar com isso?

REGRA #2: React redesenha tudo a cada atualização

Parece custoso? Mas é rápido!
Virtual DOM

  • Cria uma descrição leve da interface do componente
  • Calcula as diferenças entre versão atual e a anterior
  • Computa o conjunto mínimo de alterações a serem aplicadas ao DOM
  • Executa em lote todas as alterações

REGRA #3: Única fonte de dados

// import React, { Component, PropTypes } from 'react';
// import Relay from 'react-relay';
// import DeleteTagMutation from './DeleteTagMutation';
//
// class Tag extends Component {
//   handleDelete() {
       const id = this.props.tag.id;
       this.setState({ deleted: true });
//     Relay.Store.commitUpdate(
//       new DeleteTagMutation({
//         id: id
//       })
//     );
//   }
// 
//   render() {
      const tag = this.props.tag;

      if (this.state.deleted) {
        return (<span>Deleted</span>);
      }
//
//    componentWillMount() {
//      this.sort();
//    }
//
//    return (
//      <div className="tag">
//        <span>{tag.tag}</span>
//        <span onClick={this.delete.bind(this)}>
//        x
//        </span>
//      </div>
//    );
//  }
// }
//
// export default Tag;

props

São imutáveis

 

state

É mutável

class Tags extends Component {
  render() {
    return (
      <ul className="tags-list">
        {props.tags.map(function(tag) {
          return (
            <Tag tag={tag}>
          );
        })}
      </ul>
    );
  }
}
// import React, { Component, PropTypes } from 'react';
// import Relay from 'react-relay';
// import DeleteTagMutation from './DeleteTagMutation';
//
// class Tag extends Component {
     componentWillMount() {
       this.parent.sort();
     }
 
//   render() {
//     const tag = this.props.tag;
//
//     if (this.state.deleted) {
//       return (<span>Deleted</span>);
//     }
//
//     return (
//       <div className="tag">
//         <span>{tag.tag}</span>
//         <span onClick={this.delete.bind(this)}>
//         x
//         </span>
//       </div>
//     );
//   }
// }
//
// export default Tag;

Ciclo de vida

Callbacks que são chamados

em sequência na processo de construção/atualização

de um componente

React Native

  • Biblioteca que converte JSX para:
    • iOS Cocoa
    • Android UI
  • Aplicações com performance similar a aplicação nativa
  • Extensível
  • Possibilidade de reaproveitar os componentes
  • Código portável para Android e iOS
class Tags extends Component {
  render() {
    return (
      <ul className="tags-list">
        {props.tags.map(function(tag) {
          return (
            <Tag tag={tag}>
          );
        })}
      </ul>
    );
  }
}

Componente React

React.js

DOM

React Native

Android

iOS

Relay

Relay

GraphQL

React.js

class MediaComponent extends Component {
  render() {
    const media = this.props.media;

    return (
      <div>
        <article>
          <MediaDetail media={media} />
          <Tags tags={media.tags} />
          <h3>Verification Timeline</h3>
          <Comments comments={media.comments} />
        </article>
      </div>
    );
  }
}
class MediaComponent extends Component {
  render() {
    const media = this.props.media;

    return (
      <div>
        <article>
          <MediaDetail media={media} />
          <h2>{media.title}</h2>
          <Tags tags={media.tags} />
          <h3>Verification Timeline</h3>
          <Comments comments={media.comments} />
        </article>
      </div>
    );
  }
}
const MediaContainer = Relay.createContainer(
  MediaComponent, {
  fragments: {
    media: () => Relay.QL`
      fragment on Media {
        title
        embed
        user { ...userFragment }
        tags(first: 3) { edges { node {
          tag, id
        } } }
        comments(first: 5) { edges { node {
          ...commentFragment
        } } }
      }
    `
  }
});

class Media extends Component {
  render() {
    var route = new MediaRoute({ id: 1 });
    return (
      <Relay.RootContainer 
       Component={MediaContainer}
       route={route}
      />
    );
  }
}
class MediaRoute extends Relay.Route {
  static queries = {
    media: () => Relay.QL`query Media { 
                   media(id: $id)
                 }`, 
  };
  static paramDefinitions = {
    id: { required: true }
  };
  static routeName = 'MediaRoute';
};
  • Apenas dados novos são requisitados
  • Primeiramente as queries são validadas localmente
  • Consulta feita primeiramente no cache local
    • Relay Store

Mutações

class CreateTagMutation extends Relay.Mutation {
  getMutation() {
    return Relay.QL`mutation createTag {
      createTag
    }`;
  }

  getFatQuery() {
    return Relay.QL`fragment on CreateTagPayload
    { tagEdge, media { tags } }`;
  }

  getVariables() {
    const media = this.props.media;
    return { tag: media.tag.tag, media_id: media.id };
  }

  getConfigs() {
    return [{
      type: 'RANGE_ADD',
      parentName: 'media',
      parentID: this.props.media.id,
      connectionName: 'tags',
      edgeName: 'tagEdge',
      rangeBehaviors: {
        '': 'append'
      }
    }];
  }
}
// class CreateTagMutation extends Relay.Mutation {
     getMutation() {
       return Relay.QL`mutation createTag {
         createTag
       }`;
     }
// 
//   getFatQuery() {
//     return Relay.QL`fragment on CreateTagPayload
//     { tagEdge, media { tags } }`;
//   }
// 
//   getVariables() {
//     const media = this.props.media;
//     return { tag: media.tag.tag, media_id: media.id };
//   }
// 
//   getConfigs() {
//     return [{
//       type: 'RANGE_ADD',
//       parentName: 'media',
//       parentID: this.props.media.id,
//       connectionName: 'tags',
//       edgeName: 'tagEdge',
//       rangeBehaviors: {
//         '': 'append'
//       }
//     }];
//   }
// }

Mutação a ser

chamada

Retorno esperado

Parâmetros de

entrada

Como atualizar o cache local

// class CreateTagMutation extends Relay.Mutation {
//   getMutation() {
//     return Relay.QL`mutation createTag {
//       createTag
//     }`;
//   }
// 
     getFatQuery() {
       return Relay.QL`fragment on CreateTagPayload
       { tagEdge, media { tags } }`;
     }
// 
//   getVariables() {
//     const media = this.props.media;
//     return { tag: media.tag.tag, media_id: media.id };
//   }
// 
//   getConfigs() {
//     return [{
//       type: 'RANGE_ADD',
//       parentName: 'media',
//       parentID: this.props.media.id,
//       connectionName: 'tags',
//       edgeName: 'tagEdge',
//       rangeBehaviors: {
//         '': 'append'
//       }
//     }];
//   }
// }

Mutação a ser

chamada

Retorno esperado

Parâmetros de

entrada

Como atualizar o cache local

// class CreateTagMutation extends Relay.Mutation {
//   getMutation() {
//     return Relay.QL`mutation createTag {
//       createTag
//     }`;
//   }
// 
//   getFatQuery() {
//     return Relay.QL`fragment on CreateTagPayload
//     { tagEdge, media { tags } }`;
//   }
// 
     getVariables() {
       const media = this.props.media;
       return { tag: media.tag.tag, media_id: media.id };
     }
// 
//   getConfigs() {
//     return [{
//       type: 'RANGE_ADD',
//       parentName: 'media',
//       parentID: this.props.media.id,
//       connectionName: 'tags',
//       edgeName: 'tagEdge',
//       rangeBehaviors: {
//         '': 'append'
//       }
//     }];
//   }
// }

Mutação a ser

chamada

Retorno esperado

Parâmetros de

entrada

Como atualizar o cache local

// class CreateTagMutation extends Relay.Mutation {
//   getMutation() {
//     return Relay.QL`mutation createTag {
//       createTag
//     }`;
//   }
// 
//   getFatQuery() {
//     return Relay.QL`fragment on CreateTagPayload
//     { tagEdge, media { tags } }`;
//   }
// 
//   getVariables() {
//     const media = this.props.media;
//     return { tag: media.tag.tag, media_id: media.id };
//   }
// 
     getConfigs() {
       return [{
         type: 'RANGE_ADD',
         parentName: 'media',
         parentID: this.props.media.id,
         connectionName: 'tags',
         edgeName: 'tagEdge',
         rangeBehaviors: {
           '': 'append'
         }
       }];
     }
   }

Mutação a ser

chamada

Retorno esperado

Parâmetros de

entrada

Como atualizar o cache local

// import React, { Component, PropTypes } from 'react';
// import Relay from 'react-relay';
// import DeleteTagMutation from './DeleteTagMutation';
// 
// class Tag extends Component {
//   handleDelete() {
//     const id = this.props.tag.id;
       Relay.Store.commitUpdate(
         new DeleteTagMutation({
           id: id
         })
       );
//   }
// 
//   render() {
//     const tag = this.props.tag;
// 
//     return (
//       <div className="tag">
//         <span>{tag.tag}</span>
//         <span onClick={this.delete.bind(this)}>
//           x
//         </span>
//       </div>
//     );
//   }
// }
// 
// export default Tag;

Chamandouma mutação

Obrigado! :)

slides.com/caiosba/graphql