Asset Pipeline para Iniciantes

2012 July 01, 05:34 h

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

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:

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 https://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 https://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:

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

tags: learning beginner rails tutorial front-end

Comments

comentários deste blog disponibilizados por Disqus