Como construímos componentes de interface do usuário no Rails no Flywheel

Publicados: 2019-11-16

Manter a consistência visual em um grande aplicativo da Web é um problema compartilhado por muitas organizações. No Flywheel, não somos diferentes. Nosso aplicativo web principal é construído com Ruby on Rails e temos cerca de 15 desenvolvedores Rails e três desenvolvedores front-end fazendo commit de código para ele em um determinado dia. Também somos grandes em design (é um dos nossos principais valores como empresa) e temos três designers que trabalham muito próximos aos desenvolvedores em nossas equipes Scrum.

Um dos nossos principais objetivos é garantir que qualquer desenvolvedor do Flywheel possa criar uma página responsiva sem obstáculos. Os roadblocks geralmente incluem não saber quais componentes existentes usar para construir uma maquete (o que leva a inflar a base de código com componentes redundantes muito semelhantes) e não saber quando discutir a reutilização com os designers. Isso contribui para experiências inconsistentes do cliente, frustração do desenvolvedor e uma linguagem de design diferente entre desenvolvedores e designers.

Passamos por várias iterações de guias de estilo e métodos de construção/manutenção de padrões e componentes de interface do usuário, e cada iteração ajudou a resolver os problemas que estávamos enfrentando naquele momento. Estamos agora em uma nova abordagem (para nós) que estou confiante de que nos preparará por um longo tempo. Se você enfrenta problemas semelhantes em sua aplicação Rails e gostaria de abordar componentes do lado do servidor, espero que este artigo possa lhe dar algumas ideias.

Neste artigo, vou mergulhar em:

  • O que estamos resolvendo
  • Restringir componentes
  • Renderizando componentes no lado do servidor
  • Onde não podemos usar componentes do lado do servidor


O que estamos resolvendo

Queríamos restringir completamente nossos componentes de interface do usuário e eliminar a possibilidade de a mesma interface do usuário ser criada de mais de uma maneira. Embora um cliente possa não saber (no início), não ter restrições nos componentes leva a uma experiência de desenvolvedor confusa, dificulta muito a manutenção e dificulta a realização de alterações globais no design.

A maneira tradicional de abordarmos os componentes era por meio de nosso guia de estilo, que listava toda a marcação necessária para construir um determinado componente. Por exemplo, aqui está a aparência da página do guia de estilo para o nosso componente slat:

Isso funcionou bem e nos serviu por vários anos, mas os problemas começaram a surgir quando adicionamos variantes, estados ou formas alternativas de usar o componente. Com uma parte complexa da interface do usuário, tornou-se complicado consultar o guia de estilo para saber quais classes usar e quais evitar, e em que ordem a marcação precisava estar para produzir a variação desejada. E, muitas vezes, os designers faziam pequenas adições ou ajustes em um determinado componente. Como o guia de estilo não suportava isso, hacks alternativos para obter esse ajuste para exibir corretamente (como canibalizar inadequadamente parte de outro componente) tornaram-se irritantemente comuns.

Exemplo de componente irrestrito

Para ilustrar como as inconsistências surgem ao longo do tempo, usarei um exemplo simples (e artificial), mas muito comum, de um de nossos componentes no aplicativo Flywheel: cabeçalhos de cartão.

Começando de novo a partir de uma maquete de design, é assim que um cabeçalho de cartão se parece. Era bem simples com um título, um botão e uma borda inferior.

.card__header
  .card__header-left
    %h2 Backups

  .card__header-right
    = link_to "#" do
      = icon("plus_small")

Depois de codificado, imagine um designer querendo adicionar um ícone à esquerda do título. Fora da caixa, não haverá margem entre o ícone e o título.

...
  .card__header-left
    = icon("arrow_backup", color: "gray25")
    %h2 Backups
...

Idealmente, resolveríamos isso no CSS para cabeçalhos de cartão, mas para este exemplo, digamos que outro desenvolvedor pensasse “Ah, eu sei! Temos alguns auxiliares de margem. Vou apenas dar um tapa na classe auxiliar no título.”

...
  .card__header-left
    = icon("arrow_backup", color: "gray25")
    %h2.--ml-10 Backups
...

Bem, isso tecnicamente parece com a maquete, certo?! Claro, mas digamos que um mês depois, outro desenvolvedor precise de um cabeçalho de cartão, mas sem o ícone. Eles encontram o último exemplo, copiam/colam e simplesmente removem o ícone.

Novamente, parece correto, certo? Fora de contexto, para alguém sem um olho afiado para design, claro! Mas olhe para ele ao lado do original. Essa margem esquerda no título ainda está lá porque eles não perceberam que o auxiliar de margem esquerda precisava ser removido!

Levando este exemplo um passo adiante, digamos que outra maquete pedisse um cabeçalho de cartão sem uma borda inferior. Pode-se encontrar um estado que temos no guia de estilo chamado “sem fronteiras” e aplicá-lo. Perfeito!

Outro desenvolvedor pode tentar reutilizar esse código, mas, nesse caso, eles realmente precisam de uma borda. Vamos hipoteticamente dizer que eles ignoram o uso adequado documentado no guia de estilo e não percebem que a remoção da classe sem bordas dará a eles sua borda. Em vez disso, eles adicionam uma régua horizontal. Acaba havendo algum preenchimento extra entre o título e a borda, então eles aplicam uma classe auxiliar ao hr e voila!

Com todas essas modificações no cabeçalho do cartão original, agora temos uma bagunça em nossas mãos no código.

.card__header.--borderless
  .card__header-left
    %h2.--ml-10 Backups

  .card__header-right
    = link_to "#" do
      = icon("plus_small")

  %hr.--mt-0.--mb-0

Tenha em mente que o exemplo acima é apenas para ilustrar o ponto de como componentes irrestritos podem se tornar confusos ao longo do tempo. Se alguém em nossa equipe tentou enviar uma variação de um cabeçalho de cartão, isso deve ser detectado por uma revisão de design ou revisão de código. Mas coisas como essa às vezes escapam das rachaduras, daí nossa necessidade de coisas à prova de balas!


Restringir componentes

Você pode estar pensando que os problemas listados acima já foram claramente resolvidos com componentes. Essa é uma suposição correta! Frameworks de front-end como React e Vue são super populares para esse propósito exato; são ferramentas incríveis para encapsular a interface do usuário. No entanto, há um problema com eles que nem sempre gostamos – eles exigem que sua interface do usuário seja renderizada por JavaScript.

O aplicativo Flywheel é muito pesado no back-end com HTML renderizado principalmente pelo servidor – mas, felizmente para nós, os componentes podem vir de várias formas. No final das contas, um componente de interface do usuário é um encapsulamento de estilos e regras de design que gera marcação para um navegador. Com essa percepção, podemos adotar a mesma abordagem para componentes, mas sem a sobrecarga de uma estrutura JavaScript.

Veremos como construímos componentes restritos abaixo, mas aqui estão alguns dos benefícios que encontramos ao usá-los:

  • Nunca há uma maneira errada de montar um componente.
  • O componente faz todo o design thinking para você. (Você apenas passa nas opções!)
  • A sintaxe para criar um componente é muito consistente e fácil de raciocinar.
  • Se for necessária uma alteração de projeto em um componente, podemos alterá-la uma vez no componente e ter certeza de que ela será atualizada em todos os lugares.

Renderizando componentes no lado do servidor

Então, do que estamos falando ao restringir componentes? Vamos cavar!

Como mencionado anteriormente, queremos que qualquer desenvolvedor que trabalhe no aplicativo Flywheel possa ver uma maquete de design de uma página e construir essa página imediatamente sem impedimentos. Isso significa que o método de criação da interface do usuário deve ser A) muito bem documentado e B) muito declarativo e livre de suposições.

Parciais para o resgate (ou assim pensamos)

Uma primeira tentativa que tentamos no passado foi usar parciais do Rails. Partials são a única ferramenta que o Rails oferece para reutilização em templates. Naturalmente, eles são a primeira coisa que todos buscam. Mas há desvantagens significativas em confiar neles porque, se você precisar combinar a lógica com um modelo reutilizável, terá duas opções: duplicar a lógica em cada controlador que usa a parcial ou incorporar a lógica na própria parcial.

Parciais evitam erros de duplicação de copiar/colar e funcionam bem nas primeiras vezes em que você precisa reutilizar algo. Mas, pela nossa experiência, os parciais logo ficam confusos com suporte para mais e mais funcionalidades e lógica. Mas a lógica não deve viver em modelos!

Introdução às células

Felizmente, há uma alternativa melhor para parciais que nos permite reutilizar o código e manter a lógica fora da visão. Chama-se Cells, uma gema Ruby desenvolvida pela Trailblazer. As células existem bem antes do aumento da popularidade em frameworks front-end como React e Vue e permitem que você escreva modelos de visão encapsulados que lidam com lógica e modelagem. Eles fornecem uma abstração de modelo de visão, que o Rails simplesmente não tem pronto para uso. Na verdade, estamos usando o Cells no aplicativo Flywheel há algum tempo, mas não em uma escala global e super reutilizável.

No nível mais simples, Cells nos permite abstrair um pedaço de marcação como este (usamos Haml para nossa linguagem de modelagem):

%div
  %h1 Hello, world!

Em um modelo de visualização reutilizável (muito semelhante aos parciais neste momento) e transforme-o nisso:

= cell("hello_world")

Isso nos ajuda a restringir o componente para onde classes auxiliares ou componentes filho incorretos não podem ser adicionados sem modificar a própria célula.

Construindo células

Colocamos todas as nossas células de interface do usuário em um diretório app/cells/ui. Cada célula deve conter apenas um arquivo Ruby, com o sufixo _cell.rb. Você pode tecnicamente escrever os templates diretamente em Ruby com o auxiliar content_tag, mas a maioria de nossas células também contém um template Haml correspondente que fica em uma pasta nomeada pelo componente.

Uma célula super básica sem lógica se parece com isso:

// cells/ui/slat_cell.rb

module UI
  class SlatCell < ViewModel
    def show
    end
  end
end

O método show é o que é renderizado quando você instancia a célula e buscará automaticamente um arquivo show.haml correspondente na pasta com o mesmo nome da célula. Nesse caso, é app/cells/ui/slat (escolhemos todas as nossas células de interface do usuário para o módulo de interface do usuário).

No modelo, você pode acessar as opções passadas para a célula. Por exemplo, se a célula for instanciada em uma view como = cell(“ui/slat”, title: “Title”, subtitle: “Subtitle”, label: “Label”), podemos acessar essas opções através do objeto options.

// cells/ui/slat/show.haml
.slat
  .slat__inner
    .slat__content
      %h4= options[:title]
      %p= options[:subtitle]
      = icon(options[:icon], color: "blue")

Muitas vezes movemos elementos simples e seus valores para um método na célula para evitar que elementos vazios sejam renderizados se uma opção não estiver presente.

// cells/ui/slat_cell.rb
def title
  return unless options[:title]
  content_tag :h4, options[:title]
end

def subtitle
  return unless options[:subtitle]
  content_tag :p, options[:subtitle]
end
// cells/ui/slat/show.haml
.slat
  .slat__inner
    .slat__content
      = title
      = subtitle

Agrupando células com um utilitário de interface do usuário

Depois de provar o conceito de que isso poderia funcionar em grande escala, eu queria lidar com a marcação estranha necessária para chamar uma célula. Simplesmente não flui direito e é difícil de lembrar. Então fizemos um pequeno ajudante para isso! Agora podemos apenas chamar = ui “name_of_component” e passar as opções inline.

= ui "slat", title: "Title", subtitle: "Subtitle", label: "Label"

Passando opções como um bloco em vez de inline

Levando o utilitário de interface do usuário um pouco mais longe, rapidamente ficou claro que uma célula com um monte de opções em uma linha seria super difícil de seguir e simplesmente feia. Aqui está um exemplo de uma célula com muitas opções definidas inline:

= ui “slat", title: “Title”, subtitle: “Subtitle”, label: “Label”, link: “#”, tertiary_title: “Tertiary”, disabled: true, checklist: [“Item 1”, “Item 2”, “Item 3”]

É muito complicado, o que nos leva a criar uma classe chamada OptionProxy que intercepta os métodos setter Cells e os traduz em valores de hash, que são então mesclados em opções. Se isso soa complicado, não se preocupe – é complicado para mim também. Aqui está um resumo da classe OptionProxy que Adam, um de nossos engenheiros de software sênior, escreveu.

Aqui está um exemplo de uso da classe OptionProxy dentro de nossa célula:

module UI
  class SlatCell < ViewModel
    def show
      OptionProxy.new(self).yield!(options, &block)
      super()
    end
  end
end

Agora com isso no lugar, podemos transformar nossas opções inline complicadas em um bloco mais agradável!

= ui "slat" do |slat|
  - slat.title = "Title"
  - slat.subtitle = "Subtitle"
  - slat.label = "Label"
  - slat.link = "#"
  - slat.tertiary_title = "Tertiary"
  - slat.disabled = true
  - slat.checklist = ["Item 1", "Item 2", "Item 3"]

Apresentando a lógica

Até este ponto, os exemplos não incluíram nenhuma lógica em torno do que a exibição exibe. Essa é uma das melhores coisas que o Cells oferece, então vamos falar sobre isso!

Mantendo nosso componente slat, temos a necessidade de algumas vezes renderizar a coisa toda como um link e algumas vezes renderizá-la como uma div, com base na presença ou não de uma opção de link. Acredito que este seja o único componente que temos que pode ser renderizado como div ou link, mas é um exemplo bem legal do poder das células.

O método abaixo chama um auxiliar link_to ou content_tag dependendo da presença de opções [:link] .

Nota: Isso foi inspirado e criado por Adam Lassek, que foi extremamente influente em nos ajudar a construir todo esse método de desenvolvimento de interface do usuário com o Cells.

def container(&block)
  tag =
    if options[:link]
      [:link_to, options[:link]]
    else
      [:content_tag, :div]
    end

  send(*tag, class: “slat__inner”, &block)
end

Isso nos permite substituir o elemento .slat__inner no template por um bloco container:

.slat
  = container do
  ...

Outro exemplo de lógica em Cells que usamos muito é o de saída condicional de classes. Digamos que adicionamos uma opção desabilitada à célula. Nada mais na invocação da célula muda, exceto que agora você pode passar uma opção disabled: true e observar como a coisa toda se transforma em um estado desativado (acinzentado com links não clicáveis).

= ui "slat" do |slat|
  ...
  - slat.disabled = true

Quando a opção desabilitada for verdadeira, podemos definir classes em elementos no modelo que são necessários para obter a aparência desabilitada desejada.

.slat{ class: possible_classes("--disabled": options[:disabled]) }
  .slat__inner
    .slat__content
      %h4{ class: possible_classes("--alt": options[:disabled]) }= options[:title]
      %p{ class: possible_classes("--alt": options[:disabled]) }=
      options[:subtitle]
      = icon(options[:icon], color: "gray")

Tradicionalmente, teríamos que lembrar (ou fazer referência ao guia de estilo) quais elementos individuais precisavam de classes adicionais para fazer tudo funcionar corretamente no estado desabilitado. As células nos permitem declarar uma opção e depois fazer o trabalho pesado para nós.

Nota: possible_classes é um método que criamos para permitir a aplicação condicional de classes no Haml de uma maneira agradável.


Onde não podemos usar componentes do lado do servidor

Embora a abordagem da célula seja extremamente útil para nossa aplicação específica e para a maneira como trabalhamos, seria negligente dizer que ela resolveu 100% dos nossos problemas. Ainda escrevemos JavaScript (muito) e construímos algumas experiências em Vue em todo o nosso aplicativo. 75% do tempo, nosso template Vue ainda vive em Haml e nós ligamos nossas instâncias Vue ao elemento que o contém, o que nos permite ainda tirar vantagem da abordagem de célula.

No entanto, em lugares onde faz mais sentido restringir completamente um componente como uma instância Vue de arquivo único, não podemos usar Cells. Nossas listas de seleção, por exemplo, são todas Vue. Mas acho que está tudo bem! Nós realmente não temos a necessidade de ter versões duplicadas de componentes em ambos os componentes Cells e Vue, então tudo bem que alguns componentes são 100% construídos com Vue e alguns são com Cells. Se um componente é construído com Vue, significa que o JavaScript é necessário para construí-lo no DOM e aproveitamos o framework Vue para fazer isso. No entanto, para a maioria de nossos outros componentes, eles não exigem JavaScript e, se o fizerem, exigem que o DOM já esteja compilado e apenas conectamos e adicionamos ouvintes de eventos.

À medida que continuamos progredindo com a abordagem de célula, definitivamente vamos experimentar a combinação de componentes de célula e componentes Vue para que tenhamos uma e apenas uma maneira de criar e usar componentes. Ainda não sei como é, então vamos atravessar a ponte quando chegarmos lá!


Nossa conclusão

Até agora, convertemos cerca de trinta de nossos componentes visuais mais usados ​​em Células. Isso nos deu uma enorme explosão de produtividade e dá aos desenvolvedores uma sensação de validação de que as experiências que estão construindo estão corretas e não foram hackeadas.

Nossa equipe de design está mais confiante do que nunca de que os componentes e as experiências em nosso aplicativo são 1:1 com o que eles projetaram no Adobe XD. As alterações ou adições aos componentes agora são tratadas apenas por meio de uma interação com um designer e um desenvolvedor front-end, o que mantém o restante da equipe focado e livre de preocupações em saber como ajustar um componente para corresponder a um modelo de design.

Estamos constantemente repetindo nossa abordagem para restringir os componentes da interface do usuário, mas espero que as técnicas ilustradas neste artigo dêem a você um vislumbre do que está funcionando bem para nós!


Venha trabalhar na Volante!

Na Flywheel, cada departamento tem um impacto significativo em nossos clientes e resultados. Seja suporte ao cliente, desenvolvimento de software, marketing ou qualquer outra coisa, estamos todos trabalhando juntos em nossa missão de construir uma empresa de hospedagem pela qual as pessoas possam realmente se apaixonar.

Pronto para se juntar à nossa equipe? Estamos contratando! Aplique aqui.