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.

ship-analogy
Minha analogia de barcos para a função do module.exports do Node.js

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 de module.exports
  • exports é uma variável com escopo de módulo que faz referência inicialmente a module.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 e module.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. 🎉