Gabriel / Ramos

⟨ pintor de pixel ⟩

  • Blog
  • Módulos em JavaScript

Módulos em JavaScript

Um breve passeio sobre a história e a utilização de módulos na linguagem

10 min. de leitura

Foto por Glen Carrie

Se você trabalha com JavaScript (ou qualquer outra linguagem), muito provavelmente você já ouviu falar no termo módulo.

Podemos entender como módulo um pedaço de código qualquer, representado de forma isolada do restante de uma aplicação. Portanto, podemos também pensar que uma aplicação é o agrupamento de vários módulos diferentes com um objetivo em comum: seja uma simples função, uma classe ou um arquivo com variáveis de configuração.

Outra maneira de pensar em módulos é imaginar funcionalidades. Tanto as operações de cálculo em uma calculadora e até unidades que representam interface, como nossos botões e campos de formulário podem ser considerados módulos separados, sejam eles de códigos e funcionalidades abstratas ou de representações visuais.

#À moda antiga

Hoje em dia temos técnicas e formas um pouco mais rebuscadas de lidar com nosso código em JavaScript. Entretanto, isso nem sempre foi assim.

Antes da era dos agrupadores de módulos (ou module bundlers) como Webpack e da explosão dos frameworks e ferramentas (época do saudoso jQuery) a forma de modularizar trechos de código era um pouco diferente.

Uma delas era simplesmente escrever os módulos em arquivos diferentes e carregá-los na página necessária, mantendo a devida ordem.

Por exemplo, vamos imaginar que a aplicação calculadora é composta por 5 módulos, 4 módulos contendo as operações básicas (soma, subtração, multiplicação e divisão) e um módulo chamado calculadora, que agrupa essas operações e inicia a aplicação da nossa página.

Na página, precisaríamos carregar os arquivos mais ou menos assim:

<script src="operacoes/soma.js"></script>
<script src="operacoes/subtracao.js"></script>
<script src="operacoes/multiplicacao.js"></script>
<script src="operacoes/divisao.js"></script>
<script src="app.js"></script>

Isso funciona perfeitamente bem. No entanto, a ordem é muito importante nesse cenário. Já que o arquivo app.js é responsável por utilizar as funções presentes no arquivo soma.js, subtracao.js, multiplicacao.js, divisao.js, é necessário que as funções e variáveis exportadas por esses arquivos sejam carregadas antes do próprio arquivo da aplicação.

Poderíamos até dar um passo a frente utilizar um padrão chamado Revealing Module Pattern, onde definimos uma IIFE (Immediately Invoked Function Expression ou uma Expressão de Função com Inovação Imediata) onde podemos declarar variáveis locais e "exportar" somente o que precisarmos utilizar de forma "pública".

var soma = (function() {
  // declara variaveis privadas
  var variavelPrivada;
  var outraVariavelPrivada;

  // declara operacao
  var operacao = function() {};

  // retorna e "exporta" operacao
  return operacao;
})();

#AMD

AMD é a sigla para Definição de Módulos Assíncronos (ou Asynchronous Module Definition) e você pode checar sua API no repositório do GitHub e é um padrão que segue uma estrutura mais ou menos assim:

  • Você cria uma configuração para os módulos da sua aplicação e indica o caminho dos arquivos físicos:
config({
  baseUrl: 'js/modules',
  paths: {
    soma: 'operacoes/soma',
    subtracao: 'operacoes/subtracao',
    multiplicacao: 'opercoes/multiplicacao',
    multiplicacao: 'opercoes/multiplicacao'
  }
});
  • você define os módulos separados utilizando, via de regra, uma função chamada define, que pode receber 3 argumentos, sendo eles um nome (opcional) para o módulo, um array de dependências (também opcional) que o módulo necessita e uma função que definirá o módulo e será executada recebendo as dependências informadas como parâmetro:
define('calculadora', [ // nome
  'soma',
  'subtracao',
  'multiplicacao',
  'divisao'
], function(soma, subtracao, multiplicacao, divisao) { // dependências
  // função
  // executa e define código com as operações
});
  • por fim, esse módulo pode retornar o que for necessário "exportar", vamos imaginar que o módulo calculadora exporta um objeto com uma função init:
define('calculadora', [ // nome
  'soma',
  'subtracao',
  'multiplicacao',
  'divisao'
], function(soma, subtracao, multiplicacao, divisao) { // dependências
  // função
  var calculadora = {
    init() {
      // executa e define código com as operações
    }
  };

  // "exporta" o objeto calculadora
  return calculadora;
});

Bibliotecas como RequireJS aplicam o carregamento de módulos utilizando o padrão AMD. Se quiser você pode brincar um pouco com elas fazendo algum teste no seu navegador e até mesmo via NodeJS.


#CommonJS

Uma outra forma de escrever módulos que é popular até os dias de hoje é o padrão CommonJS (ou CJS, em siglas). Na verdade, CommonJS é o nome do grupo por trás da especificação, mas podemos dizer que é o nome do padrão já que é o adotado mais comumente.

Se você já utilizo Node certeza já deve ter visto esse padrão e você pode ver sua API na página oficial da documentação e também na Wiki do site CommonJS (que é acessível através da página principal do grupo CommonJS)

Esse padrão segue utiliza a função require para importar módulos e exports ou module.exports para exportar módulos.

Dessa forma, nossa calculadora seria algo como:

var soma = require('operacoes/soma');
var subtracao = require('operacoes/subtracao');
var multiplicacao = require('operacoes/multiplicacao');
var divisao = require('operacoes/divisao');

function calculadora() {
  // executa trecho de código qualquer
}

module.exports = calculadora;

E dentro dos arquivos das operações, poderíamos exportar da seguinte maneira:

function soma() {
  // define a função soma
}

module.exports = soma;

O que faria com que o valor exportado do módulo diretamente fosse uma função.

Também podemos exportar diretamente um objeto com a função soma, poderíamos fazer isso da seguinte forma:

exports.soma = function() {
  // define a função soma
};

Funcionaria perfeitamente, mas dessa forma, precisamos ajustar a maneira como executamos a função, já que agora exportamos um objeto com a propriedade soma:

var operacao = require('operacao/soma'); // retorna { soma: Function }
operacao.soma(); // nossa função

Uma ferramenta bem bacana que ficou conhecida por aplicar esse padrão e também segue a linha dos empacotadores de módulo) é o Browserify.

#UMD

UMD é a sigla para Definição de Módulo Universal (ou Universal Module Definition) é outra maneira de trabalhar com módulos e você pode checar a API no repositório do GitHub.

A ideia por trás do UMD é prover uma forma de importar/exportar módulos que funcionasse tanto em ambientes com AMD quanto com CommonJS.

À primeira vista pode ser um padrão um pouco estranho de ver, mas o que ele faz é, basicamente, verificar se as funções existentes no ambiente suportam AMD ou CommonJS e adapta a utilização dos módulos para cada caso

// recebe como parâmetros
// root (objeto raíz) e factory (função que criará o módulo)
(function(root, factory) {
  if (typeof define === 'function' && define.amd) {
    // verifica se a função define (AMD) existe para utilização
    define([], factory);
  } else if (typeof module === 'object' && module.exports) {
    // verifica se existe module.exports (CJS)  para utilização
    module.exports = factory();
  } else {
    // insere o módulo no objeto raíz (possivelmente window)
    root.returnExports = factory();
  }
})(typeof self !== 'undefined' ? self : this, function() {
  // execução da IIFE passando o objeto raíz
  // função "factory" indicada acima
  // apenas retorna um objeto como exemplo de um módulo
  return {};
});

Eu sei, eu sei... É um trecho de código bem chatinho e não é nada prático ficar escrevendo isso, principalmente se compararmos com os padrões do AMD e CommonJS. No entando, algumas ferramentas podem ser utilizadas para facilitar a criação desse arquivo UMD. Porém, ainda assim não era algo tão prático.

#ESModules

Também conhecido como ESM ou ECMAScript Modules, é o padrão adotado pelo TC-39 (comitê responsável pela especificação ECMAScript, que compõe a linguagem JavaScript) é a forma padronizada de se trabalhar com módulos na linguagem. Podemos verificar sua documentação na página oficial. A documentação da MDN e do Node também são referências muito boas e atualizadas.

Esse padrão já havia sido adotado por diversas ferramentas, principalmente pelos agrupadores de módulo como o Webpack que comentamos no início do post e foi oficialmente implementado no JS recentemente.

Para trabalhar com esse padrão, temos uma variação de escrita utilizando import e export nos nossos módulos.

Podemos exportar nossos módulos utilizando export através de seus nomes (ou named exports) ou valores padrão (default) da seguinte forma:

// arquivo soma.js
// export nomeado como "valor"
export var valor = 1;
// export nomeado como "soma"
export function soma() {}

// export padrão
export default {
  outroValor: 2
};

E, para importá-los, basta utilizar import:

// importa valorDefault, valor1 e soma
import valorDefault, { valor1, soma } from './soma';

console.log(valorDefault); // { outroValor: 2 }
console.log(valor1); // 1
console.log(soma); // function

Para valores exportados como padrão, podemos atribuir um novo nome ao importá-lo e não utilizamos as chaves:

import valorDefaultRenomeado from './soma';

console.log(valorDefault); // { outroValor: 2 }

Já para exports nomeados, precisamos utilizar as chaves e importar exatamente os mesmos nomes exportados. Caso precise renomear esses valores, podemos utilizar a palavra chave as para fazer esse trabalho:

// importa valor1 e importa soma renomeando para operacao
import { valor1, soma as operacao } from './soma';

console.log(valo1); // 1
console.log(operacao); // Function

Podemos, inclusive, utilizar módulos para exportar valores de outros módulos:

// arquivo-qualquer.js

// exporta soma renomeado para operacaoSoma
export { soma as operacaoSoma } from './soma';

// exporta o valor padrão renomeando para valorPadrao
export { default as valorPadrao } from './soma';

Com isso, podemos até fazer um paralelo com o que vimos de CommonJS e perceber que:

  • export default é similar ao module.exports
  • export var nome são similares ao exports.nome

#Node

NodeJS só suportava CommonJS nativamente em suas versões anteriores. Vamos imaginar esses dois arquivos:

Hoje em dia é possível trabalhar com ESModules de duas formas:

  • criando arquivos com extensão .mjs:
// arquivo modulo.mjs
export default 'valor';
// arquivo index.mjs
import value from './modulo.mjs';

console.log(value);
# executa arquivo index.mjs
node index.mjs

Seguindo essa linha, você também pode utilizar arquivos com CommonJS em aplicações usando ESModules. Para fazer isso é só criar o arquivo com extensão .cjs.

  • criando arquivos com extensão .js como de costume, mas inserindo a chave "type" com o valor "module" no package.json do projeto:
{
  "type": "module" // inserindo no package.json do projeto
}
// arquivo modulo.js
export default 'valor';
// arquivo index.js
import value from './modulo.js';

console.log(value);
# executa arquivo index
node .

#Browser

Já é possível trabalhar com ESModules no navegador hoje em dia também.

Vamos imaginar os mesmos arquivos que fizemos anteriormente:

// arquivo modulo.js
export default 'valor';
// arquivo index.js
import value from './modulo.js';

console.log(value);

Para executá-los no navegador, na sua tag script basta indicar que o type será module:

<script src="index.js" type="module"></script>

E pronto: a mensagem aparecerá no console!

#Quantas opções... Qual devo usar?

Com o tempo, os diferentes padrões surgiram e agora tudo está se ajeitando ao redor do ESModules (ainda bem).

Entretanto, não custa saber as formas anteriores de modularizar um código em JavaScript, afinal, foram parte importante da história da linguagem.

Os padrões anteriores devem continuar existindo principalmente em aplicações legadas. Ainda mais o CommonJS que ainda está presente em grande parte das aplicações em Node e é extremamente comum de se utilizar no dia-a-dia.

#E você, já tinha trabalhado com todos esses tipos de módulos?

Espero que toda essa (longa) história de se trabalhar com módulos no JavaScript tenha ficado um pouco mais clara e menos assustadora pra você!

Você pode não ter usado todos esses padrões, mas com certeza conhecer um pouquinho deles e entender em que pé esse assunto está pode ajudar você a se situar melhor dentro do ecossistema JavaScript e a entender onde aplicar (ou não) cada tipo de modularização.


Compartilhe