Jean Carlo Emer

Uma nova sintaxe para módulos no ES6

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:

import asap from 'asap';
import { later } from 'asap';
import asap, { later } from 'asap';

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:

if (type == 'me') {
  this.import('me').then(function(user) {
    // do stuff here
  });
}

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.