Go to English Blog

Usando ES6 com Asset Pipeline no Ruby on Rails

Leia em 7 minutos

O JavaScript renasceu. Até pouco tempo, o desenvolvimento de novas funcionalidades era quase que inexistente. A última grande atualização tinha sido em 2009, com o lançamento do ES5. E, mesmo assim, ainda não podíamos usar todas as funcionalidades devido às incompatibilidades dos navegadores.

Para permitir que esse nível de compatibilidade aumentasse, era muito comum usar coisas como es5-shim, que verificava as funcionalidades do navegador e implementava condicionalmente os polyfills.

E por causa da estagnação do JavaScript, surgiram os primeiros pré-processadores, como o CoffeeScript. Eles interpretavam construções especiais, gerando código que os navegadores podiam entender.

Curiosamente, em 2009 Brendan Eich anunciou que iria começar a definir as especificações de uma nova versão denominada Harmony1, que posteriormente ficou conhecida como ES6. Os primeiros rascunhos dessa nova versão só começaram a ser publicados em 2011, mas a versão final da especificação só foi terminada em junho deste ano.

O ES6 possui uma série de funcionalidades úteis:

Mesmo com a evolução dos navegadores, usar as funcionalidades introduzidas pelo ES6 iria levar algum tempo, anos talvez. Felizmente, com o surgimento do Babel.js você pode usar essas funcionalidades hoje, sem se preocupar com problemas de compatibilidade.

O Babel.js2 nada mais é que um pré-processador. Você escreve o seu código usando as novas funcionalidades do JavaScript, que depois são convertidas em código que pode ser interpretado pelos navegadores, mesmo aqueles que não entendem ES6.

Usando Babel.js

Para instalar o Babel você vai precisar ter Node.js instalado.

$ npm install babel -g

Depois, basta usar o comando babel para compilar os seus arquivos de JavaScript. A melhor maneira de fazer isso é ativar o modo de escuta de modificações dos arquivos. No exemplo abaixo vamos compilar todos os arquivos presentes em src e salvá-los em dist.

$ babel --watch --out-dir=dist src

Uma das funcionalidades que mais gosto é a definição de classes, que abstrai a sintaxe de funções construtoras. No exemplo à seguir, defino uma classe User que recebe dois argumentos na inicialização do objeto.

class User {
  constructor(name, email) {
    this.name = name;
    this.email = email;
  }
}

Ao processar este arquivo com o Babel, teríamos algo como isso:

'use strict';

var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })();

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }

var User = (function () {
  function User() {
    _classCallCheck(this, User);
  }

  _createClass(User, [{
    key: 'construct',
    value: function construct(name, email) {
      this.name = name;
      this.email = email;
    }
  }]);

  return User;
})();

var user = new User('John', 'john@example.com');

console.log('name:', user.name);
console.log('email:', user.email);

Perceba como o Babel gera todo o código necessário para dar suporte à definição de classes. Embora o código gerado seja um pouco mais verboso, você pode utilizar uma opção de reutilizar essas funcionalidades.

$ babel --external-helpers --watch --out-dir=dist src

Agora, em vez de copiar o código que dá suporte para o Babel, ele irá chamar uma função no objeto babelHelpers.

'use strict';

var User = (function () {
  function User() {
    babelHelpers.classCallCheck(this, User);
  }

  babelHelpers.createClass(User, [{
    key: 'construct',
    value: function construct(name, email) {
      this.name = name;
      this.email = email;
    }
  }]);
  return User;
})();

var user = new User('John', 'john@example.com');

console.log('name:', user.name);
console.log('email:', user.email);

Aí bastaria carregar este arquivo de helpers do próprio pacote do Babel.

Usando Babel.js com Asset Pipeline

Além do próprio Babel, muita gente usa builders como Grunt ou Gulp para automatizar o processo de compilação. Porém, se você usa Ruby on Rails você provavelmente irá usar o próprio Asset Pipeline para gerenciar os seus arquivos de front-end.

Infelizmente, ainda não existe suporte nativo para ES6 na última versão estável do Sprockets. Se você não se incomoda de viver perigosamente, pode usar o branch master do repositório.

Primeiro você vai precisar definir as dependências. Seu arquivo Gemfile deve ser algo assim:

source 'https://rubygems.org'

gem 'rails', '4.2.4'
gem 'sqlite3'
gem 'uglifier', '>= 1.3.0'

gem 'sass-rails', github: 'rails/sass-rails', branch: 'master'
gem 'sprockets-rails', github: 'rails/sprockets-rails', branch: 'master'
gem 'sprockets', github: 'rails/sprockets', branch: 'master'
gem 'babel-transpiler'

gem 'turbolinks'
gem 'jquery-rails'

Pronto! Agora todos os arquivos com extensão .es6 serão compilados usando o Babel.

Crie um arquivo em app/assets/javascripts/hello.es6 com o seguinte conteúdo:

class Hello {
  constructor() {
    alert('Hello!');
  }
}

new Hello();

Certifique-se que o arquivo app/assets/javascripts/application.js esteja carregando o arquivo hello.es6.

//= require_tree .
//= require_self

Ao acessar a página você deve receber uma alert com a mensagem Hello.

Alert box - Hello

Se você pretende usar módulos, tem que configurar algumas outras coisas.

Usando módulos ES6

O ES6 introduziu suporte para módulos. Em vez de definir todo o seu código no escopo global, você pode usar módulos que possui um contexto próprio por arquivo. A sintaxe é bem simples:

import Foo from 'foo';

Isso importará apenas Foo do arquivo foo.js. Um arquivo de JavaScript pode exportar diversos módulos, e você não precisa carregar todos eles; você pode carregar apenas o que realmente precisa, de uma forma bem parecida como o Python faz.

O Babel não tem ideia de como você quer exportar esses módulos e, por padrão, irá gerar no formato do CommonJS. Este formato não é entendido pelo navegador, então vamos precisar de uma outra alternativa.

O modo mais simples é usando AMD e, para isso, vamos usar o almond. Use o gerenciador de dependências de front-end que você preferir; neste exemplo irei utilizar o http://rails-assets.org. Seu arquivo Gemfile deve ficar da seguinte maneira:

source 'https://rubygems.org'

gem 'rails', '4.2.4'
gem 'sqlite3'
gem 'uglifier', '>= 1.3.0'

gem 'sass-rails', github: 'rails/sass-rails', branch: 'master'
gem 'sprockets-rails', github: 'rails/sprockets-rails', branch: 'master'
gem 'sprockets', github: 'rails/sprockets', branch: 'master'
gem 'babel-transpiler'

gem 'turbolinks'
gem 'jquery-rails'

source 'https://rails-assets.org' do
  gem 'rails-assets-almond'
end

Você também vai precisar configurar o Babel; crie o arquivo config/initializers/babel.rb com o seguinte conteúdo:

Rails.application.config.assets.configure do |env|
  babel = Sprockets::BabelProcessor.new(
    'modules'    => 'amd',
    'moduleIds'  => true
  )
  env.register_transformer 'application/ecmascript-6', 'application/javascript', babel
end

Finalmente, altere seu arquivo app/assets/javascripts/application.js para que ele carregue o almond.

//= require almond
//= require jquery
//= require turbolinks
//= require_tree .
//= require_self

Aqui você precisará pensar em como você quer executar os seus arquivos. Vai usar algum script para disparar as rotas? Vai carregar um script por view? Vai criar o seu próprio mecanismo de execução? Como a resposta depende basicamente do modo como você trabalha, não vou entrar em detalhes. Por isso, apenas adicione o código de inicialização ao final do arquivo.

//= require almond
//= require jquery
//= require turbolinks
//= require_tree .
//= require_self

require(['application/boot']);

O arquivo de boot deve ser criado em app/assets/javascripts/application/boot.es6. Vou simplesmente ouvir os eventos de inicialização do jQuery e Turbolinks, executando um arquivo baseado no nome do controller e da action.

import $ from 'jquery';

function runner() {
  // All scripts must live in app/assets/javascripts/application/pages/**/*.es6.
  var path = $('body').data('route');

  // Load script for this page.
  // We should use System.import, but it's not worth the trouble, so
  // let's use almond's require instead.
  try {
    require([path], onload, null, true);
  } catch (error) {
    handleError(error);
  }
}

function onload(Page) {
  // Instantiate the page, passing <body> as the root element.
  var page = new Page($(document.body));

  // Set up page and run scripts for it.
  if (page.setup) {
    page.setup();
  }

  page.run();
}

// Handles exception.
function handleError(error) {
  if (error.message.match(/undefined missing/)) {
    console.warn('missing module:', error.message.split(' ').pop());
  } else {
    throw error;
  }
}

$(window)
  .ready(runner)
  .on('page:load', runner);

Esse script precisa da propriedade data-route no elemento <body>. Você pode adicionar algo como o código à seguir ao arquivo de layout (e.g. app/views/layouts/application.html.erb):

<body data-route="application/pages/<%= controller.controller_name %>/<%= controller.action_name %>">

Agora, vamos criar o código que será executado para uma determinada página. Imagine que o nome do controller é site e sua action é home; você precisará criar o arquivo app/assets/javascripts/application/pages/site/home.es6.

export default class Home {
  constructor(root) {
    this.root = root;
  }

  setup() {
    // add event listeners
    console.log('-> setting up listeners and whatnot');
  }

  run() {
    // trigger initial action (e.g. perform http requests)
    console.log('-> perform initial actions');
  }
}

Veja como estamos exportando a classe Home como sendo o módulo padrão. O método Home#constructor será executado na instanciação do objeto, que neste caso receberá o <body> como sendo o elemento raíz. Finalmente, temos os métodos de configuração (Home#setup) e execução (Home#run).

A definição de classes é uma das partes que mais gosto do ES6. Compare a definição desta classe com o modo clássico de definição de função construtora:

function Home(root) {
  this.root = root;
}

Home.prototype.setup = function() {
  // add listeners
};

Home.prototype.run = function() {
  // initial execution
};

Embora sejam muito parecidos, a utilização da palavra class permite que você escreva um código mais próximo do que usamos em outras linguagens. Esta mesma classe teria a seguinte aparência no Ruby:

class Home
  def initialize(root)
    @root = root
  end

  def setup
  end

  def run
  end
end

O ES6 possui muitas outras coisas legais. Para saber mais sobre a nova versão do JavaScript, não deixe de ler o livro Exploring ES6, disponível na íntegra gratuitamente para leitura online.

Finalizando

Usar ES6 já é uma opção viável. Com Babel, você pode contar com todas as novas funcionalidades, sem ter que se preocupar com compatibilidade entre navegadores.

Poder contar com o Asset Pipeline para fazer isso, em vez de recorrer a build tools como Gulp e Grunt torna a coisa ainda mais fácil, mesmo para quem não conhece muito sobre o ecossistema Node.js.

Você pode ver este artigo aplicado em um repositório do Github.


  1. O ES6 também é conhecido como ES.Next

  2. Este projeto se chamava 6to5 antes de ser renomeado para Babel.js. Saiba mais.