-> all posts

html vivo finalmente

/ 9 min read
elixir web tutorial

this post is only available in português

No fim do post passado ficou a pergunta: se a página mostrasse a Fubá de verdade, o que precisaria acontecer entre o pedido chegar e o HTML sair? A jhujuba pensou um pouco e respondeu: “tá… alguém precisa montar o HTML na hora, né? mas… onde a coelhinha fica guardada entre um pedido e outro?”.

Licensa poética aqui, ela não pensou mas tá mó legal escrever como se fosse uma conversa com ela

As duas respostas dela são o post de hoje. E no meio do caminho ela soltou a pergunta oficial: “tá, mas que diacho é template?”.

html com buracos

Um template é um HTML com buracos. Em vez de escrever a carinha da Fubá no arquivo, você escreve um “buraco” no lugar dela - e quem entrega a página preenche os buracos com os valores. Olha a transformação do nosso index.html:

<% humor = Fuba.Cuidado.humor(@fuba) %>
...
<p class="carinha"><%= Fuba.Humor.carinha(humor) %></p>
<h1><%= @fuba.nome %></h1>
<p class="humor"><%= FubaWeb.Tela.frase(humor) %></p>
...
<span class="barra <%= if @fuba.cafeina <= 1, do: "baixa" %>">
  <%= FubaWeb.Tela.barrinha(@fuba.cafeina) %>
</span>

O arquivo sai de priv/static/index.html e vira lib/fuba_web/tela.html.eex. O CSS continua exatamente o mesmo - o que muda são os “buracos”, e eles falam Elixir:

  • <%= ... %> - “calcula isso e imprime o resultado aqui”;
  • <% ... %> - “só executa, sem imprimir” (a primeira linha guarda o humor numa variável pro resto da página usar);
  • @fuba - a coelhinha que veio de fora, entregue na hora de preencher.

Compara com a página morta: antes a carinha chatinha e a cafeína baixa estavam escritas à mão no arquivo. Agora a carinha vem de Fuba.Humor.carinha/1, a barrinha é desenhada a partir do número real, e o vermelho de “baixa” só aparece se o medidor estiver mesmo em 1 ou menos. A página deixou de ser uma foto e virou uma pergunta: “como tá a Fubá agora?”.

onde a coelhinha mora

Só que “como tá a Fubá agora?” esconde um problema novo. No iex, a coelhinha morava numa variável: fuba = Cuidado.dar_biscoito(fuba). Mas o servidor atende pedidos soltos - cada GET é uma conversa independente, e nenhuma variável sua sobrevive de um pedido pro outro. A coelhinha precisa de um lugar pra morar entre os pedidos.

Em Elixir, esse lugar é um processo: uma caixinha leve que roda dentro da aplicação, guardando um valor e respondendo bilhetes - “me mostra”, “atualiza com essa função”. Elixir já traz uma caixinha pronta pra exatamente isso, o Agent. A nossa caixinha:

# lib/fuba/guarda.ex
defmodule Fuba.Guarda do
  @moduledoc "A caixinha que guarda a coelhinha."

  use Agent

  def start_link(_opts) do
    Agent.start_link(fn -> %Fuba.Coelhinha{} end, name: __MODULE__)
  end

  def espiar, do: Agent.get(__MODULE__, & &1)
  def atualizar(fun), do: Agent.update(__MODULE__, fun)
end

Duas operações só: espiar/0 devolve a coelhinha de agora, e atualizar/1 recebe uma função e troca a coelhinha pelo resultado dela - Cuidado.dar_biscoito/1 encaixa direto, porque ela recebe uma coelhinha e devolve outra. O name: __MODULE__ registra a caixinha com o nome do próprio módulo - nesse caso Fuba.Guarda, pra qualquer parte do programa achar ela sem precisar de senha.

Repara onde a Guarda mora: em lib/fuba/, do lado da coelhinha - ela não sabe o que é HTTP e nem internet. E não se preocupa com a teoria de processos (“caixinhas”) agora; o @moduledoc já avisa que ela vem no post 8, quando a Fubá ganhar relógio.

a tela

Quem preenche os buracos é um módulo novo:

# lib/fuba_web/tela.ex
defmodule FubaWeb.Tela do
  require EEx

  EEx.function_from_file(:defp, :render_eex, "lib/fuba_web/tela.html.eex", [:assigns])

  def render(fuba), do: render_eex(%{fuba: fuba})

  def barrinha(n), do: String.duplicate("■", n) <> String.duplicate("□", 5 - n)

  def frase(:feliz), do: "tudo certo por aqui"
  def frase(:chatinha), do: "tá chatinha… cadê o café?"
  def frase(:desregulada), do: "desregulada. só espaço ajuda."
  def frase(:go_queen), do: "GO QUEEN!"
  def frase(_), do: "…"
end

A linha no começo é o truque: EEx.function_from_file/4 lê o template e o transforma numa função Elixir de verdade, render_eex/1. Template não é um arquivo interpretado a cada pedido - depois de compilado (rodar mix compile), ele é código, tão rápido quanto qualquer função sua. O [:assigns] no fim diz que a função recebe um mapa, e é daí que sai o @fuba lá do template: @ é atalho pra “o que me passaram”, chamado “fuba”, como uma variável especial pro template.

O render/1 recebe uma coelhinha, bota dentro num mapa %{fuba: fuba}, chama a função do template. E isso tudo fica na camada web, porque são assunto de apresentação: barrinha/1 desenha ■■■□□ enchendo de até o número e completando com até cinco, e frase/1 dá uma legenda pra cada humor. Carinha é responsabilidade da coelhinha (Fuba.Humor); frase de efeito é da tela. Cada um com seu papel.

o roteador

Última peça: quem responde o pedido. O FubaWeb.Plug foi demitido e o Plug.Static junto - faz sentido: a página não é mais um arquivo parado em disco, então não tem mais arquivo estático pra servir. No lugar, um roteador:

# lib/fuba_web/router.ex
defmodule FubaWeb.Router do
  use Plug.Router

  plug :match
  plug :dispatch

  get "/" do
    corpo = FubaWeb.Tela.render(Fuba.Guarda.espiar())

    conn
    |> put_resp_content_type("text/html; charset=utf-8")
    |> send_resp(200, corpo)
  end

  match _ do
    send_resp(conn, 404, "nada aqui, uai")
  end
end

Um roteador é o recepcionista do servidor: olha o método e o caminho do pedido e encaminha pra sala certa. O use Plug.Router te dá os blocos get e match - get "/" atende só GET na raiz, e o match _ no fim é a cláusula pega-tudo: qualquer outro caminho cai ali e ganha o “nada aqui, uai”, que sobreviveu à reforma.

E a rota da raiz é uma linha que conta o post inteiro: espia a coelhinha na caixinha, renderiza a tela com ela, devolve o HTML.

Tem nome bonito pra isso, aliás: server-side rendering, SSR, ou “renderização do lado do servidor”. O HTML nasce no servidor, fresco, a cada pedido. O navegador continua só desenhando - mas agora cada desenho é feito na hora.

subindo a caixinha

Aquele arquivo que toma conta do programa, o lib/fuba/application.ex, ganha um morador novo:

# lib/fuba/application.ex
children = [
  Fuba.Guarda
  {Bandit, scheme: :http, plug: FubaWeb.Router, port: 4000}
]

A Guarda sobe primeiro - ela precisa estar de pé antes de qualquer pedido chegar. E o Bandit agora aponta pro FubaWeb.Router.

vamos ver isso funcionando

Sobe com iex -S mix e abre http://localhost:4000 - nota que agora a raiz / funciona, porque o roteador atende / de propósito. A página abre feliz: ( ᵔ ᴥ ᵔ ), “tudo certo por aqui”.

Agora, no iex:

iex> Fuba.Guarda.atualizar(fn c -> %{c | cafeina: 0} end)
:ok

Volta no navegador e atualiza a página (ctrl+r ou cmd+r no Mac).

(  ̄^ ̄) - “tá chatinha… cadê o café?”, e a barrinha de cafeína vermelha em um quadradinho. A página mudou porque a coelhinha mudou. Cada F5 é o servidor espiando a caixinha e montando o HTML daquele instante. Dá dois biscoitos nela (Fuba.Guarda.atualizar(&Fuba.Cuidado.dar_biscoito/1)), atualiza de novo, vê a barrinha encher.

Os botões, claro, continuam mortos - a página agora mostra a coelhinha, mas clicar ainda não fala com ela. Falta o botão avisar o servidor. Esse aviso tem nome (formulário, POST) e consequência (a página pisca). Próximo post.

fechou?

Três critérios:

  1. http://localhost:4000 abre a página feliz - na raiz, sem /index.html.
  2. Mudar a coelhinha no iex com Fuba.Guarda.atualizar/1 muda a página no próximo F5 - testa zerar a cafeína e depois dar café.
  3. Você consegue explicar, em voz alta, o caminho: pedido chega → roteador → espiar a caixinha → render preenche os buracos → HTML sai.

Fechou? A Fubá agora tem rosto e o rosto é dela de verdade. Só falta ele reagir ao clique do botão - e é pra isso que servem formulários.