Módulos no JavaScript moderno
Modularização no desenvolvimento de software é algo imprescindível.
Por menos complexidade que um sistema possua, a divisão em pequenas partes de código garante a escalabilidade da aplicação, além de muitos outros benefícios.
Quanto mais isolada uma funcionalidade for, mais fácil será a sua manutenção no futuro.
Ken Thompson definiu uma série de normas culturais e abordagens filosóficas para o desenvolvimento de software, a famosa Unix philosophy:
Escreva programas que façam apenas uma coisa, e que o façam bem feito. Escreva programas que trabalhem juntos. Escreva programas que manipulem streams de texto, pois esta é uma interface universal.
Todas linguagens de programação modernas possuem uma alternativa para se trabalhar de forma modularizada, facilitando assim o objetivo de se criar pequenos pedaços de software que cumpram apenas um objetivo de forma plenamente satisfatória.
Até a versão 5.1 da ECMAScript-262, penúltima versão do JavaScript (2011), trabalhar com módulos de forma nativa não era possível. Entenda por trabalhar com módulos de forma nativa o exemplo abaixo em Python (retirado da documentação oficial):
# fibo.py # Fibonacci numbers module def fib(n): # escreve séries Fibonacci até n a, b = 0, 1 while b < n: print b, a, b = b, a+b def fib2(n): # retorna séries Fibonacci até n result = [] a, b = 0, 1 while b < n: result.append(b) a, b = b, a+b return result
Um módulo em Python é apenas um arquivo com definições e declarações. O nome do arquivo é o nome do módulo, no exemplo acima fibo.py
, com o sufixo .py
. Para usar o módulo definido, podemos importá-lo na nossa aplicação da seguinte forma:
import fibo fibo.fib(1000) # 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987
Note como a linguagem nos fornece uma maneira fácil de definir pequenos pedaços de código. Dessa forma conseguimos isolar os mesmos, reutilizá-los e criar uma aplicação modular. No módulo do exemplo acima, temos 2 funções: fib
e fib2
. Ao importar o módulo com import fibo
temos acesso às 2 funções. No Python, ainda é possível importar pedaços de dentro de um módulo, veja o exemplo abaixo:
from fibo import fib fib(1000) # 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987
Usar o import
para pegar apenas partes de um módulo é algo realmente útil e nos permite tornar o nosso software mais enxuto, escalável e performático.
Como mencionado acima, até agora o JavaScript não nos permite tal modularização de forma nativa, ou seja, diretamente na linguagem. Sendo modularização um dos princípios básicos de desenvolvimento de software, algumas alternativas foram criadas/descobertas para o uso de módulos no JavaScript. A maneira mais simplória, utilizada em larga escala nos útimos anos é utilizar o escopo global.
var fibo = function() { ... }
Ao criar a função fibo
no escopo global, a mesma fica disponível em qualquer ponto da aplicação. Isso pode parecer uma boa ideia no começo, mas conforme a complexidade do software aumenta, mais difícil se torna manter "módulos" no escopo global. Notavelmente este modelo não é o ideal, e a comunidade JavaScript evoluiu com algumas boas soluções.
CommonJS
O CommonJS define uma API síncrona para trabalhar com módulos. Essa abordagem funciona muito bem em ambientes server-side, e um dos principais casos de uso do CommonJS é a sua utilização no Node.js. Veja um exemplo:
var path = require('path'); console.log(path.extname('index.html')); // .html
No exemplo acima a variável path
importa o módulo path
nativo do Node.js. Após o import
, é possível utilizar os métodos disponíveis no módulo path
, como o extname
. Também podemos criar nossos próprios módulos:
// foo.js module.exports = 'foo';
// app.js var foo = require('foo'); console.log(foo); // foo
Note a semelhança com o primeiro exemplo em Python. O conceito é basicamente o mesmo, porém com peculiaridades e sintaxe diferente. Para implementações server-side esta abordagem funciona perfeitamente, visto que o acesso aos módulos é algo trivial, através do sistema de arquivos. No navegador a história é bem diferente. O conceito de dividir o software em pequenas partes pode ser um inconveniente no ambiente do navegador. Imagine se cada módulo de sua aplicação precisasse ser carregado dinamicamente, isso traria um overhead desnecessário para a sua aplicação, além de ser algo inviável em termos de performance. Enquanto o futuro promissor do HTTP/2 não chega (este assunto fica para outro artigo), precisamos de um sistema de build pré-produção. Este sistema de build nos permite manter a filosofia intacta sem onerar a aplicação. No tempo de escrita deste artigo diversas ferramentas (Browserify, Webpack, Rollup) possibilitam o build de módulos CommonJS, sendo o Browserify (talvez) o mais utilizado. O Browserify nos permite criar módulos como se estivéssemos em um ambiente server-side controlado, como no Node.js.
// bar.js module.exports = 'bar';
// app.js var bar = require('bar'); console.log(bar);
A diferença é que este é o nosso código-fonte da aplicação. É preciso um processo de compilação para o total funcionamento no navegador. Esse processo de compilação pode, além de executar outras tarefas, combinar arquivos em apenas um ou mais arquivos e definir uma API para o uso no browser. No caso do Browserify, todos os módulos são mesclados em um único arquivo, e uma API para lidar com módulos através dos métodos module.exports
e require
é criada e incorporada no mesmo arquivo.
AMD
Outra solução para módulos, amplamente utilizada por desenvolvedores JavaScipt é o AMD, do inglês, Asynchronous Module Definition. Basicamente, o padrão AMD define uma API com mecanismos para definição de módulos e suas dependências de forma assíncrona. Este padrão foi desenvolvido especificamente para o ambiente dos navegadores. Várias bibliotecas implementam o padrão AMD, no exemplo a seguir estamos utilizando a biblioteca RequireJS:
// bar.js define(function () { return 'bar'; });
// app.js require(['bar'], function(bar) { console.log(bar); // bar });
Note que com AMD precisamos passar uma função de callback para fazer a definição e para declarar o módulo que queremos utilizar. Estes callbacks são da natureza padrão AMD, onde tudo é assíncrono.
Outros padrões foram criados, mas sem grande aceitação da comunidade. Podemos dizer que CommonJS e AMD são as abordagens mais eficazes de se trabalhar com módulos até os dias de hoje.
ECMAScript 2015 (ES6) Modules
Até aqui vimos alguns conceitos e filosofia de desenvolvimento de software modular, como isso se aplica em diferentes linguagens e qual é o estado atual para o desenvolvimento de módulos no JavaScript. A 6º versão da especificação, popularmente chamada de ES6 ou ES2015, define um conjunto de funcionalidades que leva o JavaScript a um outro patamar. Fortemente inspirada no Node.js e em outras linguagens, como Python, o suporte a módulos na ES6/ES2015 é uma das funcionalidades mais valiosas da nova especificação. Vejamos o mesmo exemplo feito em Python, no começo do artigo, agora utilizando a sintaxe ES6/ES2015:
function fib(n) { // escreve a série Fibonacci até n let [a, b] = [0, 1]; while (b < n) { console.log(b); [a,b] = [b, a + b]; } } function fib2(n) { // retorna series Fibonacci até n let result = []; let [a, b] = [0, 1]; while (b < n) { result.push(b); [a, b] = [b, a + b]; } return result; } export default fib; export { fib2 };
A implementação é exatamente a mesma, porém no JavaScript, precisamos explicitamente informar o que deve ser exportado pelo módulo.
Existem 2 tipos de exports, o padrão e o nomeado. O export padrão é para o caso onde temos algum valor primário (função, variável, etc), já o export nomeado é para quando queremos que o nosso módulo possa ser consumido em pedaços, diretamente pelos nomes dos valores exportados. No nosso exemplo acima, a função fib
foi definida com o export padrão, já a função fib2
é um export nomeado. Vejamos agora como importar o nosso módulo:
import fib from './fibo'; fib(1000); // 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987
Como a função fib
foi definida com o export padrão, ao importar o módulo, apenas a função fib
estará disponível. Para utilizar um export nomeado, precisamos explicitamente informar o nome do export que queremos importar, entre chaves:
import { fib2 } from './fibo'; let result = fib2(100); console.log(result); // [ 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89 ]
Neste caso, apenas a função fib2
está disponível no programa. Ainda é possível importar das 2 maneiras, utilizando valores exportados como padrão e como nomeados: ~~~language-javascript import fib, { fib2 } from './fibo'; fib(1000); let result = fib2(100); console.log(result); ~~~
Até aqui cobrimos as principais funcionalidades disponíveis para se trabalhar com módulos na ES6/ES2015, mas ainda temos outras. É possível renomear um valor nomeado que foi importado:
~~~language-javascript import { fib2 as myfib } from './fibo'; let result = myfib(100); console.log(result); [ 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89 ] ~~~
Podemos importar todos os valores nomeados diretamente para um único namespace ("objeto"):
import * as fib from './fibo'; let result = fib.fib2(100); console.log(result); [ 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89 ]
Note que isto só vale para exports nomeados, nesse caso a função primária não fica disponível, mas sim todos os valores nomeados que foram exportados.
Um export nomeado ainda possui uma alternativa mais curta:
export function fib(n) { let [a, b] = [0, 1]; while (b < n) { console.log(b); [a,b] = [b, a + b]; } }
Qualquer valor pode ser exportado dessa maneira, utilizando var
, let
,function
, e até mesmo class
:
export var foo = 'foo'; export let bar = 'bar'; export class Animal {};
Exports nomeados também podem ser agrupados:
export { fib, fib2 }; function fib() { ... } function fib2() { ... }
Assim como os imports nomeados:
import { fib, fib2 } from './fibo';
Uma técnica importante que se tornou possível com módulos ES6/ES2015 e que se popularizou com a ferramenta Rollup, foi o Tree-shaking, ou Module folding, como também pode ser chamada. Esta técnica possibilita a exclusão de código desnecessário, fazendo com que o resultado final contenha apenas o que foi explicitamente importado na aplicação. Você pode aprender mais sobre este assunto no excelente artigo Module folding: matando o código morto do Fabrício Matté aqui no blog do BrazilJS.
Loader
A especificação de módulos no JavaScript trata apenas de sua sintaxe e semântica. Atualmente só é possível utilizar os benefícios desta funcionalidade com alguma ferramenta de build, exemplificado anteriormente. Porém, o WHATWG, grupo independente responsável por criar novas especificações para o HTML, já está trabalhando em um loader que possibilitará o uso da sintaxe de módulos nos navegadores.
Conclusão
A nova especificação nos permite trabalhar de forma modular, semelhante ao modelo existente em outras linguagens, como Python. As novidadades introduzidas pela linguagem nos proporcionam uma maneira nova de se pensar em aplicações JavaScript. Ter essas funcionalidades de forma nativa faz com que possamos eliminar camadas de abstrações muitas vezes onerosas, além de deixar nossas aplicações mais robustas e manuteníveis. Outro ponto importante é a possibilidade de, em alguns casos, eliminar um passo no processo de build, deixando que o programa rode puramente no ambiente onde está sendo executado (navegador, servidor). Ainda não é possível utilizar módulos de forma nativa, mas a especificação do loader já está em desenvolvimento na maioria dos navegadores modernos.
Referências: JS Modules: http://jsmodules.io/
ExploringJS (Leitura altamente recomendada!): http://exploringjs.com/es6/ch_modules.html
ECMAScript 6 modules: the final syntax: http://www.2ality.com/2014/09/es6-modules-final.html
A new syntax for modules in ES6: http://jsrocks.org/2014/07/a-new-syntax-for-modules-in-es6/
ES6 modules today with 6to5 (now Babel): http://jsrocks.org/2014/10/es6-modules-today-with-6to5/
Imagem (Apollo Command Module) do topo por Richard Moore Imagem seção CommonJS por Geoff Collins Imagem seção Loader por me5otron Imagem seção AMD por mowinkle6 Imagem seção módulos ES6/ES2015 por Columbia GSAPP