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

Made with Slides.com