O grupo TC39 - ECMAScript já está finalizando a sexta versão da especificação do ECMAScript. A agenda do grupo aponta o mês de junho do próximo ano como sendo a data de lançamento. A partir de agora, poucas mudanças significativas devem surgir. Já é tempo de se aprofundar no estudo.
Este artigo não pretende abordar a importância da escrita de código modularizado. Já escrevi sobre o assunto no artigo Modularização em JavaScript. Sites como JavaScript Modules entre outros já são uma ótima referência sobre como escrever módulos ES6. A intenção aqui é esclarecer e justificar a necessidade de uma nova sintaxe para escrita de módulos.
Formatos atuais
Os mais famosos formatos de definição de módulos até então eram o AMD, padrão para bibliotecas client-side e CommonJS, adotado pelo Node.js e levado para navegadores pelo Browserify. Cada um possui características determinadas pelo ecossistema em que são utilizados. A exemplo, o AMD encapsula cada módulo no interior de uma função definindo escopo e permitindo carregamento assíncrono de suas dependências nos navegadores. Por outro lado, os módulos CommonJS implicitamente definem a criação de um escopo de módulo o que inviabiliza seu uso diretamente em navegadores.
A escolha de um formato
As bibliotecas são as que mais sofrem com a existência de diferentes formatos. A inconsistência pode ser normalizada com uma abstração que encapsula os módulos e os torna funcionais em mais de um formato. O projeto Universal Module Definition (UMD) guarda uma coleção destas abstrações.
Acompanhando a evolução e observando o surgimento desta unificação, o problema de modularização parece resolvido. Engano. O projeto UMD guarda mais de dez variações de abstrações e todas desviam o código do módulo do seu objetivo: resolver o problema que é responsável. Observe o exemplo fictício do módulo UMD add2
que depende de add
:
(function (factory) {
if (typeof define === 'function' && define.amd) {
define(['add'], factory);
} else if (typeof exports === 'object') {
module.exports = factory(require('add'));
}
}(function (add) {
return function (param) {
return add(2, param);
};
}));
Seguir escrevendo código para dois formatos (ou mais) seguindo o UMD não é uma boa opção. Por que não jogar dois ou um entre os membros do TC39 e escolher um único formato? Melhor analisar cada um dos formatos e identificar qual é mais poderoso em termos de expressividade.
Analisando os formatos atuais
No formato AMD, encapsular o código do módulo em uma função trata-se de um contra tempo que não traz ganho algum em expressividade. A função faz parte de outro universo de resolução de problemas. Uma nova especificação poderia muito bem considerar que cada arquivo de módulo já possui seu próprio escopo, lembre-se que é uma nova versão da linguagem. Não nos restaria nenhuma razão para adotar AMD.
Os CommonJS Modules são mais expressivos. Trata-se de uma grande vantagem deixar de lado o encapsulamento através de funções e ainda poder indicar qual porção de código da dependência será utilizado já na sua importação var debug = require('util').debug;
ou até já utilizar o código require('util').debug('message on stderr');
.
Seguiremos considerando os módulos CommonJS e apontando quais seus pontos fracos que levaram gradativamente a adoção de uma nova sintaxe.
Requisição de dependências (imports)
Os módulos CommonJS foram concebidos para requisitar as dependências sincronamente. A execução do script é bloqueada enquanto uma dependência é carregada. Novamente, esta abordagem não traz nenhum inconveniente para o Node.js que possui um acesso rápido ao sistema de arquivos.
Considerando evoluções futuras nos protocolos de redes e mesmo se pensarmos nos dias atuais, um formato de módulo adequado para navegadores precisa operar com carregamento assíncrono das dependências. Para isto, os módulos precisam ser analisados estaticamente a título de identificar suas dependências antes de serem executados. Assim é possível fazer o download simultâneo das dependências e condicionar a execução do módulo para quando as dependências estiverem prontas.
Os formatos de módulos que dispomos não permitem análise estática. Pegando como exemplo o formato CommonJS, o require
trata-se de uma função que aceita um identificador de módulo. Assim como qualquer outra função, seu argumento pode ser calculado de diferentes maneiras. Analise o código a seguir que também sofre a influência do controle de fluxo:
if (type == 'me') {
var user = require('me');
} else {
var user = require('module' + Math.random());
}
Espero que isto já sirva para atestar como não é possível identificar as dependências nestes formatos sem que o código seja executado. Ferramentas como o Browserify já não convertem módulos que tenham dependências dinâmicas causando uma certa confusão. Apenas com uma sintaxe específica é possível coibir declarações de dependências como estas.
Os módulos ES6 trazem consigo toda a flexibilidade de declaração de dependências dos módulos CommonJS permitindo a análise estática do código:
Como apontado em um comentário do Yehuda Katz, não são permitidos códigos como este if (type == 'me') { import user from 'me'; }
. Entretanto, a especificação não deixa de fora a possibilidade de executar requisições dinâmicas utilizando promessas:
Exportando código (exports)
O formato CommonJS permite exportar código através de propriedades no objeto contido na variável exports
. O retorno de um módulo é um objeto com propriedades. Uma variação na implementação do Node.js possibilita que módulos retornem por padrão outros tipos de valores, observe o módulo foo
:
module.exports = exports = function defaultFn() {
return 'default';
};
exports.another = function () { return 'another'; };
O código acima permite executar require('foo')()
e require('foo').another()
. O efeito colateral desta abordagem é adicionar propriedades diretamente na função defaultFn
.
Utilizando a nova sintaxe, é possível declarar um retorno padrão. Os demais valores exportados não serão mais atribuídos na forma de propriedades na função defaultFn
. Veja o mesmo exemplo transcrito:
export default function defaultFn() {
return 'default';
};
export function another() { return 'another'; };
Palavras finais
A especificação do ES6 também abrange a definição de um loader responsável por requisições assíncronas que ainda permite utilizar módulos em diferentes formatos. O assunto está fora do escopo deste artigo. A seção The Compilation Pipeline do artigo ES6 Modules de Yehuda Katz apresenta muito bem as possibilidades.
Espero que tenha convencido você da superioridade da nova sintaxe em relação a outros formatos de módulos. Claro que sintaxes trazem consigo o ônus do seu aprendizado. Mas neste caso, permitem ganho de expressividade e aumento das possibilidades.
A nova sintaxe de módulos leva em consideração com maestria todos os diferentes ambientes em que a linguagem é utilizada: web server, desktop, linha de comando e navegadores. Os módulos alteram significativamente o funcionamento da linguagem e são sem dúvida o melhor da nova especificação.