Artigo original: JavaScript Modules: A Beginner’s Guide

Escrito por: Preethi Kasireddy

Se você é um recém-chegado ao JavaScript, jargões como "bundlers de módulos x carregadores de módulos", "Webpack x Browserify" e "AMD x CommonJS" podem rapidamente se tornar assustadores.

O sistema de módulos do JavaScript pode ser intimidante, mas compreendê-lo é vital para os desenvolvedores para a web.

Neste artigo, eu tentarei explicar essas palavras para você em português simples (e com algumas amostras de código). Espero que você considere útil!

Observação: por uma questão de simplicidade, o artigo será dividido em duas seções: A parte 1 tratará do que são módulos e no motivo de usarmos esses módulos. A parte 2 (a ser publicada) vai mostrar o que significa agrupar módulos e as diferentes maneiras de fazê-lo.

Parte 1: Alguém pode me explicar o que são módulos?

Os bons autores dividem seus livros em capítulos e seções. Os bons programadores dividem seus programas em módulos.

Como um capítulo de livro, os módulos são apenas conjuntos de palavras (ou de código, conforme o caso).

Bons módulos, no entanto, são altamente autocontidos, com funcionalidade distinta, permitindo que sejam embaralhados, removidos ou adicionados conforme necessário, sem interromper o sistema como um todo.

Por que usar módulos?

Há muitos benefícios no uso de módulos em favor de uma base de código ampla e interdependente. Os mais importantes, em minha opinião, são:

1) Capacidade de manutenção: por definição, um módulo é autocontido. Um módulo bem projetado visa diminuir ao máximo as dependências de partes da base de código, para que ela possa crescer e melhorar independentemente. A atualização de um único módulo é muito mais fácil quando o módulo é desacoplado de outros pedaços de código.

Voltando ao nosso exemplo do livro, se você quisesse atualizar um capítulo de seu livro, seria um pesadelo se uma pequena mudança em um capítulo exigisse que você também ajustasse todos os outros capítulos. Em vez disso, você gostaria de escrever cada capítulo de tal forma que as melhorias pudessem ser feitas sem afetar outros capítulos.

2) Espaço dos nomes: no JavaScript, as variáveis fora do escopo de uma função de nível superior são globais (ou seja, todos podem acessá-las). Por causa disso, é comum ter "poluição do espaço de nomes", onde o código totalmente não relacionado compartilha variáveis globais.

O compartilhamento de variáveis globais entre códigos não relacionados é um grande "não" em desenvolvimento (texto em inglês).

Como veremos mais adiante neste post, os módulos nos permitem evitar a poluição do espaçamento de nomes, criando um espaço privado para as nossas variáveis.

3) Reusabilidade: sejamos honestos aqui. Todos nós copiamos código que escrevemos anteriormente em novos projetos em um momento ou outro. Por exemplo, vamos imaginar que você copiou alguns métodos utilitários que você escreveu de um projeto anterior para o seu projeto atual.

Tudo isso está bem, mas se você encontrar uma maneira melhor de escrever alguma parte desse código, você terá que voltar e se lembrar de atualizá-lo em todos os outros lugares em que o escreveu.

Isto é obviamente uma enorme perda de tempo. Não seria muito mais fácil se houvesse um módulo que pudéssemos reutilizar repetidamente?

Como você pode incorporar módulos?

Há muitas maneiras de se incorporar módulos em seus programas. Vamos passo a passo por algumas delas:

Padrão Module

O padrão Module é usado para imitar o conceito de classes (já que o JavaScript não dá suporte às classes nativamente) para que possamos armazenar tanto métodos e variáveis públicas quanto privadas dentro de um único objeto - similar a como as classes são usadas em outras linguagens de programação, como Java ou Python. Isso nos permite criar uma API pública para os métodos que queremos expor ao mundo, enquanto ainda encapsulamos variáveis e métodos privados em um escopo de closure.

Há várias maneiras de realizar o padrão Module. Neste primeiro exemplo, vou usar uma closure anônima. Isso nos ajudará a atingir nosso objetivo, colocando todo nosso código em uma função anônima. Lembre-se: em JavaScript, as funções são a única maneira de se criar um escopo.

Exemplo 1: Closure anônima

(function () {
  // Mantemos estas variáveis privadas dentro deste escopo de closure
  
  var minhasNotas = [93, 95, 88, 0, 55, 91];
  
  var media = function() {
    var total = minhasNotas.reduce(function(accumulator, item) {
      return acumulador + item}, 0);
    
      return 'Sua média é ' + total / minhasNotas.length + '.';
  }

  var semSucesso = function(){
    var notasSemSucesso = minhasNotas.filter(function(item) {
      return item < 70;});
      
    return 'Você não passou ' + notasSemSucesso.length + ' vezes.';
  }

  console.log(semSucesso());

}());

// ‘Você não passou 2 vezes.’

Com esta construção, nossa função anônima tem seu próprio ambiente de avaliação ou "closure". Nós, então, a avaliamos imediatamente. Isto nos permite ocultar variáveis do espaço de nomes acima dela (global).

O que é bom nessa abordagem é que você pode usar variáveis locais dentro dessa função sem sobrescrever acidentalmente as variáveis globais existentes, mas ainda assim acessar as variáveis globais, deste modo:

var global = 'Olá, eu sou uma variável global :)';

(function () {
  // Mantemos estas variáveis privadas dentro deste escopo de closure
  
  var minhasNotas = [93, 95, 88, 0, 55, 91];
  
  var media = function() {
    var total = minhasNotas.reduce(function(acumulador, item) {
      return acumulador + item}, 0);
    
    return 'Sua média é ' + total / minhasNotas.length + '.';
  }

  var semSucesso = function(){
    var notasSemSucesso = minhasNotas.filter(function(item) {
      return item < 70;});
      
    return 'Você não passou ' + notasSemSucesso.length + ' vezes.';
  }

  console.log(semSucesso());
  console.log(global);
}());

// 'Você não passou 2 vezes.'
// 'Olá, eu sou uma variável global :)'

Observe que os parênteses ao redor da função anônima são necessários, porque as declarações que começam com a palavra-chave function são sempre consideradas como declarações de função (lembre-se: você não pode ter declarações de função sem nome em JavaScript). Consequentemente, os parênteses ao redor criam uma expressão de função em seu lugar. Se você estiver curioso, pode ler mais aqui (texto em inglês).

Exemplo 2: Importação global

Outra abordagem popular utilizada por bibliotecas como a jQuery é a importação global. É semelhante à closure anônima que acabamos de ver, exceto que agora passamos globais como parâmetros:

(function (variavelGlobal) {

  // Mantém esta variávei privada dentro deste escopo de closure
  var funcaoPrivada = function() {
    console.log('Shhhh, isso é privado!');
  }

  // Expõe os métodos abaixo através da interface globalVariable enquanto
  // ocultando a implementação do método dentro do  
  // bloco de função

  variavelGlobal.each = function(colecao, iterador) {
    if (Array.isArray(colecao)) {
      for (var i = 0; i < colecao.length; i++) {
        iterador(colecao[i], i, colecao);
      }
    } else {
      for (var chave in colecao) {
        iterador(colecao[chave], chave, colecao);
      }
    }
  };

  variavelGlobal.filter = function(colecao, teste) {
    var filtrada = [];
    variavelGlobal.each(colecao, function(item) {
      if (teste(item)) {
        filtrada.push(item);
      }
    });
    return filtrada;
  };

  variavelGlobal.map = function(colecao, iterador) {
    var mapeada = [];
    utilsGlobal.each(colecao, function(valor, chave, colecao) {
      mapeada.push(iterador(valor));
    });
    return mapeada;
  };

  variavelGlobal.reduce = function(colecao, iterador, acumulador) {
    var valorInicialAusente = acumulador === undefined;

    variavelGlobal.each(colecao, function(item) {
      if(valorInicialAusente) {
        acumulador = item;
        valorInicialAusente = false;
      } else {
        acumulador = iterador(acumulador, item);
      }
    });

    return acumulador;

  };

 }(variavelGlobal));
  

Neste exemplo, variavelGlobal é a única variável que é global. O benefício dessa abordagem sobre as closures anônimas é que você declara as variáveis globais antecipadamente, tornando tudo muito claro para as pessoas que leem seu código.

Exemplo 3: Interface de objeto

Ainda outra abordagem é criar módulos usando uma interface de objetos autocontida, assim:

var calcularMinhasNotas = (function () {
    
  //Mantém esta variável privada dentro deste escopo de closure
  var minhasNotas = [93, 95, 88, 0, 55, 91];

  // Expõe estas funções através de uma interface enquanto se esconde
  // a implementação do módulo dentro do bloco de função()

  return {
    media: function() {
      var total = minhasNotas.reduce(function(acumulador, item) {
        return acumulador + item;
        }, 0);
        
      return'Sua média é ' + total / minhasNotas.length + '.';
    },

    semSucesso: function() {
      var notasSemSucesso = minhasNotas.filter(function(item) {
          return item < 70;
        });

      return 'Você não passou ' + notasSemSucesso.length + ' vezes.';
    }
  }
})();

calcularMinhasNotas.semSucesso(); // 'Você não passou 2 vezes.' 
calcularMinhasNotas.media(); // 'Sua média é 70.33333333333333.'

Como você pode ver, essa abordagem nos permite decidir quais variáveis/métodos queremos manter privados (por exemplo, minhasNotas) e quais variáveis/métodos queremos expor, colocando-os na declaração de retorno (por exemplo, média e semSucesso).

Exemplo 4: Revelando o módulo padrão

Esta abordagem é muito semelhante à abordagem acima, exceto pelo fato de que ela garante que todos os métodos e variáveis sejam mantidos privados até serem explicitamente expostos:

var calcularMinhasNotas = (function () {
    
  // Mantém esta variável privada dentro deste escopo de closure
  var minhasNotas = [93, 95, 88, 0, 55, 91];
  
  var media = function() {
    var total = minhasNotas.reduce(function(acumulador, item) {
      return acumulador + item;
      }, 0);
      
    return'Sua média é ' + total / minhasNotas.length + '.';
  };

  var semSucesso = function() {
    var notasSemSucesso = minhasNotas.filter(function(item) {
        return item < 70;
      });

    return 'Você não passou ' + failingGrades.length + ' vezes.';
  };

  // Revela explicitamente as indicações públicas para as funções privadas 
  // que queremos revelar publicamente

  return {
    media: media,
    semSucesso: semSucesso
  }
})();

calcularMinhasNotas.semSucesso(); // 'Você falhou 2 vezes.' 
calcularMinhasNotas.media(); // 'Sua média é 70.33333333333333.'

Elas podem se parecer muito, mas é apenas a ponta do iceberg quando se trata de padrões de módulo. Aqui estão alguns dos recursos que achei úteis em minhas próprias explorações (todas as fontes estão em inglês):

CommonJS e AMD

As abordagens, acima de tudo, têm uma coisa em comum: o uso de uma única variável global para envolver seu código em uma função, criando assim um espaço de nomes privado para si mesmas usando um escopo de closure.

Embora as abordagens sejam eficazes à sua própria maneira, elas têm seus pontos negativos.

Para começar, como desenvolvedor, você precisa saber a ordem de dependência correta para carregar seus arquivos. Por exemplo, digamos que você esteja usando o Backbone em seu projeto, então você inclui a tag do script para o código fonte do Backbone em seu arquivo.

Entretanto, como o Backbone tem uma forte dependência do Underscore.js, a tag do script para o arquivo Backbone não pode ser colocada antes do arquivo Underscore.js.

Como desenvolvedor, gerenciar as dependências e acertar essas coisas às vezes pode ser uma dor de cabeça.

Outro ponto negativo é o fato de que elas ainda podem levar a colisões de espaço de nomes, por exemplo, quando dois dos módulos tiverem o mesmo nome. Você também pode ter duas versões de um mesmo módulo e precisar de ambas.

Então, você, provavelmente, está se perguntando: podemos achar uma maneira de pedir a interface de um módulo sem passar pelo escopo global?

Felizmente, a resposta é sim.

Há duas abordagens populares e bem implementadas: CommonJS e AMD.

CommonJS

CommonJS é um grupo de trabalho voluntário que projeta e implementa APIs de JavaScript para a declaração de módulos.

Um módulo CommonJS é essencialmente uma peça reutilizável de JavaScript que exporta objetos específicos, tornando-os disponíveis para que outros módulos possam solicitá-las (em inglês, require) em seus programas. Se você já tiver programado em Node.js, estará bastante familiarizado com esse formato.

Com o CommonJS, cada arquivo JavaScript armazena módulos em seu próprio contexto de módulo único (semelhante a envolvê-lo em uma closure). Neste escopo, usamos o objeto module.exports para expor os módulos e solicitamos sua importação.

Quando você estiver definindo um módulo CommonJS, ele pode parecer algo assim:

function myModule() {
  this.hello = function() {
    return 'olá!';
  }

  this.goodbye = function() {
    return 'tchau!';
  }
}

module.exports = myModule;

Utilizamos o módulo objeto especial e colocamos uma referência da nossa função em module.exports. Isto permite ao sistema de módulos do CommonJS saber o que queremos expor para que os outros arquivos possam consumi-lo.

Então, quando alguém quiser usar myModule, pode solicitá-lo em seu arquivo, assim:

var myModule = require('myModule');

var myModuleInstance = new myModule();
myModuleInstance.hello(); // 'olá!'
myModuleInstance.goodbye(); // 'tchau!'

Há dois benefícios óbvios para essa abordagem sobre os módulos padrões que discutimos anteriormente:

  1. Evitar a poluição global do espaço de nome
  2. Tornar nossas dependências explícitas

Além disso, a sintaxe é muito compacta, o que eu pessoalmente adoro.

Outra coisa a ser notada é que o CommonJS adota a abordagem que privilegia o servidor e carrega os módulos de modo sincronizado. Isto é importante, pois se tivermos outros três módulos que precisamos solicitar, ele os carregará um a um.

Isso funciona muito bem no servidor mas, infelizmente, se torna mais difícil de usar ao escrever JavaScript para o navegador. Basta dizer que ler um módulo a partir da web leva muito mais tempo do que ler a partir do disco. Enquanto o script para carregar um módulo estiver rodando, ele impede que o navegador execute qualquer outra coisa até que ele termine de carregar. Ele se comporta dessa maneira porque a thread do JavaScript para até que o código tenha sido carregado.

Observação: vou abordar como podemos contornar esse problema na parte 2, quando discutirmos o bundling de módulos. Por enquanto, isso é tudo o que precisamos saber.

AMD

O CommonJS é bom, mas se quisermos carregar módulos de forma assíncrona, como podemos fazer? A resposta é chamada de Definição de Módulos Assíncrona, ou AMD, para abreviar (em inglês, Asynchronous Module Definition).

O carregamento de módulos usando AMD tem esta aparência:

define(['myModule', 'myOtherModule'], function(myModule, myOtherModule) {
  console.log(myModule.hello());
});

O que está acontecendo aqui é que a função define toma como seu primeiro argumento um array de cada uma das dependências do módulo. Estas dependências são carregadas no plano de fundo (de modo não bloqueado). Uma vez carregado, define faz as chamadas de callback da função que lhe foi dada.

Em seguida, a função de callback toma como argumentos as dependências que foram carregadas – no nosso caso, myModule e myOtherModule – permitindo que a função use essas dependências. Finalmente, as próprias dependências também devem ser definidas usando a palavra-chave define.

Por exemplo, o myModule pode ter esta aparência:

define([], function() {

  return {
    hello: function() {
      console.log('olá');
    },
    goodbye: function() {
      console.log('tchau');
    }
  };
});

Novamente, ao contrário do CommonJS, a AMD adota uma abordagem de privilegiar o navegador junto com o comportamento assíncrono para realizar o trabalho. Observe que há muitas pessoas que acreditam fortemente que carregar arquivos dinamicamente de modo fragmentado, à medida que você começa a executar o código, não é favorável. Exploraremos mais a respeito quando estivermos na próxima seção sobre a construção de módulos.

Além da assincronia, outro benefício do AMD é que seus módulos podem ser objetos, funções, construtores, strings, JSON e muitos outros tipos, enquanto o CommonJS só dá suporte a objetos como módulos.

Dito isso, a AMD não é compatível com o io, filesystem e outros recursos orientados ao servidor disponíveis através do CommonJS. A sintaxe de wrapping de funções é um pouco mais extensa em comparação com uma simples declaração de solicitação.

UMD

Para projetos que requerem o suporte tanto da AMD como das funcionalidades do CommonJS, há ainda outro formato: Definição do Módulo Universal (UMD, do inglês, Universal Module Definition).

A UMD, essencialmente, cria uma maneira de usar qualquer uma das duas, ao mesmo tempo em que também suporta a definição de variável global. Como resultado, os módulos UMD são capazes de trabalhar tanto no client quanto no servidor.

Aqui está uma rápida amostra de como a UMD realiza suas tarefas:

(function (root, factory) {
  if (typeof define === 'function' && define.amd) {
      // AMD
    define(['myModule', 'myOtherModule'], factory);
  } else if (typeof exports === 'object') {
      // CommonJS
    module.exports = factory(require('myModule'), require('myOtherModule'));
  } else {
    // Navegadores Globais (Nota: root é a janela - window)
    root.returnExports = factory(root.myModule, root.myOtherModule);
  }
}(this, function (myModule, myOtherModule) {
  // Métodos
  function notHelloOrGoodbye(){}; // Um método privado
  function hello(){}; // Um método público porque é retornado (ver abaixo))
  function goodbye(){}; // Um método público porque é retornado (ver abaixo))

  // Métodos públicos expostos
  return {
      hello: hello,
      goodbye: goodbye
  }
}));

Para mais exemplos de formatos UMD, confira este repositório esclarecedor no GitHub.

JS nativo

Ainda está por aqui? Não perdi você no meio do caminho? Ótimo, pois temos *mais um* tipo de módulo para definir antes de terminarmos.

Como você provavelmente notou, nenhum dos módulos acima era nativo de JavaScript. Em vez disso, criamos maneiras de emular um sistema de módulos usando o padrão de módulos, CommonJS ou AMD.

Felizmente, o pessoal inteligente do TC39 (o corpo de normas que define a sintaxe e semântica do ECMAScript) introduziu módulos integrados com o ECMAScript 6 (ES6).

O ES6 oferece uma variedade de possibilidades para importação e exportação de módulos que outros fizeram um grande trabalho explicando – aqui estão alguns desses recursos explicativos (em inglês):

O que é ótimo nos módulos ES6 em comparação com CommonJS ou AMD é como eles conseguem oferecer o melhor dos dois mundos: sintaxe compacta e declarativa e carregamento assíncrono, além de benefícios adicionais, como melhor suporte para dependências cíclicas.

Provavelmente, minha funcionalidade favorita dos módulos ES6 é que as importações são visualizações somente leitura no mesmo momento das exportações. Compare isso com o CommonJS, onde as importações são cópias das exportações e, consequentemente, não são assim.

Aqui está um exemplo de como isso funciona:

// lib/counter.js

var counter = 1;

function increment() {
  counter++;
}

function decrement() {
  counter--;
}

module.exports = {
  counter: counter,
  increment: increment,
  decrement: decrement
};


// src/main.js

var counter = require('../../lib/counter');

counter.increment();
console.log(counter.counter); // 1

Neste exemplo, basicamente fazemos duas cópias do módulo: uma quando o exportamos, e outra quando o solicitamos.

Além disso, a cópia no main.js está agora desconectada do módulo original. É por isso que, mesmo quando incrementamos o nosso contador, ele ainda retorna 1 – porque a variável do contador que importamos é uma cópia desconectada da variável do contador do módulo.

Assim, incrementar o contador vai incrementá-lo no módulo, mas não incrementará sua versão copiada. A única maneira de modificar a versão copiada da variável do contador é fazê-lo manualmente:

counter.counter++;
console.log(counter.counter); // 2

Por outro lado, o ES6 cria uma visualização somente leitura no mesmo momento dos módulos que importamos:

// lib/counter.js
export let counter = 1;

export function increment() {
  counter++;
}

export function decrement() {
  counter--;
}


// src/main.js
import * as counter from '../../counter';

console.log(counter.counter); // 1
counter.increment();
console.log(counter.counter); // 2

Muito bom, não é? O que eu acho realmente atraente nas visualizações somente leitura no mesmo momento é como elas permitem que você divida seus módulos em peças menores sem perder a funcionalidade.

Então, você pode dar a volta e mesclá-los novamente, sem problemas. Simplesmente funciona.

Olhando para o futuro: bundling de módulos

Uau! O tempo voa, não é mesmo? Foi um passeio e tanto, mas espero, sinceramente, que tenha dado a você uma melhor compreensão dos módulos em JavaScript.

No próximo artigo, tratarei do bundling de módulos, cobrindo tópicos centrais, incluindo:

  • Por que fazer o bundling de módulos
  • Diferentes abordagens para o bundling
  • API carregadora de módulos do ECMAScript
  • … e mais. :)
OBSERVAÇÃO: para manter o texto simples, eu pulei alguns dos detalhes minuciosos (por exemplo, dependências cíclicas) neste artigo. Se eu deixei de fora algo importante e/ou fascinante, fique à vontade para comentar!