Asset Pipeline para Iniciantes - Parte 2

2012 July 01, 23:35 h - tags: tutorial front-end rails learning

Não perca o início deste artigo, na Parte 1

Ambientes de Desenvolvimento e Produção

Agora entenda uma regra básica:

  • toda URL que você passar ao Rails é analisada pelo sistema de roteamento, que você configura em config/routes.rb
  • a exceção é tudo que estiver no diretório public, graças ao middleware Rack::Static.

Ou seja, se você pedir por http://localhost:3000/uploads/application.js isso nem vai passar pelo Rails porque, como vimos na listagem do diretório public/uploads acima, esse arquivo existe lá e, portanto, será servido diretamente.

Por isso a recomendação, depois do último exercício com pré-compilação é apagar tudo:

1
rm -Rf public/uploads

Desta forma, quando você subir o servidor Rails no ambiente padrão de desenvolvimento, a mesma URL irá passar pelo Rails, pelo Asset Pipeline, e processará o arquivo app/uploads/javascripts/application.js. Assim, toda vez que fizer modificações e recarregar a página no navegador, você verá as mudanças imediatamente. Se estivesse précompilado, veria a versão antiga, sem as modificações novas.

A compilação em tempo real só acontece em desenvolvimento pois, obviamente, é um processo lento. Em produção, ninguém em sã consciência vai alterar nenhum arquivo direto em servidor de produção, portanto é importante ter tudo précompilado em public/uploads para economizar o aplicativo Rails do trabalho.

Outra coisa importante, os arquivos de assets que se chamam “application”, são automaticamente compilados. Mas digamos que você tem um stylesheet que não vai carregar no layout principal, mas só em algumas outra seções, digamos que se chame special.css, ou seja, precisamos encontrá-lo em http://localhost:3000/uploads/special.css, criamos nesta estrutura:

1
2
3
4
app
  assets
    stylesheets
      special.css

Porém, se reexecutarmos o rake task assets:precompile novamente, verá que esse arquivo não aparece em public/uploads. Recapitulando, arquivos “application” são automaticamente usados na précompilação. Todo asset declarado nesses manifestos, será usado dentro dos arquivos minificados “application”. Agora, para adicionar arquivos extras, precisamos declará-los manualmente em config/application.rb, adicionado uma linha assim:

1
config.assets.precompile += %w(special.css)

Colocar tudo no “application” e carregar esse arquivo no layout principal para que toda página tenha tudo? Ou separar CSS e JSS que é necessário seção a seção do site? Não tenho uma regra rígida. Se a aplicação não for tão grande assim, provavelmente centralizar tudo em “application” é a saída mais simples de gerenciar. Porém se a aplicação for ou muito grande, ou se determinada seção é relativamente mais pesada em JS que as demais, por exemplo, talvez valha a pena gerenciar assets em separado, usando a configuração acima.

Agora sim, se executar a task de precompile novamente, teremos algo como:

1
2
3
4
special-f7bf76f875ce5c9edd0075eaea3f6140.css
special-f7bf76f875ce5c9edd0075eaea3f6140.css.gz
special.css
special.css.gz

Compass e SASS

Agora, por que adicionamos o Compass? Não vou fazer desta seção um tutorial de Compass, mas para que entendem o básico, podemos adicionar o seguinte CSS de exemplo em app/uploads/stylesheets/application.css.scss:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@import "compass";

.box {
  font {
    family: Lucida Grande;
    size: 12px;
  }
  width: 400px;
  padding: 15px;
  border: 1px solid black;
  @include text-shadow(1px 0 1px opacify(#5c5c5c, .37));
  @include box-orient(horizontal);
  @include border-radius(20px, 20px);
}

Na maior parte parece um CSS normal, mas a diferença maior está nos comandos @include do Sass, que serve para carregar “Mixins”. Pense em Mixin no Sass como “funções” que retornam CSS parametrizável e reusável. O Compass é uma biblioteca de Mixins que encapsulam alguns dos aspectos mais comuns de CSS. No exemplo, estamos chamando os mixins text-shadow e border-radius, cuja função deve ser bastante óbvia. Vamos ver o CSS que isso gera:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* line 3, ../../app/uploads/stylesheets/application.css.scss */
.box {
  width: 400px;
  padding: 15px;
  border: 1px solid black;
  text-shadow: 1px 0 1px #5c5c5c;
  -webkit-box-orient: horizontal;
  -moz-box-orient: horizontal;
  -ms-box-orient: horizontal;
  box-orient: horizontal;
  -webkit-border-radius: 20px 20px;
  -moz-border-radius: 20px / 20px;
  -ms-border-radius: 20px / 20px;
  -o-border-radius: 20px / 20px;
  border-radius: 20px / 20px;
}
/* line 4, ../../app/uploads/stylesheets/application.css.scss */
.box font {
  family: Lucida Grande;
  size: 12px;
}

Fica claro a função de “nesting”, para herdar estilos de um elemento pai, que usamos no elemento “font” para não repetir em blocos separados, como no CSS final. O “text-shadow” foi simples, mas o “box-orient” e border-radius" automaticamente adicionaram todas as diretivas específicas de cada browser, coisas como “-webkit”, “-moz”, etc.

Veja a documentação de referência do Compass para encontrar todos os mixins e exemplos detalhados de como usá-los.

Isso é interessante, mas a principal função, para fechar todas as situações que descrevi na introdução do artigo, é a que se segue.

Primeiro, vamos adicionar duas novas imagens na pasta app/images/social-icons:

1
2
3
4
5
6
app
  images
    social-icons
      facebook.png
      linkedin.png
      twitter.png

Agora, vamos adicionar o seguinte HTML em app/views/home/index.html.erb:

1
2
3
4
5
6
...
<ol class="social">
  <li class="twitter"><a href="http://www.twitter.com">Twitter</a></li>
  <li class="facebook"><a href="http://www.facebook.com">Facebook</a></li>
  <li class="linkedin"><a href="http://www.linkedin.com">LinkedIn</a></li>
</ol>

Nada demais, apenas o objetivo de adicionar ícones de redes sociais com links a eles. Para estilizá-los, vamos completar nosso SCSS assim:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@import "compass";
@import "social-icons/*.png";

...

ol.social {
  @include horizontal-list;
  @each $network in twitter, facebook, linkedin {
    li.#{$network} a {
      @include social-icons_sprite(#{$network})
    }
  }
  a {
    height: 32px;
    width: 32px;
    display: block;
    text-indent: -9000px;
    color: #FFF;
  }
}

Novamente, estude SASS para entender essa sintaxe e também note que novamente usamos um mixin do Compass chamado horizontal-list. Lembre que colocamos 3 novas imagens, listados acima. Agora neste SCSS, na segunda linha, fazemos um @import de todos esses ícones.

Agora localize esta linha: @include social-icons_sprite(#{$network}). O nome da pasta, com o sufixo “_sprite” se torna um mixin, que recebe como parâmetro o nome da imagem/sprite. O que significa isso no CSS gerado ao final? Vejamos:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* line 58, social-icons/*.png */
.social-icons-sprite, ol.social li.twitter a, ol.social li.facebook a, ol.social li.linkedin a {
  background: url(/uploads/social-icons-s25bc94da3e.png) no-repeat;
}
...
/* line 20, ../../app/uploads/stylesheets/application.css.scss */
ol.social li.twitter a {
  background-position: 0 -32px;
}
/* line 20, ../../app/uploads/stylesheets/application.css.scss */
ol.social li.facebook a {
  background-position: 0 -64px;
}
/* line 20, ../../app/uploads/stylesheets/application.css.scss */
ol.social li.linkedin a {
  background-position: 0 0;
}
...

Aqui vemos o que aconteceu: as 3 imagens separadas foram concatenadas numa única, declarada no topo do CSS, chamada neste exemplo de /uploads/social-icons-s25bc94da3e.png. Agora, cada uma das classes de rede social que criamos tem um reposicionamento da mesma imagem, como: background-position: 0 -64px;.

Isto resolve o ponto 2 da introdução do artigo, que estava pendente: gerenciamento de sprites. Dependendo da quantidade de pequenas imagens que uma única página do seu site tem, somente este truque pode causar um impacto positivo que seus usuários irão notar rapidamente por causa do aumento na velocidade de renderização.

Para complementar, sempre que usarmos chamadas como background: url(/uploads/social-icons-s25bc94da3e.png) no CSS, nunca devemos digitar a URL absoluta manualmente. Devemos deixar o Asset Pipeline cuidar disso, como ele fez aqui no caso dos sprites.

Vejamos um exemplo mais concreto. Vamos adicionar o seguinte ao nosso application.css.scss:

1
2
3
4
5
h1 {
  padding-left: 60px;
  height: 70px;
  background: url("/uploads/rails.png") no-repeat;
}

O que confunde é que isso de fato funciona. Porém, caímos nos problema mencionados no início do artigo: sem o número timestamp, se atualizarmos a imagem, os usuários ficarão travados na versão antiga em cache local. Se quisermos migrar para CDN vamos ter problemas de mudar todas essas URLs manualmente, etc. Portanto, no caso de SASS o correto é usar a função image-url e fazer desta forma:

1
  background: image-url("rails.png") no-repeat;

Isto irá gerar a URL correta. Temos as seguinte variações:

1
2
3
4
image-url("rails.png")         # url(/uploads/rails.png)
image-path("rails.png")        # "/uploads/rails.png".
asset-url("rails.png", image)  # url(/uploads/rails.png)
asset-path("rails.png", image) # "/uploads/rails.png"

Agora, se for necessário URLs de assets dentro do Javascript, não há equivalente no application.js puro, por isso precisaríamos renomeá-lo para application.js.erb e então a mesma regra que usaríamos em views HTML ERB normais valem:

1
2
3
4
5
6
var imagem     = "<%= image_path("rails.png") %>";
var tag_imagem = "<%= image_tag("rails.png") %>";
var audio      = "<%= audio_path("rails.mp3") %>";
var tag_audio  = "<%= image_tag("rails.mp3") %>";
var video      = "<%= video_path("rails.m4v") %>";
var tag_video  = "<%= image_tag("rails.m4v") %>";

Vejam a documentação do ActionView::Helpers::AssetTagHelper para entender melhor sobre estes helpers, mas o importante é: se estiver escrevendo a URL de um asset manualmente, como um string, você está fazendo errado.

NGINX

Não vou entrar em detalhes sobre como configurar um NGINX completo, mas fica um lembrete para não esquecer de adicionar à configuração do servidor o seguinte trecho:

1
2
3
4
5
6
7
location ~ ^/uploads/ {
  expires 1y;
  add_header Cache-Control public;
  add_header Last-Modified "";
  add_header ETag "";
  break;
}

Isso garantirá que o browser do usuário guarde todos os assets no seu cache local para não pedir novamente. Com o Asset Pipeline, como explicamos exaustivamente acima, assets atualizados são imunes ao cache local.

E para diminuir ainda mais a quantidade de bits transportado entre o servidor e o browser dos usuários, lembre de checar se o suporte a gzip está ativado:

1
2
3
4
5
6
7
8
9
10
11
12
13
##
# Gzip Settings
##

gzip on;
gzip_disable "msie6";

# gzip_vary on;
# gzip_proxied any;
# gzip_comp_level 6;
# gzip_buffers 16 8k;
# gzip_http_version 1.1;
# gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;

Conclusão

Como podem ver, o Asset Pipeline é bastante simples para a grande maioria dos casos de uso. As regras são diferentes mas simples:

  • Assets devem ir todas em app/uploads
  • Em desenvolvimento public/uploads deve ficar vazio. Em produção, sempre execute rake assets:precompile
  • Não digite URL de assets manualmente, use os helpers de URL

Isso lhe dará, “de graça”:

  • concatenação de arquivos de assets, minificação de javascript e stylesheets
  • compatibilidade com caches, CDNs e suporte a gzip
  • gerenciamento de sprites numa única imagem com posicionamento via CSS
  • suporte a diferentes geradores de templates, como SASS

Expanda seus conhecimentos, aprenda mais sobre:

Esqueçam rumores, opiniões contrárias, rants e trolls. O Asset Pipeline certamente não é simples. Porém está longe do bicho de sete cabeças que costumam descrever. No início haviam muitos bugs, que já foram corrigidos e, toda vez que alguém falar em “Asset Poopline”, normalmente é problema de de BIOS mais do que de ferramenta. Google e Stackoverflow, for the help.

Comments

comentários deste blog disponibilizados por Disqus