GraphQL
Caio Almeida, GDG Meetup, 2020
- Caio Almeida
- @caiosba
- https://ca.ios.ba
- Engenheiro 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
GraphQL
By Caio Sacramento
GraphQL
GraphQL é uma linguagem de consulta criada pelo Facebook em 2012 e lançada publicamente em 2015. É considerada uma alternativa para arquiteturas REST, e uma tentativa de unificar um padrão de consulta e disponibilização de dados por APIs web, independentemente da linguagem de programação utilizada no frontend ou no backend. Neste workshop, será apresentado o conceito de GraphQL, sua comparação com APIs REST e implementações no frontend e no backend para diferentes linguagens.
- 897