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

  • 982