Blog tech

Un asset pipeline pour sinatra

Rédigé par Martin Catty | 16 juillet 2013

Ruby on Rails nous offre de précieux outils par défaut pour améliorer la performance de nos applications web.

L’asset pipeline en fait partie. En suivant quelques conventions, les fichiers JavaScript et CSS sont automatiquement compressés et minifiés en production.

Sprockets

Ce travail est réalisé par la gem sprockets.

Toutefois Sprockets est une application Rack à part entière et est maintenant complètement décorrélée de Ruby on Rails.

Rien ne nous empêche donc de l’utiliser dans un autre framework basé sur Rack comme Sinatra.

Intégration de Sprockets dans Sinatra

Sinatra est le micro framework que nous utilisons pour faire tourner ce site.

Pour de petits besoins applicatifs il est parfaitement adapté.

Pour commencer nous allons charger les gems qui vont bien :

# Gemfile
gem 'sprockets'
gem 'yui-compressor'

yui-compressor va se charger de compresser le résultat en sortie.

Puis nous éditons notre fichier de rackup :

require 'sprockets'
require "yui/compressor"

map '/assets' do
  environment = Sprockets::Environment.new
  environment.append_path 'assets/javascripts'
  environment.append_path 'assets/stylesheets'

  environment.js_compressor  = YUI::JavaScriptCompressor.new
  environment.css_compressor = YUI::CssCompressor.new

  run environment
end

Ensuite il reste à créer une arborescence assets avec les sous-dossiers javascripts et stylesheets et d’y placer les manifest et les fichiers.

On pourra également créer un sous dossier vendor.

Un exemple de manifest:

//= require vendor/foo
//= require bar

C’est tout ce dont vous aurez besoin pour que vos assets soient servis compressés et minifiés.

Le problème est que cette compilation va se faire à chaud (à l’éxécution), ce qui n’est pas particulièrement performant et donc pas acceptable en production.

L’idéal serait que, comme en Rails, un fichier soit compilé au déploiement et automatiquement servi.

Compiler les assets

Commencons par définir sprockets dans le fichier de notre application, pour nous synbioz.rb :

APP_DIR = Dir.pwd

set :sprockets, (Sprockets::Environment.new(APP_DIR) { |env| env.logger = Logger.new(STDOUT) })
set :assets_prefix, 'compiled'
set :assets_path, File.join(APP_DIR, 'public', settings.assets_prefix)

configure do
  settings.sprockets.append_path 'assets/javascripts'
  settings.sprockets.append_path 'assets/stylesheets'

  settings.sprockets.js_compressor  = YUI::JavaScriptCompressor.new
  settings.sprockets.css_compressor = YUI::CssCompressor.new
end

Nous allons maintenant adapter le bloc /assets de notre rackup, qui sera utilisé uniquement en mode development.

En effet les assets ne seront plus servis en invoquant directement sprockets en production.

map '/assets' do
  run settings.sprockets
end

Dorénavant il nous faut une tâche pour compiler nos manifest. Créons cette tâche dans le Rakefile :

namespace :assets do
  desc 'compile assets'
  task :compile => [:assets_version, :compile_js, :compile_css]

  desc 'compile javascript assets'
  task :compile_js do
    compile('js')
  end

  desc 'compile css assets'
  task :compile_css do
    compile('css')
  end

  desc 'create assets version'
  task :assets_version do
    set :assets_version, Time.now.to_i
    File.open(File.join('public', 'ASSETS_VERSION'), 'w+') { |f| f.write(settings.assets_version) }
  end

  private
  def compile(filetype)
    sprockets = settings.sprockets
    asset     = sprockets["application.#{filetype}"]
    outpath   = File.join(settings.assets_path, filetype)
    outfile   = Pathname.new(outpath).join("application.#{settings.assets_version}.#{filetype}")

    FileUtils.mkdir_p outfile.dirname

    asset.write_to(outfile)
    asset.write_to("#{outfile}.gz")
  end
end

La tâche de compilation, disponible via rake assets:compile va compiler les CSS et les JS respectivement dans public/compiled/css et public/compiled/js en créant les dossiers s’ils n’existent pas.

Évidemment ces dossiers ne doivent pas être versionnés.

Nous créons également un fichier contenant la version des assets, sous forme d’un timestamp, le but étant d’éviter d’avoir toujours le même nom de fichier et que les navigateurs servent le fichier de leur cache et non le fichier à jour.

Le pipeline de Rails utilise un mécanisme de digest, ici nous utilisons un simple timestamp. Les fichiers finaux auront ce type de chemin : public/compiled/js/application.1373894136.js.gz

Helpers et déploiement

Il nous reste maintenant à mettre en place des helpers pour que le contenu soit servi directement par sprockets en development et depuis les fichiers minifiés statiques autrement.

Par souci de clarté nos helpers sont encapsulés dans un module.

module AssetHelper
  def digest
    @digest ||= File.read(File.join("public", "ASSETS_VERSION"))
  end

  def js_tag
    if ENV['RACK_ENV'] == 'development'
      '<script src="/assets/application.js" type="text/javascript"></script>'
    else
      "<script src=\"/compiled/js/application.#{digest}.js\" type=\"text/javascript\"></script>"
    end
  end

  def css_tag
    if ENV['RACK_ENV'] == 'development'
      '<link rel="stylesheet" href="/assets/application.css" type="text/css" media="all" />'
    else
      "<link rel=\"stylesheet\" href=\"/compiled/css/application.#{digest}.css\" type=\"text/css\" media=\"all\" />"
    end
  end
end

helpers AssetHelper

Un point important à prendre en compte, notamment pour vos CSS : faites attention à vos chemins.

Étant donné que public/compiled/css possède plusieurs niveau de profondeur, les chemins relatifs risquent d’être complexes à maintenir entre dev et production.

Mes images étant servies depuis /public, un chemin absolu me convient bien, autrement un helper, type asset_path en Rails, peut être pratique.

Dernière chose à ne pas oublier, la compilation des assets au déploiement :

run "cd #{release_path}; RACK_ENV=#{rack_env} bin/rake assets:compile"

Et voilà, vous n’avez plus d’excuses pour faire 15 requêtes HTTP quand 2 sont possibles depuis votre site en Sinatra.

L’équipe Synbioz.

Libres d’être ensemble.