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