Asset Pipeline para Iniciantes

2012 July 01, 05:34 h - tags: tutorial front-end beginner rails learning

Este é provavelmente um dos assuntos mais confusos para quem está iniciando com Ruby on Rails. Antigamente, as regras eram simples:

  • coloque todos os seus assets (imagens, stylesheets e javascripts) organizados nas pastas public/images, public/stylesheets, public/javascripts;
  • utilize helpers como image_tag, stylesheet_link_tag e javascript_include_tag;
  • configure seu servidor web (Apache, NGINX) para servir URIs como /images/rails.png diretamente de public/images/rails.png para não precisar passar pelo Rails;

Pronto, está tudo preparado para funcionar. Porém, existiam e ainda existem muitas situações que essa regra não cobria e diversas técnicas, “boas práticas” e gems externas precisaram ser criadas para resolvê-las. Em particular, temos as seguintes situações cotidianas em desenvolvimento web:

  • quando se tem muitos assets, como javascripts, é considerado boa prática “minificá-los”, ou seja, otimizar ao máximo a quantidade de bytes eliminando supérfluos como espaços em branco e quebras de linha, nomes de variáveis e funções longas, etc. E além disso concatenar a maior quantidade de arquivos num único quanto possível. Em desenvolvimento, precisamos ter todos abertos e individuais para facilitar o debugging, mas em produção o correto é “compilá-los”
  • cache precisa ser usado o máximo possível e escrever o caminho a um path manualmente, como <img src=“/images/rails.png”/> é ruim, pois se precisarmos mudar o conteúdo dessa imagem, os usuários precisariam limpar seus caches pois o correto é configurarmos os servidores web com diretivas para manter assets no cache local por um longo período de tempo (1 ano ou mais). Helpers como image_tag criavam caminhos como <img src=“/images/rails.png?12345678”/>, sendo esse número derivado do timestamp de modificação do asset. Assim, se o asset era atualizado esse número mudava. Mas isso não funciona bem com muitos tipos de caches e proxies, que ignoram o que vem depois do “?”
  • quando uma página possui dezenas ou às vezes centenas de pequenas imagens e ícones (setas, botões, logotipos de seção, linhas, bordas, etc), o correto é usar a mesma técnica que usamos com stylesheets e javascripts: concatenar muitas imagens em um único arquivo maior e então utilizar CSS para manipular a posição x e y dentro dessa única imagem grande para posicioná-la corretamente onde precisamos.
  • começamos a utilizar vários tipos diferentes de geradores de templates, como LESS e SASS para gerar stylesheets, CoffeeScript para gerar Javascript, além do próprio ERB para adicionar conteúdo dinâmico nos templates.

Para resolver essas e outras situações é que foi criado o chamado Asset Pipeline, que é um conjunto de bibliotecas e convenções para resolver o problema de assets da melhor forma possível. O Asset Pipeline sozinho não resolve tudo, ele é um framework para que seja possível integrar diferentes soluções de forma organizada.

Tudo que será explicado neste artigo vale para o Rails 3.2 e superior, existem diferenças importantes nas versões anteriores que não serão tratadas aqui. Leia o Rails Guides, especialmente os Release Notes de cada versão.

Iniciando um projeto Rails

Quando iniciamos um novo projeto com o comando rails new novo_projeto, o primeiro arquivo que você vai querer mexer é o Gemfile:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# Original
group :assets do
  gem 'sass-rails',   '~> 3.2.3'
  gem 'coffee-rails', '~> 3.2.1'

  # See https://github.com/sstephenson/execjs#readme for more supported runtimes
  # gem 'therubyracer', :platforms => :ruby

  gem 'uglifier', '>= 1.0.3'
end

# Recomendado para iniciar
group :assets do
  gem 'sass-rails'
  gem 'compass-rails'

  # See https://github.com/sstephenson/execjs#readme for more supported runtimes
  gem 'therubyracer', :platforms => :ruby

  gem 'uglifier'
end

Não vamos entrar na controvérsia agora sobre CoffeeScript. Se você está iniciando, esqueça CoffeeScript por enquanto para não complicar ainda mais. Por outro lado, usar o Compass e particularmente o Compass Rails é algo que nem precisamos discutir já que o Compass provê diversos mixins de Sass muito úteis.

Aliás, se você ainda não conhece SASS, faça a você mesmo um favor e aprenda. Se você entende CSS, não vai ter problemas entendendo Sass, em particular a versão “SCSS” ou “Sassy CSS” que não é mais do que um conjunto acima do CSS3. Lembrando que mesmo escolhendo usar SASS podemos misturar arquivos .css.scss e arquivos convencionais .css no mesmo projeto.

Ao modificar o arquivo Gemfile, lembre-se de executar os seguintes comandos no terminal:

1
bundle

Para exercitar, vamos criar um simples controller com uma única página dinâmica para entender o que podemos fazer com isso. De volta ao terminal faça o seguinte:

1
2
rm public/index.html
bundle exec rails g controller home index

O resultado será:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  create  app/controllers/home_controller.rb
   route  get "home/index"
  invoke  erb
  create    app/views/home
  create    app/views/home/index.html.erb
  invoke  test_unit
  create    test/functional/home_controller_test.rb
  invoke  helper
  create    app/helpers/home_helper.rb
  invoke    test_unit
  create      test/unit/helpers/home_helper_test.rb
  invoke  assets
  invoke    js
  create      app/uploads/javascripts/home.js
  invoke    scss
  create      app/uploads/stylesheets/home.css.scss

E para combinar, já que estamos recomendando SCSS, vamos apagar o arquivo app/stylesheets/application.css e criar um novo:

1
2
rm app/stylesheets/application.css
touch app/stylesheets/application.css.scss

E nesse novo arquivo podemos colocar somente:

1
@import "compass"

Outra boa prática é ignorar o diretório public/uploads do repositório Git (você utilizar Git, correto?). Faça o seguinte:

1
echo "public/uploads" >> .gitignore

E agora já podemos iniciar o servidor local de Rails e examinar o que temos até agora:

1
bundle exec rails s

Processo de Pré-Compilação

Resumidamente, em termos de assets temos os seguintes principais elementos e estrutura:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
app
  assets
    images
      rails.png
    javascripts
      application.js
      home.js
    stylesheets
      application.css.scss
      home.css.scss
  views
    home
      index.html.erb
    layouts
      application.html.erb
config
  application.rb
public
  assets
Gemfile
Gemfile.lock

O código fonte do layout app/views/layouts/application.html.erb contém o seguinte:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!DOCTYPE html>
<html>
<head>
  <title>NovoProjeto</title>
  <%= stylesheet_link_tag    "application", :media => "all" %>
  <%= javascript_include_tag "application" %>
  <%= csrf_meta_tags %>
</head>
<body>

<%= yield %>

</body>
</html>

Se você já tinha visto até o Rails 2.x, um layout padrão ERB não é tão diferente. Com o servidor de pé, em ambiente de desenvolvimento, vejamos o HTML gerado ao abrir http://localhost:3000/home/index:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!DOCTYPE html>
<html>
<head>
  <title>NovoProjeto</title>
  <link href="/uploads/application.css" media="all" rel="stylesheet" type="text/css" />
  <script src="/uploads/jquery.js?body=1" type="text/javascript"></script>
  <script src="/uploads/jquery_ujs.js?body=1" type="text/javascript"></script>
  <script src="/uploads/home.js?body=1" type="text/javascript"></script>
  <script src="/uploads/application.js?body=1" type="text/javascript"></script>
  <meta content="authenticity_token" name="csrf-param" />
  <meta content="OFmZwwtshevVgcs1DUg56WVIQ8NcJZsri/nUubhEJCk=" name="csrf-token" />
</head>
<body>

<h1>Home#index</h1>
<p>Find me in app/views/home/index.html.erb</p>

</body>
</html>

No HTML gerado, note que os links para os assets apontam todos para /uploads. Além disso note que a chamada javascript_include_tag(“application”) expandiu para 4 javascripts diferentes. Para entender isso, precisamos examinar mais de perto o arquivo app/uploads/javascripts/application.js:

1
2
3
4
...
//= require jquery
//= require jquery_ujs
//= require_tree .

Sobre o detalhe do jQuery, todo novo projeto Rails tem declarado gem ‘jquery-rails’ no Gemfile.

Os arquivos application.∗ podendo “∗” ser “js”, “js.coffee”, “css”, “css.scss”, “css.sass”, “js.erb”, “css.erb”, etc. Eles são conhecidos como “Manifestos”. São arquivos “guarda-chuva” que declaram todos os outros arquivos que eles dependem, em ordem, para serem concatenados em um único arquivo ao serem compilados.

No exemplo padrão, no application.js a primeira e segunda linha com require declaram o jquery.js e depois o jquery_ujs.js e a terceira linha com require_tree . manda carregar todos os outros arquivos javascripts no mesmo diretório que, por acaso, tem o home.js criado pelo gerador de controller que usamos antes. Agora vejam novamente o HTML gerado e verá que são exatamente os javascripts carregados na ordem que expliquei, sendo o quarto o próprio conteúdo do arquivo application.js.

Normalmente usar o require_tree . não é exatamente ruim se os javascripts não dependem da ordem de carregamento, mas você provavelmente vai querer declarar explicitamente coisas como plugins de jQuery para garantir que eles estão carregados antes de poder usá-los.

Para explicar como tudo isso funciona é importante pararmos o servidor Rails que subimos antes e reexecutá-lo em modo produçao:

1
bundle exec rails s -e production

Agora, se tentarmos carregar a mesma URL http://localhost:3000/home/index no browser, receberemos um erro 500 com o seguinte backtrace:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Started GET "/home/index" for 127.0.0.1 at 2012-07-01 03:31:55 -0300
Connecting to database specified by database.yml
Processing by HomeController#index as HTML
  Rendered home/index.html.erb within layouts/application (12.0ms)
Completed 500 Internal Server Error in 155ms

ActionView::Template::Error (application.css isn't precompiled):
    2: <html>
    3: <head>
    4:   <title>NovoProjeto</title>
    5:   <%= stylesheet_link_tag    "application", :media => "all" %>
    6:   <%= javascript_include_tag "application" %>
    7:   <%= csrf_meta_tags %>
    8: </head>
  app/views/layouts/application.html.erb:5:in `_app_views_layouts_application_html_erb__408740569075721590_70099961775620'

Este é o sinal que não realizamos um passo importante que deve ser executado toda vez que você realizar uma atualização em produção: pré-compilar os assets. É o processo que lê os arquivos manifesto e realiza a concatenação dos arquivos declarados e sua minificação (utilizando a gem Uglifier). Portanto, precisamos executar o seguinte:

1
bundle exec rake assets:precompile

Lembrando que antes disso o diretório public/uploads estava originalmente vazio (e em desenvolvimento, você deve garantir que esse diretório esteja sempre vazio, já explicamos porque). Após executar a a pré-compilação, esse diretório terá os seguintes arquivos:

1
2
3
4
5
6
7
8
9
10
11
application-363316399c9b02b9eb98cd1b13517abd.js
application-363316399c9b02b9eb98cd1b13517abd.js.gz
application-7270767b2a9e9fff880aa5de378ca791.css
application-7270767b2a9e9fff880aa5de378ca791.css.gz
application.css
application.css.gz
application.js
application.js.gz
manifest.yml
rails-be8732dac73d845ac5b142c8fb5f9fb0.png
rails.png

E para entender vejamos o código-fonte do HTML gerado em produção:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html>
<head>
  <title>NovoProjeto</title>
  <link href="/uploads/application-7270767b2a9e9fff880aa5de378ca791.css" media="all" rel="stylesheet" type="text/css" />
  <script src="/uploads/application-363316399c9b02b9eb98cd1b13517abd.js" type="text/javascript"></script>
  <meta content="authenticity_token" name="csrf-param" />
<meta content="OFmZwwtshevVgcs1DUg56WVIQ8NcJZsri/nUubhEJCk=" name="csrf-token" />
</head>
<body>

<h1>Home#index</h1>
<p>Find me in app/views/home/index.html.erb</p>

</body>
</html>

Compare este HTML com o anterior que analisamos gerado em ambiente de desenvolvimento. Em vez de 1 arquivo CSS e 4 Javascripts, temos apenas 1 CSS e 1 Javascript.

Para entendermos melhor, vejamos o que tem no arquivo public/uploads/manifest.yml:

1
2
3
rails.png: rails-be8732dac73d845ac5b142c8fb5f9fb0.png
application.js: application-363316399c9b02b9eb98cd1b13517abd.js
application.css: application-7270767b2a9e9fff880aa5de378ca791.css

Ou seja, o arquivo application.js é idêntico ao application-363316399c9b02b9eb98cd1b13517abd.js. Se algum dos arquivos declarados no manifesto app/uploads/javascripts/application.js mudar, esse número sufixo irá mudar e o HTML apontará para o novo. Olhando novamente nossa lista de situações que precisam ser solucionadas, que apresentamos no início do arquivo, temos já até aqui a solução de 3 dos pontos:

  • o ponto 1 explica o problema que é sempre melhor ter apenas um único arquivo de CSS ou JS do que dezenas deles separados, pois o navegador só precisa ter o peso de pedir um único arquivo (quanto mais arquivos, independente do tamanho, mais tempo vai demorar para a página renderizar). Além disso, graças ao Uglifier teremos esses arquivos “minificados”, ou seja, reescritos de forma a minimizar seu tamanho em bytes sem modificar a lógica da programação. Além disso, o pipeline vai um passo além e tem as versões de todos esses arquivos com extensão “.gz” que significa “gzip”. Se o browser fizer uma requisição dizendo que aceita conteúdo compactado em formato zip, se o web server disser que entende zip, ele pode diretamente enviar a versão do arquivo com extensão “.gz”. No exemplo acima, isso significa enviar um JS de 34kb em vez dos 98kb descomprimidos.
  • o ponto 2 explica o problema de quanto assets mudam mas o browser guarda em cache baseado na URL que ele carregou. Se ele pedisse http://localhost:3000/uploads/application.js, mesmo se o JS fosse modificado, o browser não pediria novamente porque a boa prática diz que o web server deveria enviar cabeçalho dizendo para esse tipo de arquivo ficar em cache por 1 ano. Mas como o HTML na realidade pede por http://localhost:3000/uploads/application-363316399c9b02b9eb98cd1b13517abd.js, e se o JS mudar, esse número vai mudar também, não importa mais que esses assets fiquem indefinidamente em cache, pois da próxima vez que precisar dar versão mais nova, o nome do arquivo será completamente diferente do que estava no cache.
  • finalmente, o ponto 4 explica sobre os diferente geradores de assets. Implicitamente já podemos ver isso no caso do SASS, onde arquivos com extensão “.css.scss” são convertidos em “.css”.

Para reforçar o ponto 2, vamos adicionar a seguinte função no arquivo app/uploads/javascripts/application.js:

1
2
3
function helloWorld() {
  console.log("Hello World");
}

Agora executamos a pré-compilação novamente:

1
bundle exec rake assets:precompile

O que temos no diretório public/uploads será:

1
2
3
4
5
6
7
8
9
10
11
12
13
application-363316399c9b02b9eb98cd1b13517abd.js
application-363316399c9b02b9eb98cd1b13517abd.js.gz
application-4fee97e9e402a9816ab9b3edf7a4c08b.js
application-4fee97e9e402a9816ab9b3edf7a4c08b.js.gz
application-7270767b2a9e9fff880aa5de378ca791.css
application-7270767b2a9e9fff880aa5de378ca791.css.gz
application.css
application.css.gz
application.js
application.js.gz
manifest.yml
rails-be8732dac73d845ac5b142c8fb5f9fb0.png
rails.png

Como não limpamos o diretório antes, temos a versão antiga e a recente. Compare, a anterior se chamava application-363316399c9b02b9eb98cd1b13517abd.js e a nova com a função de demonstração se chama application-4fee97e9e402a9816ab9b3edf7a4c08b.js. Reiniciando o servidor Rails no ambiente de produção e vendo o novo HTML gerado, verá este trecho:

1
<script src="/uploads/application-4fee97e9e402a9816ab9b3edf7a4c08b.js" type="text/javascript"></script>

Espero que esse detalhamente deixe bem claro o objetivo do pipeline de pré-compilação e as convenções de nomenclatura e quais problemas ele soluciona. Num artigo que escrevi recentemente chamado Enciclopédia do Heroku explico como mover esses assets pré-compilados para uma conta na Amazon S3, com o objetivo de descarregar processamento do seu servidor de aplicação, servindo a partir de um CDN, o que deve melhorar a experiência do usuário final ao carregar assets de um servidor mais próximo.

Parte 2

O artigo ficou longo, por isso vamos continuar na Parte 2

Comments

comentários deste blog disponibilizados por Disqus