Módulos JavaScript: um guia para iniciantes

Se você for um novato em JavaScript, jargões como “bundlers de módulo vs. carregadores de módulo”, “Webpack vs. Browserify” e “AMD vs. CommonJS” podem rapidamente se tornar opressores.

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

Nesta postagem, vou descompactar esses chavões para você em inglês simples (e alguns exemplos de código). Espero que você ache isso útil!

Observação: para simplificar, isso será dividido em duas seções: a Parte 1 explicará o que são módulos e por que os usamos. A Parte 2 (postada na próxima semana) explicará o que significa agrupar módulos e as diferentes maneiras de fazer isso.

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

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

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

Bons módulos, no entanto, são altamente autocontidos com funcionalidades distintas, 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 em usar módulos em favor de uma base de código interdependente e extensa. Os mais importantes, na minha opinião, são:

1) Capacidade de manutenção: Por definição, um módulo é independente. Um módulo bem projetado visa diminuir as dependências de partes da base de código tanto quanto possível, para que possa crescer e melhorar de forma independente. Atualizar um único módulo é muito mais fácil quando o módulo está desacoplado de outras partes do código.

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

2) Namespacing: em 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 haver “poluição do namespace”, onde códigos completamente não relacionados compartilham variáveis ​​globais.

Compartilhar variáveis ​​globais entre códigos não relacionados é uma grande desvantagem no desenvolvimento.

Como veremos mais tarde nesta postagem, os módulos nos permitem evitar a poluição do namespace criando um espaço privado para nossas variáveis.

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

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

Obviamente, isso é uma grande perda de tempo. Não seria muito mais fácil se houvesse - espere por ele - um módulo que podemos reutilizar continuamente?

Como você pode incorporar módulos?

Existem muitas maneiras de incorporar módulos em seus programas. Vamos examinar alguns deles:

Padrão de módulo

O padrão de módulo é usado para imitar o conceito de classes (uma vez que JavaScript não suporta classes nativamente) para que possamos armazenar métodos e variáveis ​​públicos e privados dentro de um único objeto - semelhante a como as classes são usadas em outras linguagens de programação como Java ou Python. Isso nos permite criar uma API voltada para o público para os métodos que queremos expor ao mundo, enquanto ainda encapsulamos variáveis ​​e métodos privados em um escopo de fechamento.

Existem várias maneiras de realizar o padrão de módulo. Neste primeiro exemplo, usarei um encerramento anônimo. Isso nos ajudará a cumprir nosso objetivo, colocando todo o nosso código em uma função anônima. (Lembre-se: em JavaScript, as funções são a única maneira de criar um novo escopo.)

Exemplo 1: fechamento anônimo

(function () { // We keep these variables private inside this closure scope var myGrades = [93, 95, 88, 0, 55, 91]; var average = function() { var total = myGrades.reduce(function(accumulator, item) { return accumulator + item}, 0); return 'Your average grade is ' + total / myGrades.length + '.'; } var failing = function(){ var failingGrades = myGrades.filter(function(item) { return item < 70;}); return 'You failed ' + failingGrades.length + ' times.'; } console.log(failing()); }()); // ‘You failed 2 times.’

Com esta construção, nossa função anônima tem seu próprio ambiente de avaliação ou “fechamento”, e então o avaliamos imediatamente. Isso nos permite ocultar variáveis ​​do namespace pai (global).

O que é bom sobre essa abordagem é que você pode usar variáveis ​​locais dentro desta função sem substituir acidentalmente as variáveis ​​globais existentes, mas ainda assim acessar as variáveis ​​globais, assim:

var global = 'Hello, I am a global variable :)'; (function () { // We keep these variables private inside this closure scope var myGrades = [93, 95, 88, 0, 55, 91]; var average = function() { var total = myGrades.reduce(function(accumulator, item) { return accumulator + item}, 0); return 'Your average grade is ' + total / myGrades.length + '.'; } var failing = function(){ var failingGrades = myGrades.filter(function(item) { return item < 70;}); return 'You failed ' + failingGrades.length + ' times.'; } console.log(failing()); console.log(global); }()); // 'You failed 2 times.' // 'Hello, I am a global variable :)'

Observe que os parênteses em torno da função anônima são necessários, porque as instruções que começam com a palavra-chave função são sempre consideradas 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 circundantes criam uma expressão de função em vez de. Se você estiver curioso, pode ler mais aqui.

Exemplo 2: importação global

Outra abordagem popular usada por bibliotecas como jQuery é a importação global. É semelhante ao fechamento anônimo que acabamos de ver, exceto que agora passamos em globais como parâmetros:

(function (globalVariable) { // Keep this variables private inside this closure scope var privateFunction = function() { console.log('Shhhh, this is private!'); } // Expose the below methods via the globalVariable interface while // hiding the implementation of the method within the // function() block globalVariable.each = function(collection, iterator) { if (Array.isArray(collection)) { for (var i = 0; i < collection.length; i++) { iterator(collection[i], i, collection); } } else { for (var key in collection) { iterator(collection[key], key, collection); } } }; globalVariable.filter = function(collection, test) { var filtered = []; globalVariable.each(collection, function(item) { if (test(item)) { filtered.push(item); } }); return filtered; }; globalVariable.map = function(collection, iterator) { var mapped = []; globalUtils.each(collection, function(value, key, collection) { mapped.push(iterator(value)); }); return mapped; }; globalVariable.reduce = function(collection, iterator, accumulator) { var startingValueMissing = accumulator === undefined; globalVariable.each(collection, function(item) { if(startingValueMissing) { accumulator = item; startingValueMissing = false; } else { accumulator = iterator(accumulator, item); } }); return accumulator; }; }(globalVariable)); 

Neste exemplo, globalVariable é a única variável global. O benefício dessa abordagem em relação aos fechamentos anônimos é que você declara as variáveis ​​globais antecipadamente, tornando-as muito claras para as pessoas que estão lendo seu código.

Exemplo 3: Interface do objeto

Ainda outra abordagem é criar módulos usando uma interface de objeto independente, assim:

var myGradesCalculate = (function () { // Keep this variable private inside this closure scope var myGrades = [93, 95, 88, 0, 55, 91]; // Expose these functions via an interface while hiding // the implementation of the module within the function() block return { average: function() { var total = myGrades.reduce(function(accumulator, item) { return accumulator + item; }, 0); return'Your average grade is ' + total / myGrades.length + '.'; }, failing: function() { var failingGrades = myGrades.filter(function(item) { return item < 70; }); return 'You failed ' + failingGrades.length + ' times.'; } } })(); myGradesCalculate.failing(); // 'You failed 2 times.' myGradesCalculate.average(); // 'Your average grade is 70.33333333333333.'

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

Exemplo 4: Padrão de módulo revelador

Isso é muito semelhante à abordagem acima, exceto que garante que todos os métodos e variáveis ​​sejam mantidos privados até que sejam explicitamente expostos:

var myGradesCalculate = (function () { // Keep this variable private inside this closure scope var myGrades = [93, 95, 88, 0, 55, 91]; var average = function() { var total = myGrades.reduce(function(accumulator, item) { return accumulator + item; }, 0); return'Your average grade is ' + total / myGrades.length + '.'; }; var failing = function() { var failingGrades = myGrades.filter(function(item) { return item < 70; }); return 'You failed ' + failingGrades.length + ' times.'; }; // Explicitly reveal public pointers to the private functions // that we want to reveal publicly return { average: average, failing: failing } })(); myGradesCalculate.failing(); // 'You failed 2 times.' myGradesCalculate.average(); // 'Your average grade is 70.33333333333333.'

Pode parecer muita coisa, mas é apenas a ponta do iceberg quando se trata de padrões de módulos. Aqui estão alguns dos recursos que achei úteis em minhas próprias explorações:

  • Learning JavaScript Design Patterns de Addy Osmani: um tesouro de detalhes em uma leitura impressionantemente sucinta
  • Adequately Good por Ben Cherry: uma visão geral útil com exemplos de uso avançado do padrão de módulo
  • Blog de Carl Danley: visão geral do padrão de módulo e recursos para outros padrões JavaScript.

CommonJS e AMD

As abordagens acima todas têm uma coisa em comum: o uso de uma única variável global para agrupar seu código em uma função, criando assim um namespace privado para si mesmo usando um escopo de encerramento.

While each approach is effective in its own way, they have their downsides.

For one, as a developer, you need to know the right dependency order to load your files in. For instance, let’s say you’re using Backbone in your project, so you include the script tag for Backbone’s source code in your file.

However, since Backbone has a hard dependency on Underscore.js, the script tag for the Backbone file can’t be placed before the Underscore.js file.

As a developer, managing dependencies and getting these things right can sometimes be a headache.

Another downside is that they can still lead to namespace collisions. For example, what if two of your modules have the same name? Or what if you have two versions of a module, and you need both?

So you’re probably wondering: can we design a way to ask for a module’s interface without going through the global scope?

Fortunately, the answer is yes.

There are two popular and well-implemented approaches: CommonJS and AMD.

CommonJS

CommonJS is a volunteer working group that designs and implements JavaScript APIs for declaring modules.

A CommonJS module is essentially a reusable piece of JavaScript which exports specific objects, making them available for other modules to require in their programs. If you’ve programmed in Node.js, you’ll be very familiar with this format.

With CommonJS, each JavaScript file stores modules in its own unique module context (just like wrapping it in a closure). In this scope, we use the module.exports object to expose modules, and require to import them.

When you’re defining a CommonJS module, it might look something like this:

function myModule() { this.hello = function() { return 'hello!'; } this.goodbye = function() { return 'goodbye!'; } } module.exports = myModule;

We use the special object module and place a reference of our function into module.exports. This lets the CommonJS module system know what we want to expose so that other files can consume it.

Then when someone wants to use myModule, they can require it in their file, like so:

var myModule = require('myModule'); var myModuleInstance = new myModule(); myModuleInstance.hello(); // 'hello!' myModuleInstance.goodbye(); // 'goodbye!'

There are two obvious benefits to this approach over the module patterns we discussed before:

1. Avoiding global namespace pollution

2. Making our dependencies explicit

Moreover, the syntax is very compact, which I personally love.

Another thing to note is that CommonJS takes a server-first approach and synchronously loads modules. This matters because if we have three other modules we need to require, it’ll load them one by one.

Now, that works great on the server but, unfortunately, makes it harder to use when writing JavaScript for the browser. Suffice it to say that reading a module from the web takes a lot longer than reading from disk. For as long as the script to load a module is running, it blocks the browser from running anything else until it finishes loading. It behaves this way because the JavaScript thread stops until the code has been loaded. (I’ll cover how we can work around this issue in Part 2 when we discuss module bundling. For now, that’s all we need to know).

AMD

CommonJS is all well and good, but what if we want to load modules asynchronously? The answer is called Asynchronous Module Definition, or AMD for short.

Loading modules using AMD looks something like this:

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

What’s happening here is that the define function takes as its first argument an array of each of the module’s dependencies. These dependencies are loaded in the background (in a non-blocking manner), and once loaded define calls the callback function it was given.

Next, the callback function takes, as arguments, the dependencies that were loaded — in our case, myModule and myOtherModule — allowing the function to use these dependencies. Finally, the dependencies themselves must also be defined using the define keyword.

For example, myModule might look like this:

define([], function() { return { hello: function() { console.log('hello'); }, goodbye: function() { console.log('goodbye'); } }; });

So again, unlike CommonJS, AMD takes a browser-first approach alongside asynchronous behavior to get the job done. (Note, there are a lot of people who strongly believe that dynamically loading files piecemeal as you start to run code isn’t favorable, which we’ll explore more when in the next section on module-building).

Aside from asynchronicity, another benefit of AMD is that your modules can be objects, functions, constructors, strings, JSON and many other types, while CommonJS only supports objects as modules.

That being said, AMD isn’t compatible with io, filesystem, and other server-oriented features available via CommonJS, and the function wrapping syntax is a bit more verbose compared to a simple require statement.

UMD

For projects that require you to support both AMD and CommonJS features, there’s yet another format: Universal Module Definition (UMD).

UMD essentially creates a way to use either of the two, while also supporting the global variable definition. As a result, UMD modules are capable of working on both client and server.

Here’s a quick taste of how UMD goes about its business:

(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 { // Browser globals (Note: root is window) root.returnExports = factory(root.myModule, root.myOtherModule); } }(this, function (myModule, myOtherModule) { // Methods function notHelloOrGoodbye(){}; // A private method function hello(){}; // A public method because it's returned (see below) function goodbye(){}; // A public method because it's returned (see below) // Exposed public methods return { hello: hello, goodbye: goodbye } }));

For more examples of UMD formats, check out this enlightening repo on GitHub.

Native JS

Phew! Are you still around? I haven’t lost you in the woods here? Good! Because we have *one more* type of module to define before we’re done.

As you probably noticed, none of the modules above were native to JavaScript. Instead, we’ve created ways to emulate a modules system by using either the module pattern, CommonJS or AMD.

Fortunately, the smart folks at TC39 (the standards body that defines the syntax and semantics of ECMAScript) have introduced built-in modules with ECMAScript 6 (ES6).

ES6 offers up a variety of possibilities for importing and exporting modules which others have done a great job explaining — here are a few of those resources:

  • jsmodules.io
  • exploringjs.com

What’s great about ES6 modules relative to CommonJS or AMD is how it manages to offer the best of both worlds: compact and declarative syntax and asynchronous loading, plus added benefits like better support for cyclic dependencies.

Probably my favorite feature of ES6 modules is that imports are live read-only views of the exports. (Compare this to CommonJS, where imports are copies of exports and consequently not alive).

Here’s an example of how that works:

// 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

In this example, we basically make two copies of the module: one when we export it, and one when we require it.

Moreover, the copy in main.js is now disconnected from the original module. That’s why even when we increment our counter it still returns 1 — because the counter variable that we imported is a disconnected copy of the counter variable from the module.

So, incrementing the counter will increment it in the module, but won’t increment your copied version. The only way to modify the copied version of the counter variable is to do so manually:

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

On the other hand, ES6 creates a live read-only view of the modules we import:

// 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

Cool stuff, huh? What I find really compelling about live read-only views is how they allow you to split your modules into smaller pieces without losing functionality.

Então você pode virar e mesclá-los novamente, sem problemas. Simplesmente “funciona”.

Olhando para o futuro: empacotando módulos

Uau! Para onde o tempo vai? Foi uma viagem louca, mas espero sinceramente que tenha ajudado você a compreender melhor os módulos em JavaScript.

Na próxima seção, examinarei o agrupamento de módulos, cobrindo os principais tópicos, incluindo:

  • Por que agrupamos módulos
  • Diferentes abordagens para empacotar
  • API do carregador de módulo ECMAScript
  • …e mais. :)

NOTA: Para manter as coisas simples, pulei alguns dos detalhes essenciais (pense: dependências cíclicas) neste post. Se deixei de fora algo importante e / ou fascinante, por favor me avise nos comentários!