Arquitetura de microserviços com Ruby On Rails e multiclientes com React.js
- Check / Bridge
- 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
Arquitetura Monolítica
x
Arquitetura em Micro-serviços
Pender
github.com/meedan/pender
Check API
github.com/meedan/check-api
Alegre
github.com/meedan/alegre
Backend (Ruby On Rails APIs)
Check Web
github.com/meedan/check-web
Check Mark
github.com/meedan/check-mark
Check Slack Bot
github.com/meedan/check-bot
Frontend (JavaScript / React.js apps)
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/fisl18
Arquitetura de microserviços com Ruby On Rails e multiclientes com React.js
By Caio Sacramento
Arquitetura de microserviços com Ruby On Rails e multiclientes com React.js
Durante muito tempo, as arquiteturas monolíticas dominaram as soluções de software, com grandes softwares responsáveis por muitas tarefas, por fluxos completos e por diversas camadas, de servidor a cliente. A tendência era que tais softwares possuíssem bases de código cada dia maiores, mais difíceis de manter, de testar e de escalar. A arquitetura de microserviços surgiu nos últimos anos favorecendo componentes de software mais enxutos, responsáveis por tarefas específicas no contexto de uma aplicação distribuída, em que a comunicação entre estes ocorre através de chamadas de API via HTTP. Do mesmo modo, separou-se a camada do cliente da camada do servidor, cuja comunicação também ocorre via chamadas de API. O objetivo desta palestra é apresentar uma arquitetura deste tipo que está em produção. O Check, software livre para verificação de fake news, desenvolvido desde 2011 pelo Meedan, empresa para a qual os autores trabalham, segue esta arquitetura desde 2016 compreendendo dois microserviços em Ruby On Rails (uma API REST e uma API com GraphQL) e múltiplos clientes de React.js (cliente web, extensão de navegador, aplicativo Android (em React Native) e um bot para a plataforma de comunicação Slack). Será apresentada a arquitetura do Check, explicando o papel de seus componentes e os detalhes das tecnologias envolvidas.
- 1,861