Módulos e CommonJS vs. ES Modules
INFO
Criado por Kleber Galvão.
Bem-vindo à segunda aula do curso Node.js do Básico ao Avançado! Esta aula foca em um conceito fundamental do Node.js: módulos. Vamos explorar as diferenças entre CommonJS e ES Modules, os dois sistemas de módulos disponíveis no Node.js, e como eles são usados para organizar e reutilizar código. Além disso, você aprenderá a criar e importar módulos personalizados por meio de um exemplo prático, aplicando boas práticas e utilizando as versões mais recentes do Node.js (v20.x ou superior, em 2025).
O objetivo é que você compreenda as características, vantagens e desvantagens de cada sistema de módulos, além de dominar a criação e importação de módulos personalizados. O material é estruturado em duas partes: uma teoria detalhada sobre CommonJS e ES Modules, seguida de um exemplo prático que demonstra a implementação em ambos os formatos.
Teoria: Diferenças entre CommonJS e ES Modules
1. Introdução aos Módulos no Node.js
Módulos são blocos de código reutilizáveis que permitem organizar aplicações Node.js de forma modular. Eles encapsulam funcionalidades específicas, como funções, objetos ou classes, e podem ser importados em outros arquivos para evitar repetição de código e melhorar a manutenibilidade. No Node.js, os módulos são essenciais para estruturar projetos escaláveis, desde pequenos scripts até APIs complexas.
O Node.js suporta dois sistemas de módulos principais:
- CommonJS: O sistema original do Node.js, introduzido em 2009.
- ES Modules (ESM): O padrão oficial do JavaScript (ECMAScript), introduzido no ES6 (2015) e totalmente suportado no Node.js a partir da versão 12.
Ambos os sistemas têm sintaxes e comportamentos distintos, e entender suas diferenças é crucial para escolher o mais adequado ao seu projeto e escrever código compatível com as práticas modernas.
2. CommonJS: O Sistema de Módulos Original do Node.js
CommonJS é o sistema de módulos nativo do Node.js desde sua criação. Ele foi projetado para permitir modularidade em ambientes JavaScript fora do navegador, onde o ES6 ainda não existia. CommonJS é amplamente usado em projetos legados e em muitos pacotes npm, embora esteja sendo gradualmente substituído pelos ES Modules em projetos modernos.
2.1. Características do CommonJS
- Sintaxe: Usa
require()
para importar módulos emodule.exports
ouexports
para exportá-los. - Carregamento Síncrono: Módulos CommonJS são carregados de forma síncrona, ou seja, o Node.js lê e executa o módulo no momento da importação.
- Escopo do Módulo: Cada arquivo é tratado como um módulo independente, com seu próprio escopo. Variáveis locais não são acessíveis fora do módulo, a menos que sejam explicitamente exportadas.
- Cópia de Valores: Quando um módulo é importado, o Node.js cria uma cópia dos valores exportados. Alterações no módulo original após a importação não afetam a cópia importada (exceto para objetos, que são passados por referência).
- Suporte Nativo: CommonJS é suportado em todas as versões do Node.js, sem necessidade de configuração adicional.
2.2. Sintaxe do CommonJS
Exportação:
javascript// arquivo: math.js function soma(a, b) { return a + b; } module.exports = { soma };
Ou, alternativamente:
javascriptexports.soma = function (a, b) { return a + b; };
Importação:
javascript// arquivo: index.js const math = require('./math'); console.log(math.soma(2, 3)); // 5
Notas:
- O caminho
./math
indica um módulo local no mesmo diretório. O sufixo.js
é opcional. require()
pode importar módulos nativos (ex.:fs
,http
) ou pacotes npm (ex.:lodash
).
- O caminho
2.3. Vantagens do CommonJS
- Simplicidade: A sintaxe é direta e fácil de entender para iniciantes.
- Compatibilidade: Funciona em todas as versões do Node.js e é amplamente usado em pacotes npm legados.
- Carregamento Dinâmico: Permite importar módulos condicionalmente ou dinamicamente (ex.:
require(algumaVariavel)
). - Suporte em Ferramentas: Muitas ferramentas de build e bundlers (ex.: Webpack) suportam CommonJS nativamente.
2.4. Desvantagens do CommonJS
- Carregamento Síncrono: Pode causar atrasos em aplicações grandes, especialmente ao carregar muitos módulos no início.
- Falta de Tree Shaking: Não suporta tree shaking (remoção de código morto), o que pode aumentar o tamanho do bundle em aplicações frontend.
- Incompatibilidade com ES Modules: Não pode ser usado diretamente com ES Modules sem ferramentas de conversão (ex.: Babel).
- Sintaxe Menos Moderna: Comparada aos ES Modules, a sintaxe é considerada menos elegante e menos alinhada com o JavaScript moderno.
2.5. Como o CommonJS Funciona Internamente
Quando você usa require('./math')
, o Node.js segue este processo:
- Resolução do Módulo: O Node.js localiza o arquivo ou pacote com base no caminho fornecido.
- Carregamento: O arquivo é lido e executado.
- Wrapping: O Node.js envolve o código do módulo em uma função para criar um escopo isolado:javascript
(function (exports, require, module, __filename, __dirname) { // Código do módulo });
- Caching: O módulo é armazenado em cache, garantindo que múltiplas chamadas a
require()
retornem a mesma instância. - Exportação: O objeto
module.exports
é retornado para o código que chamourequire()
.
3. ES Modules: O Padrão Moderno do JavaScript
ES Modules (ESM) é o sistema de módulos oficial do ECMAScript, introduzido no ES6 (2015). Ele foi projetado para ser um padrão unificado para JavaScript, funcionando tanto no navegador quanto no servidor (Node.js). Desde a versão 12 do Node.js (2019), os ES Modules são suportados nativamente, e em 2025, eles são o padrão recomendado para novos projetos devido à sua integração com o ecossistema JavaScript moderno.
3.1. Características dos ES Modules
- Sintaxe: Usa
import
para importar eexport
para exportar. - Carregamento Assíncrono: Módulos ESM são carregados de forma assíncrona, permitindo melhor performance em aplicações modernas.
- Escopo Estrito: Funcionam automaticamente em modo estrito (
"use strict"
), garantindo maior segurança. - Suporte a Tree Shaking: Permite que ferramentas de build (ex.: Rollup, Vite) removam código não utilizado, otimizando o tamanho do bundle.
- Interoperabilidade: Compatível com navegadores e ferramentas modernas, facilitando o desenvolvimento full-stack.
3.2. Sintaxe dos ES Modules
Exportação:
javascript// arquivo: math.mjs export function soma(a, b) { return a + b; }
Ou, exportando múltiplos itens:
javascriptexport const soma = (a, b) => a + b; export const subtracao = (a, b) => a - b;
Importação:
javascript// arquivo: index.mjs import { soma } from './math.mjs'; console.log(soma(2, 3)); // 5
Exportação Padrão (Default):
javascript// arquivo: math.mjs export default function soma(a, b) { return a + b; }
javascript// arquivo: index.mjs import soma from './math.mjs'; console.log(soma(2, 3)); // 5
Notas:
- Arquivos ESM usam a extensão
.mjs
ou exigem"type": "module"
nopackage.json
. - O caminho deve incluir a extensão
.mjs
(ou.js
com configuração apropriada).
- Arquivos ESM usam a extensão
3.3. Vantagens dos ES Modules
- Padrão Oficial: Alinhado com o ECMAScript, garantindo compatibilidade com navegadores e Node.js.
- Carregamento Assíncrono: Melhora a performance ao carregar módulos sob demanda.
- Tree Shaking: Reduz o tamanho do código em aplicações frontend.
- Sintaxe Moderna: Mais clara e consistente com o JavaScript moderno (ex.:
import
é mais intuitivo querequire
). - Suporte a Top-Level Await: Permite usar
await
diretamente no nível superior do módulo (ex.:const data = await fetch(...)
).
3.4. Desvantagens dos ES Modules
- Configuração Adicional: Em projetos Node.js, é necessário configurar
"type": "module"
ou usar a extensão.mjs
. - Compatibilidade com CommonJS: Nem todos os pacotes npm são compatíveis com ESM, exigindo soluções como
import()
dinâmico ou ferramentas de conversão. - Curva de Aprendizado: A sintaxe e as regras (ex.: necessidade de extensões nos caminhos) podem confundir iniciantes.
- Suporte Parcial em Versões Antigas: Embora irrelevante em 2025, projetos legados em versões antigas do Node.js (pré-v12) não suportam ESM nativamente.
3.5. Como os ES Modules Funcionam Internamente
Quando você usa import
, o Node.js segue este processo:
- Resolução do Módulo: O Node.js localiza o arquivo com base no caminho, exigindo a extensão (ex.:
.mjs
). - Parsing: O módulo é analisado para identificar todas as dependências (
import
eexport
). - Carregamento Assíncrono: As dependências são carregadas em paralelo, usando promises.
- Linkagem: Os exports são vinculados aos imports, criando referências ao vivo (live bindings).
- Execução: O código do módulo é executado, e os valores exportados ficam disponíveis.
Live Bindings: Diferentemente do CommonJS, os ES Modules usam vinculação ao vivo. Se o valor exportado mudar no módulo original, a mudança é refletida no módulo que importou:
// arquivo: counter.mjs
export let count = 0;
export function increment() {
count++;
}
// arquivo: index.mjs
import { count, increment } from './counter.mjs';
console.log(count); // 0
increment();
console.log(count); // 1
4. Diferenças Chave entre CommonJS e ES Modules
Característica | CommonJS | ES Modules |
---|---|---|
Sintaxe | require / module.exports | import / export |
Carregamento | Síncrono | Assíncrono |
Extensão do Arquivo | .js (padrão) | .mjs ou .js com "type": "module" |
Escopo | Escopo isolado, mas não estrito | Modo estrito por padrão |
Tree Shaking | Não suportado | Suportado |
Live Bindings | Cópia de valores | Vinculação ao vivo |
Top-Level Await | Não suportado | Suportado |
Compatibilidade | Ampla (todos os Node.js, pacotes npm) | Nativo desde Node.js 12, mas requer configuração |
Uso Dinâmico | require(variavel) | import() (importação dinâmica) |
4.1. Exemplo de Diferenças Práticas
CommonJS:
// math.js
module.exports = {
soma: (a, b) => a + b
};
// index.js
const { soma } = require('./math');
console.log(soma(2, 3)); // 5
ES Modules:
// math.mjs
export const soma = (a, b) => a + b;
// index.mjs
import { soma } from './math.mjs';
console.log(soma(2, 3)); // 5
4.2. Configuração no Node.js
Para usar ES Modules no Node.js, você deve:
- Usar a extensão
.mjs
para arquivos ESM. - Ou adicionar
"type": "module"
aopackage.json
:json{ "name": "meu-projeto", "type": "module" }
- Para CommonJS, o padrão é
"type": "commonjs"
(ou ausência do campo).
4.3. Interoperabilidade
Misturar CommonJS e ES Modules pode ser desafiador:
Importar CommonJS em ESM:
javascriptimport { createRequire } from 'module'; const require = createRequire(import.meta.url); const modulo = require('./modulo.cjs');
Ou usar importação dinâmica:
javascriptconst modulo = await import('./modulo.cjs');
Importar ESM em CommonJS:
javascriptconst { soma } = await import('./math.mjs');
Para projetos grandes, ferramentas como Babel ou esbuild podem converter entre formatos.
5. Quando Usar CommonJS vs. ES Modules?
Use CommonJS:
- Em projetos legados ou pacotes npm que não suportam ESM.
- Quando a simplicidade e a compatibilidade são prioridades.
- Em scripts rápidos onde a configuração extra do ESM não é necessária.
Use ES Modules:
- Em projetos novos ou modernos (recomendado em 2025).
- Quando você precisa de tree shaking ou integração com navegadores.
- Para aproveitar recursos como top-level await e sintaxe moderna.
- Em projetos full-stack onde a consistência entre frontend e backend é importante.
Em 2025, ES Modules é o padrão recomendado para novos projetos Node.js devido à sua integração com o ecossistema JavaScript moderno e suporte a ferramentas como Vite, Rollup e TypeScript.
Exemplo Prático: Criar e Importar um Módulo Personalizado
Neste exemplo prático, vamos criar um módulo personalizado para realizar operações matemáticas (soma, subtração e multiplicação) e importá-lo em um arquivo principal. Implementaremos o exemplo em CommonJS e ES Modules para demonstrar as diferenças na prática.
Objetivo do Exemplo
- Criar um módulo personalizado chamado
math
que exporta funções matemáticas. - Importar o módulo em um arquivo principal (
index.js
ouindex.mjs
) e usar suas funções. - Testar ambos os formatos (CommonJS e ES Modules) em um projeto Node.js.
Passo 1: Configurar o Projeto
Crie uma nova pasta para o projeto:
bashmkdir modulo-personalizado cd modulo-personalizado npm init -y
O
package.json
será criado com configurações padrão:json{ "name": "modulo-personalizado", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC" }
Para ES Modules, modifique o
package.json
(opcional, usado na segunda parte):json{ "name": "modulo-personalizado", "version": "1.0.0", "type": "module", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC" }
Instale o
nodemon
para reiniciar o servidor automaticamente (opcional):bashnpm install --save-dev nodemon
Adicione ao
package.json
:json"scripts": { "start": "node index.js", "dev": "nodemon index.js" }
Passo 2: Implementação com CommonJS
Criar o Módulo Personalizado: Crie um arquivo
math.js
:javascript// math.js function soma(a, b) { return a + b; } function subtracao(a, b) { return a - b; } function multiplicacao(a, b) { return a * b; } module.exports = { soma, subtracao, multiplicacao };
Importar e Usar o Módulo: Crie um arquivo
index.js
:javascript// index.js const math = require('./math'); console.log('Soma: ', math.soma(5, 3)); // Soma: 8 console.log('Subtração: ', math.subtracao(5, 3)); // Subtração: 2 console.log('Multiplicação: ', math.multiplicacao(5, 3)); // Multiplicação: 15
Executar o Projeto:
bashnode index.js
Ou, com nodemon:
bashnpm run dev
Saída esperada:
Soma: 8 Subtração: 2 Multiplicação: 15
Explicação:
- O módulo
math.js
exporta um objeto contendo três funções usandomodule.exports
. - O
index.js
importa o módulo comrequire
e acessa as funções viamath.soma
,math.subtracao
, etc. - O carregamento é síncrono, e o Node.js armazena o módulo em cache após a primeira importação.
- O módulo
Passo 3: Implementação com ES Modules
Atualizar o package.json: Certifique-se de que o
package.json
inclui:json"type": "module"
Criar o Módulo Personalizado: Crie um arquivo
math.mjs
:javascript// math.mjs export function soma(a, b) { return a + b; } export function subtracao(a, b) { return a - b; } export function multiplicacao(a, b) { return a * b; } // Exportação padrão (opcional) export default { soma, subtracao, multiplicacao };
Importar e Usar o Módulo: Crie um arquivo
index.mjs
:javascript// index.mjs import { soma, subtracao, multiplicacao } from './math.mjs'; // Ou, usando importação padrão: // import math from './math.mjs'; console.log('Soma: ', soma(5, 3)); // Soma: 8 console.log('Subtração: ', subtracao(5, 3)); // Subtração: 2 console.log('Multiplicação: ', multiplicacao(5, 3)); // Multiplicação: 15
Executar o Projeto:
bashnode index.mjs
Ou, com nodemon:
bashnpm run dev
Saída esperada:
Soma: 8 Subtração: 2 Multiplicação: 15
Explicação:
- O módulo
math.mjs
usaexport
para exportar funções individualmente e, opcionalmente, um objeto padrão comexport default
. - O
index.mjs
importa as funções comimport
, especificando o caminho com a extensão.mjs
. - O carregamento é assíncrono, e os ES Modules suportam live bindings, permitindo que alterações no módulo original sejam refletidas.
- O módulo
Passo 4: Testando Diferenças com Live Bindings
Para demonstrar os live bindings dos ES Modules, vamos criar um exemplo que mostra a diferença em relação ao CommonJS.
CommonJS (Cópia de Valores):
javascript// counter.js let count = 0; function increment() { count++; console.log('Count no módulo:', count); } module.exports = { count, increment };
javascript// index.js const { count, increment } = require('./counter'); console.log('Count inicial:', count); // 0 increment(); console.log('Count após increment:', count); // 0 (não reflete a mudança)
ES Modules (Live Bindings):
javascript// counter.mjs export let count = 0; export function increment() { count++; console.log('Count no módulo:', count); }
javascript// index.mjs import { count, increment } from './counter.mjs'; console.log('Count inicial:', count); // 0 increment(); console.log('Count após increment:', count); // 1 (reflete a mudança)
Executar:
- Para CommonJS:
node index.js
- Para ES Modules:
node index.mjs
Saída do CommonJS:
Count inicial: 0 Count no módulo: 1 Count após increment: 0
Saída do ES Modules:
Count inicial: 0 Count no módulo: 1 Count após increment: 1
- Para CommonJS:
Explicação:
- No CommonJS,
count
é uma cópia do valor exportado, então mudanças no módulo original não afetam o valor importado. - Nos ES Modules,
count
é uma referência ao vivo, refletindo mudanças no módulo original.
- No CommonJS,
Passo 5: Boas Práticas
Organize Módulos:
- Crie uma pasta
lib
ouutils
para módulos reutilizáveis. - Nomeie arquivos de forma descritiva (ex.:
mathOperations.js
).
- Crie uma pasta
Use ES Modules em Projetos Novos:
- Configure
"type": "module"
nopackage.json
para consistência. - Prefira
.js
com ESM em vez de.mjs
para evitar extensões específicas.
- Configure
Evite Misturar Formatos:
- Escolha um sistema (CommonJS ou ESM) para todo o projeto para evitar problemas de interoperabilidade.
- Use
import()
dinâmico para casos excepcionais.
Versionamento:
- Adicione os arquivos ao Git:bash
git init git add . git commit -m "Módulo personalizado com CommonJS e ES Modules"
- Adicione os arquivos ao Git:
Depuração:
- Use o VS Code para depurar:
- Adicione breakpoints nos arquivos
index.js
ouindex.mjs
. - Configure o
launch.json
para ESM, se necessário:json{ "version": "0.2.0", "configurations": [ { "type": "node", "request": "launch", "name": "Launch ESM", "program": "${workspaceFolder}/index.mjs" } ] }
- Adicione breakpoints nos arquivos
- Use o VS Code para depurar:
Conclusão
Nesta aula, você aprendeu:
- CommonJS: Sistema de módulos síncrono, usando
require
emodule.exports
, ideal para projetos legados. - ES Modules: Padrão moderno, assíncrono, com
import
eexport
, recomendado para projetos novos em 2025. - Diferenças: Carregamento, sintaxe, live bindings, tree shaking e interoperabilidade.
- Exemplo Prático: Criou um módulo personalizado (
math
) e o importou em ambos os formatos, testando diferenças como live bindings.
Este conhecimento é fundamental para organizar projetos Node.js de forma modular e escalável. Nos próximos módulos, você aplicará esses conceitos para construir APIs, manipular arquivos e integrar bancos de dados.
Próximos Passos
- Experimente criar novos módulos personalizados (ex.: um módulo para strings ou cálculos avançados).
- Explore a documentação de módulos no Node.js: nodejs.org/api/modules.html.
- Prepare-se para o próximo módulo, onde abordaremos o Sistema de Arquivos (fs module).
Se tiver dúvidas ou quiser mais exemplos, é só pedir! 🚀