Artigo original: Node Module Exports Explained – With JavaScript Export Function Examples
Uma das coisas mais poderosas a respeito do desenvolvimento de software é a capacidade de reutilizar e criar com base naquilo que outras pessoas deixaram. È esse compartilhamento de código que acabou permitindo que o software avançasse imensamente.
Um mecanismo tão fantástico é fundamental em um nível mais profundo em termos de projetos individuais e de equipes.
Para o Node.js, esse processo de compartilhamento de código – tanto em projetos individuais quanto em dependências externas do npm – é facilitado pelo uso de module.exports
ou exports
.
Como funcionam os módulos do Node
Como usamos as exportações de módulos para adicionar um módulo externo ou dividir nosso projeto de modo razoável em diversos arquivos (módulos)?
O sistema de módulos do Node.js foi criado porque os criadores não queriam passar pelo mesmo problema do escopo global quebrado, como ocorria com o equivalente no navegador. Eles implementaram a especificação do CommonJS para conseguir isso.
As duas partes importantes do quebra-cabeças são o module.exports
e a função require
.
Como o module.exports funciona
module.exports
é, de fato, uma propriedade do objeto module
. É esta a aparência do objeto module
quando usamos console.log(module)
:
Module {
id: '.',
path: '/Users/stanleynguyen/Documents/Projects/blog.stanleynguyen.me',
exports: {},
parent: null,
filename: '/Users/stanleynguyen/Documents/Projects/blog.stanleynguyen.me/index.js',
loaded: false,
children: [],
paths: [
'/Users/stanleynguyen/Documents/Projects/blog.stanleynguyen.me/node_modules',
'/Users/stanleynguyen/Documents/Projects/node_modules',
'/Users/stanleynguyen/Documents/node_modules',
'/Users/stanleynguyen/node_modules',
'/Users/node_modules',
'/node_modules'
]
}
O objeto acima, basicamente, descreve um módulo encapsulado de um arquivo do JS, com module.exports
sendo o componente exportado de todos os tipos – objeto, função, string e outros. A exportação padrão em um módulo do Node.js é simples assim:
module.exports = function umaFuncaoExportada() {
return "Sim, é simples desse jeito.";
};
Existe uma outra forma de exportar a partir de um módulo do Node.js chamada "exportação nomeada". Em vez de atribuir todo o module.exports
a um valor, atribuímos propriedades individuais do objeto module.exports
padrão a valores. Algo assim:
module.exports.anExportedFunc = () => {};
module.exports.anExportedString = "Esta string é exportada.";
// ou empacotada juntamente em um objeto
module.exports = {
anExportedFunc,
anExportedString,
};
A exportação nomeada também pode ser feita de modo mais conciso com a variável predefinida exports
de escopo dos módulos, assim:
exports.anExportedFunc = () => {};
exports.anExportedString = "Esta string é exportada.";
Porém, atribuir toda a variável exports
a um novo valor não funcionará (discutiremos o motivo para isso na próxima seção), o que geralmente confunde os desenvolvedores do Node.js.
// Isto não funcionará como poderíamos esperar
exports = {
anExportedFunc,
anExportedString,
};
Imagine que as exportações dos módulos do Node.js estão enviando contêineres, com module.exports
e exports
agindo como a equipe no porto para quem diríamos qual "envio" (ou seja, quais valores) queremos enviar para um "porto estrangeiro" (outro módulo do projeto).
Bem, "a exportação padrão" diria a module.exports
qual "envio" deveria ser enviado por navio, enquanto a "exportação nomeada" carregaria contêineres diferentes no navio que module.exports
enviará para outro porto.

Agora que enviamos o navio, como esses "portos estrangeiros" receberão os navios?
Como funciona a palavra-chave require no Node.js
No lado do receptor, os módulos do Node.js podem importar usando o valor exportado de require
.
Imagine que escrevemos isto em ship.js
:
...
module.exports = {
containerA,
containerB,
};
Podemos importar esse "envio" em nosso receiving-port.js
:
// importando o ship.js inteiro como uma única variável
const ship = require("./ship.js");
console.log(ship.containerA);
console.log(ship.containerB);
// ou importando diretamente os contêineres por meio da desestruturação de objetos
const { containerA, containerB } = require("./ship.js");
console.log(containerA);
console.log(containerB);
Um ponto importante a ser observado sobre esse operador do porto estrangeiro – o require
– é que a pessoa não muda de ideia quanto a receber envios que foram feitos pelo module.exports
de outro porto qualquer. Isso nos leva à próxima seção, onde trataremos de um ponto que normalmente causa confusão.
module.exports
x exports
– Qual a diferença e por que usar um ou outro?
Agora que vimos o básico sobre exportação e requisição de módulos, é hora de tratar uma das fontes mais comuns de confusão nos módulos do Node.js.
Este é um erro de exportação de módulos que os iniciantes em Node.js geralmente cometem. Eles atribuem exports
a um novo valor, achando que é a mesma coisa que a "exportação padrão" usando module.exports
.
Isso, no entanto, não funciona porque:
require
somente usará o valor demodule.exports
exports
é uma variável com escopo de módulo que faz referência inicialmente amodule.exports
Assim, ao atribuir exports
a um novo valor, estamos efetivamente apontando o valor de exports
para outra referência que não é inicial do mesmo objeto module.exports
.
Se quiser saber mais a respeito dessa explicação técnica, a documentação oficial do Node.js é um bom ponto de partida.
Voltando à analogia que fizemos anteriormente usando navios e operadores: exports são outra equipe portuária que poderíamos informar sobre o navio que está partindo. No início, tanto module.exports
, quanto exports
têm a mesma informação sobre o "navio" de partida.
Se dissermos, no entanto, às exportações que o navio de saída será diferente (isto é, se atribuíssemos exports
a um valor completamente novo), o que quer que digamos depois (como atribuir propriedades de exports
a valores) não estará no navio que module.exports
está realmente preparando para ser recebido por require.
Por outro lado, se apenas disséssemos às exportações para "carregar alguns contêineres no navio de saída" (atribuindo propriedades de exports
ao valor), na verdade, acabaríamos carregando "contêineres" (isto é, valores de propriedade) no navio que está sendo enviado.
Com base no erro comum explicado acima, poderíamos definitivamente desenvolver algumas boas convenções em torno do uso de módulos CommonJS no Node.js.
Melhores práticas de exportação para o Node.JS – uma alternativa sensata
Logicamente, a convenção oferecida abaixo é inteiramente baseada em minhas próprias análises e raciocínio. Se tiver uma alternativa para a qual você realmente acha que pode melhorar a situação, não hesite em me enviar uma mensagem pelo Twitter a respeito.
O principal que desejo conseguir com essa convenção é:
- eliminar a confusão entre
exports
emodule.exports
- facilitar a leitura e melhorar a observabilidade com relação à exportação de módulos
Minha proposta é, portanto, consolidar os valores exportados na parte de baixo do arquivo, assim:
// exportação padrão
module.exports = function defaultExportedFunction() {};
// exportação nomeada
module.exports = {
something,
anotherThing,
};
Ao fazer isso, eliminaríamos as desvantagens em termos de concisão que module.exports
têm em comparação com a abreviação exports
. Isso removeria todos os incentivos para que usemos os exports
, confusos e potencialmente danosos.
Esta prática ainda facilitaria para que os leitores do código o lessem e aprendessem sobre os valores exportados de um módulo específico.
Indo além do CommonJS
Existe um padrão novo e, obviamente, melhor que foi introduzido há pouco no Node.js, chamamos de módulos do ECMAScript
. Os módulos do ECMAScript costumavam estar disponíveis apenas em código que precisaria mais tarde de uma transpilação do Babel, ou como parte de um recurso experimental do Node.js, versões 12 ou posteriores.
É uma maneira bastante simples e elegante de lidar com a exportação de módulos. Uma ideia geral a seu respeito pode ser resumida com a exportação padrão (em inglês, default) a seguir:
export default function exportedFunction() {}
Já as exportações nomeadas teriam esta aparência:
// Exportações nomeadas em linhas separadas
export const constantString = "CONSTANT_STRING";
export const constantNumber = 5;
// Exportações nomeadas consolidadas
export default {
constantString,
constantNumber,
};
Esses valores, podem, então, ser facilmente importados pelo receptor, assim:
// valores padrão importados
import exportedFunction from "exporting-module.js";
// importar valores das exportações nomeadas por meio da desestruturação de objetos
import { constantString, constantNumber } from "exporting-module.js";
Isso termina com a confusão entre module.exports
e exports
com uma sintaxe bonita e que qualquer um pode entender!
Existem, certamente, projetos que ainda serão migrados para a versão 14 ou posterior do Node.js e que ainda não poderão usar essa sintaxe nova.
Porém, se você tiver a oportunidade de fazer isso (por estar começando um novo projeto ou porque seu projeto foi migrado com sucesso para a versão 14 do Node.js ou posteriores), não há motivo para não mudar para este modo futurista e incrível de se olhar para a questão.
Obrigado pela leitura!
Para encerrar, se você gostou do que o autor escreveu, acesse o blog do autor para ver comentários semelhantes e siga-o no Twitter. 🎉