UTILIZANDO GRAPHQL E
REACT/REACT NATIVE/RELAY
PARA APLICAÇÕES RUBY ON RAILS MULTIPLATAFORMA
- Bacharel e mestre em Ciência da Computação (UFBA)
- Ruby On Rails desde 2008
- http://ca.ios.ba
- Engenheiro de software do Meedan desde 2011
- São Francisco - Califórnia
- http://meedan.com
Check
Verificação de veracidade de mídias sociais
2012 - atual
github.com/meedan/checkdesk
github.com/meedan/check-api
github.com/meedan/check-web
github.com/meedan/lapis
github.com/meedan/generator-keefer
Backend
Frontend
Ruby On Rails template
Yeoman generator
graphql
react.js
relay
ruby on rails
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
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
}
GraphQL
Ruby On Rails
Criando uma nova API Rails com suporte a GraphQL
$ git clone https://github.com/meedan/lapis.git
$ rails new <nome> -m <caminho para lapis_template.rb>
- Nenhuma view (headless API)
- REST & GraphQL
- Swagger UI & GraphiQL
- Docker
- Geração de gem para clientes
- Webhooks & tokens
Adicionando suporte a GraphQL
gem 'graphql'
gem 'graphql-relay'
Gemfile
class GraphqlController < BaseApiController
def create
vars = params[:variables] || {}
query = GraphQL::Query.new(RelayOnRailsSchema, params[:query], variables: vars)
render json: query.result
end
end
app/controllers/graphql_controller.rb (adicionar rota também)
RelayOnRailsSchema = GraphQL::Schema.new(query: QueryType, mutation: MutationType)
config/initializers/relay.rb
type Media {
title: String
embed: String
user: User
comments: [Comment]
}
# "Type" para diferenciar dos models
MediaType = GraphQL::ObjectType.define do
name 'Media'
description 'Media type'
field :title, types.String
field :embed, types.String
field :user do
type UserType
resolve -> (media, _args, _ctx) {
media.user
}
end
connection :comments, ->{ CommentType.connection_type }
do
argument :user_id, types.Int
resolve -> (media, args, ctx) {
media.comments(args[:user_id], ctx[:current_user])
}
end
end
Definindo um tipo GraphQL
app/graph/types/media_type.rb
# "Type" para diferenciar dos models
# MediaType = GraphQL::ObjectType.define do
# name 'Media'
# description 'Media type'
field :title, types.String
field :embed, types.String
# field :user do
# type UserType
# resolve -> (media, _args, _ctx) {
# media.user
# }
# end
# connection :comments, ->{ CommentType.connection_type }
# do
# argument :user_id, types.Int
#
# resolve -> (media, args, ctx) {
# media.comments(args[:user_id], ctx[:current_user])
# }
# end
# end
Definindo um tipo GraphQL
Por padrão, os campos
resolvem para métodos de instância
do model
# "Type" para diferenciar dos models
# MediaType = GraphQL::ObjectType.define do
# name 'Media'
# description 'Media type'
# field :title, types.String
# field :embed, types.String
field :user do
type UserType
resolve -> (media, _args, _ctx) {
media.user
}
end
# connection :comments, ->{ CommentType.connection_type }
# do
# argument :user_id, types.Int
#
# resolve -> (media, args, ctx) {
# media.comments(args[:user_id], ctx[:current_user])
# }
# end
# end
Definindo um tipo GraphQL
É possível ter um comportamento específico a partir de um método de resolução e referenciar outros tipos
# "Type" para diferenciar dos models
# MediaType = GraphQL::ObjectType.define do
# name 'Media'
# description 'Media type'
# field :title, types.String
# field :embed, types.String
# field :user do
# type UserType
# resolve -> (media, _args, _ctx) {
# media.user
# }
# end
connection :comments, ->{ CommentType.connection_type }
do
argument :user_id, types.Int
resolve -> (media, args, ctx) {
media.comments(args[:user_id], ctx[:current_user])
}
end
# end
Definindo um tipo GraphQL
Relacionamentos referenciam conexões de outros tipos, e podem receber argumentos e ter acesso ao contexto
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
CreateMedia = GraphQL::Relay::Mutation.define do
input_field :url, !types.String
return_field :media, MediaType
resolve -> (inputs, ctx) {
media = Media.new
media.url = inputs[:url]
media.save!
}
end
Em Rails, uma mutação deve definir os campos de entrada, saída e um método de execução
Metaprogramação FTW
https://github.com/meedan/check-api/blob/develop/
app/graph/mutations/graphql_crud_operations.rb
module MediaMutations
Create, Update, Destroy =
GraphqlCrudOperations.define_crud_operations(
'media',
{ url: '!str' }, # Campos de criação
{ url: 'str', id: '!id' } # Campos de atualização
)
end
React.js
- 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
Um código, diferentes plataformas
- Yeoman Generator
- Gera um esqueleto de aplicação React / React Native
- Recebe uma URL como parâmetro
- Tema via SASS
- Documentação
- Abstração de plataforma: apenas o componente pai varia entre plataformas
- Docker
- GraphQL / Relay
$ git clone 'https://github.com
/meedan/generator-keefer.git'
$ cd generator-keefer
$ cp config.yml.example config.yml
$ npm install -g yo
$ npm link
$ yo keefer
$ PLATFORM=web npm run build
Aplicação Web
$ PLATFORM=chrome npm run build
Extensão de navegador
$ PLATFORM=android npm run build
Aplicação móvel
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/rubyconfbr16
http://ca.ios.ba
Utilizando GraphQL e React/React Native/Relay para aplicações Ruby On Rails multiplataforma
By Caio Sacramento
Utilizando GraphQL e React/React Native/Relay para aplicações Ruby On Rails multiplataforma
Atualmente os usuários utilizam diversos dispositivos para acessar uma aplicação. Pode ser um aplicativo em um dispositivo Android, um aplicativo em um dispositivo iOS, uma aplicação web ou mesmo uma extensão para navegador web. Escrever diferentes códigos para diferentes plataformas, e além disso mantê-los e testá-los, é um desafio que se torna altamente custoso e em muitos casos inviável. Há muito tempo almejava-se uma tecnologia que permitisse escrever um mesmo código cliente e compilá-lo para diferentes plataformas. Cordova / Ionic parecia ser um caminho para permitir ao menos aplicações híbridas entre web, iOS e Android, mas por não utilizar código nativo, as aplicações móveis construídas com esta tecnologia sofriam com performance. Por falar em performance, não apenas o frontend sofre com isto, mas também o backend. Utilizar polling para atualização automática e não fazer cache dos dados no cliente podem comprometer a performance no lado do servidor também. Recentemente o Facebook abriu o código do React.js e React Native, tecnologias baseadas em JavaScript mas que trouxeram um novo paradigma para aplicações híbridas, com melhor performance em dispositivos móveis a partir da compilação para código nativo. Além disso, o Facebook abriu também o código do GraphQL, uma linguagem declarativa para requisição de dados que retorna apenas os dados requisitados pelo cliente, e Relay, uma tecnologia baseada em JavaScript para facilitar o consumo de dados a partir de um servidor GraphQL e que cuida automaticamente de funcionalidades como paginação, cache e subscriptions. Nesta palestra, o autor irá mostrar como adicionar uma camada GraphQL a uma aplicação Ruby On Rails existente, que é capaz de expor parte do modelo de dados via esta interface, e como construir uma aplicação JavaScript baseada em React, React Native e Relay que consome estes dados e funciona em diferentes plataformas (iOS, Android, Google Chrome, web), a partir do mesmo código-fonte. Para os exemplos, serão utilizados os frameworks Keefer (projeto de código aberto que é um gerador Node.js que permite criar facilmente uma aplicação cliente com estas características) e Lapis (outro projeto de código aberto que é um template Rails para criação de APIs web integradas com Swagger e GraphQL), ambos mantidos pelo autor. Fonte: http://www.rubyconf.com.br/pt-BR/schedule#b7093c183ff30ff68ac8ad5838fd8001
- 1,110