Dia 3 — Módulos: import e export
require e module.exports
require e module.exports
CommonJS é o sistema de módulos do Node desde o começo, e continua sendo o
padrão de qualquer projeto sem "type": "module" no package.json. A ideia é
simples: um arquivo exporta algo, outro arquivo importa. Importar é puxar o
valor de outro arquivo para o escopo atual; exportar é decidir o que desse
arquivo os outros podem ver.
// somar.js module.exports = function somar(a, b) { return a + b; }; // app.js const somar = require('./somar'); console.log(somar(2, 3));
Caminho: ./ é o arquivo ao lado
O caminho do módulo é o que vai entre aspas no require, e ele tem três
formas. ./ ou ../ é caminho relativo, resolvido a partir do arquivo que
está chamando. Um nome sem ./ é procurado dentro de node_modules — e é
assim que require('fs') acha um módulo nativo e require('mysql2') acha um
pacote instalado.
| Chamada | Onde procura |
|---|---|
require('./sala') | sala.js; se não existir, sala/index.js |
require('../config/db') | um nível acima, config/db.js |
require('node:fs') | módulo nativo do runtime |
require('mysql2') | dentro de node_modules |
Um módulo de pacote é o terceiro caso: mysql2 não é um arquivo seu, é o que o
npm install copiou para node_modules e o que vem com package.json e
versão própria. É a diferença entre o módulo nativo (node:fs, compilado
dentro do binário, sem arquivo em disco) e o pacote, que é uma pasta no seu
projeto.
O prefixo node: é opcional e serve para separar o que é nativo do runtime do
que é pacote publicado por alguém: require('node:fs') e require('fs') são o
mesmo objeto. Um detalhe que confunde: require.resolve('node:path') não
devolve um caminho, devolve node:path — módulo nativo não tem arquivo em disco,
ele é compilado dentro do binário. Só pacote instalado devolve caminho de verdade.
module.exports e exports são o mesmo objeto
exports é um apelido de module.exports criado antes do arquivo rodar. Isso
vale enquanto module.exports não for reatribuído:
exports.salvar = () => {}; // acrescenta uma propriedade module.exports = { salvar }; // substitui o objeto inteiro
Com as duas linhas, quem importa recebe { salvar } — o exports.salvar foi
escrito no objeto antigo, que ninguém mais vai ler. Por isso a regra prática: ou
o arquivo exporta um valor e usa module.exports = valor (uma função, uma
classe, um objeto), ou exporta várias coisas e usa exports.nome = valor do
começo ao fim, sem trocar de estilo no meio.
O módulo roda uma vez só
require executa o arquivo uma vez e guarda o resultado em require.cache.
Pedir o mesmo caminho de novo devolve a mesma referência, sem reexecutar. É por
isso que um estado guardado no nível do módulo sobrevive entre todos que
importam — e por isso que guardar conexão de banco em variável de módulo
funciona num script e vira vazamento num programa que deveria poder reiniciar
estado.
createRequire e require.resolve
require.resolve('mysql2') diz o caminho que o require carregaria, sem
carregar. createRequire(arquivo) monta um require ancorado naquele arquivo,
que é como se resolve um pacote a partir de um projeto diferente do diretório
atual.
O que o Node cria sozinho
require, module, exports, __dirname, __filename e process não vêm de
import nenhum: são variáveis que o Node cria para cada arquivo que executa.
__dirname é o diretório deste arquivo e process.cwd() é onde o comando
foi executado — são diferentes, e quem organiza pastas precisa saber qual dos dois
está usando.
Exemplo
'use strict'; // Exemplo da aula 1 do dia 3: CommonJS, o sistema de modulos do Node. // // `require` carrega um arquivo e devolve o que ele exportou. Duas formas fazem o // mesmo arquivo mudar de valor: `module.exports = valor` substitui tudo, e // `exports.nome = valor` acrescenta uma propriedade ao objeto. Sao o mesmo // objeto enquanto nada for reatribuido, e e por isso que trocar um pelo outro no // meio do arquivo tem consequencia. const fs = require('node:fs'); const path = require('node:path'); const { performance } = require('node:perf_hooks'); const { createRequire } = require('node:module'); // --- 1. caminho: relativo, absoluto e de pacote --- // // `./` e caminho RELATIVO ao arquivo que esta chamando, e `../` sobe um nivel. // Sem `./`, o Node procura no `node_modules` — e e assim que `require('fs')` // acha o modulo nativo e `require('mysql2')` acha um pacote do `npm install`. console.log('--- caminho ---'); console.log('relativo ao arquivo: ./aqui.js ->', path.join(__dirname, 'aqui.js')); console.log('subindo um nivel: ../sala.js ->', path.join(__dirname, '..', 'sala.js')); console.log("require('./sala') procura: sala.js, e se nao existir, sala/index.js"); // O prefixo `node:` diz "isto e nativo do runtime, nao pacote de ninguem". // `require('node:fs')` e `require('fs')` devolvem o mesmo objeto, o que se prova // comparando os dois `require` e nao os caminhos. console.log(''); console.log('--- nativo (node:) contra pacote (node_modules) ---'); console.log("require('node:fs') === require('fs'):", fs === require('fs')); console.log("o prefixo node: e opcional e nao muda nada:", require('node:fs') === require('fs')); // Um modulo nativo NAO tem arquivo: e compilado dentro do binario do Node. Por // isso o `resolve` dele devolve o proprio nome, e nao um caminho no disco. console.log("require.resolve('node:path') devolve:", require.resolve('node:path')); console.log('(e um pacote instalado devolve um caminho de verdade:'); console.log(' ' + createRequire(__filename).resolve('mysql2').split('node-mysql')[1] + ')'); // --- 2. o cache: o modulo roda uma vez so --- // // `require` executa o arquivo uma vez e guarda o resultado no cache do processo. // Pedir o mesmo caminho de novo devolve a MESMA referencia, sem reexecutar. E por // isso que um estado guardado no nivel do modulo sobrevive entre todos que // importam — e por isso que guardar conexao de banco em variavel de modulo // funciona num script e vaza memoria num programa de verdade. console.log(''); console.log('--- cache do require ---'); console.log('fs === require("node:fs"):', fs === require('node:fs')); console.log('performance e o mesmo objeto:', performance === require('node:perf_hooks').performance); console.log('modulos no cache depois de dois requires iguais:', Object.keys(require.cache).length); // --- 3. o arquivo define e exporta ao mesmo tempo --- // // Este arquivo e um modulo comum, sem nada de especial. `module.exports` pode // receber uma funcao direto — quem importa recebe a funcao, sem `.default` e sem // `.chave`. E a forma mais comum de exportar uma utilitaria. function somar(a, b) { return a + b; } module.exports = somar; // Para exportar varias coisas, cada uma vira uma propriedade do objeto // exportado. `exports` e um apelido de `module.exports`, valido enquanto o // `module.exports` nao for reatribuido — como foi na linha de cima. console.log(''); console.log('--- exportando deste arquivo ---'); console.log('typeof module.exports:', typeof module.exports); console.log('quem importa recebe:', module.exports.name, '(a funcao)'); console.log('somar(2, 3) aqui dentro:', somar(2, 3)); console.log('exports === module.exports:', exports === module.exports); console.log(' (false, e e o esperado: o `module.exports = somar` de cima trocou o'); console.log(' objeto, e o apelido `exports` continua apontando para o antigo)'); // --- 4. as variaveis que o Node cria sozinho --- // // `require`, `module`, `exports`, `__dirname`, `__filename` e `process` nao vem // de nenhum import: sao variaveis que o proprio Node cria para cada arquivo que // ele executa. Nenhuma delas existe no navegador. // // O detalhe que o `typeof` desta funcao revela: as cinco primeiras sao do // ESCOPO DO MODULO, e nao globais. `globalThis.require` e `undefined` — o // `require` existe dentro do arquivo, e nao em todo lugar. `process`, esse sim, // e global, e por isso que qualquer pacote instalado consegue ler `process.env`. function tiposDasVariaveis() { return { require: typeof require, module: typeof module, exports: typeof exports, __dirname: typeof __dirname, __filename: typeof __filename, 'globalThis.require': typeof globalThis.require, 'globalThis.__dirname': typeof globalThis.__dirname, process: typeof process, 'globalThis.process': typeof globalThis.process, }; } console.log(''); console.log('--- criadas pelo Node, sem import ---'); for (const [nome, tipo] of Object.entries(tiposDasVariaveis())) { console.log(' ' + nome.padEnd(20) + ' => ' + tipo); } console.log(' (as cinco primeiras so existem DENTRO do arquivo; process e global)'); console.log(''); console.log('__dirname e o diretorio DESTE arquivo:', __dirname); console.log('process.cwd() e onde o comando foi executado:', process.cwd()); console.log('sao diferentes de proposito: quem chama o node escolhe o cwd');
Saída real
--- caminho ---
relativo ao arquivo: ./aqui.js -> /root/materiais/node-mysql/codigo/t2/dia03/aqui.js
subindo um nivel: ../sala.js -> /root/materiais/node-mysql/codigo/t2/sala.js
require('./sala') procura: sala.js, e se nao existir, sala/index.js
--- nativo (node:) contra pacote (node_modules) ---
require('node:fs') === require('fs'): true
o prefixo node: e opcional e nao muda nada: true
require.resolve('node:path') devolve: node:path
(e um pacote instalado devolve um caminho de verdade:
/node_modules/mysql2/index.js)
--- cache do require ---
fs === require("node:fs"): true
performance e o mesmo objeto: true
modulos no cache depois de dois requires iguais: 1
--- exportando deste arquivo ---
typeof module.exports: function
quem importa recebe: somar (a funcao)
somar(2, 3) aqui dentro: 5
exports === module.exports: false
(false, e e o esperado: o `module.exports = somar` de cima trocou o
objeto, e o apelido `exports` continua apontando para o antigo)
--- criadas pelo Node, sem import ---
require => function
module => object
exports => object
__dirname => string
__filename => string
globalThis.require => undefined
globalThis.__dirname => undefined
process => object
globalThis.process => object
(as cinco primeiras so existem DENTRO do arquivo; process e global)
__dirname e o diretorio DESTE arquivo: /root/materiais/node-mysql/codigo/t2/dia03
process.cwd() e onde o comando foi executado: /root/materiais/node-mysql
sao diferentes de proposito: quem chama o node escolhe o cwd
import e export com ESM
import e export com ESM
ESM é o outro sistema de módulos do Node, e o padrão da linguagem.
O módulo ES é o arquivo que o Node trata com import/export, e ele só
funciona em arquivo que o Node não trata como CommonJS: um .mjs, ou um
.js num projeto com "type": "module" no package.json.
// util.mjs export const PI = 3.14; export function dobro(x) { return x * 2; } export default class Caixa { /* ... */ } // app.mjs import Caixa, { PI, dobro as vezes2 } from './util.mjs';
O que está no exemplo é a forma nomeada: o import { } é o import nomeado, e
o que vem entre aspas depois do from é o caminho do módulo. O nome entre
colchetes é o que existe no módulo exportador. Com as vezes2 quem importa
está fazendo renomear no import, porque o nome local pode ser qualquer
identificador válido, enquanto o exportado é o do outro arquivo.
As quatro formas de exportar
| Forma | O que sai | Quem importa |
|---|---|---|
export function f() {} | exportado com nome | import { f } |
export default algo | o default, sem nome | import algo from |
export { a as b } | renomeia o que sai | import { b } |
export * from './x' | repassa todos os nomes | import { ... } |
O export { } é a lista do que sai, e ela existe separada do export colado
na declaração porque resolve um caso que o outro não alcança: um arquivo que
recebe o valor por require de outro e precisa repassá-lo sem mudar o nome
público. export { lerConfig } no fim do arquivo é o mesmo
export function lerConfig do começo, e o que muda é a ordem de escrita.
O export default é o único que não tem nome: quem importa decide como chamá-lo.
É a razão de existir o alias — import { default as Caixa } importa o default
com outro nome, porque default é palavra reservada e não serve como variável.
O namespace: o que o import entrega
O que o import dá é um objeto namespace: todos os exportados com nome, mais
o default na chave default.
import * as tudo from './util.mjs'; tudo.dobro(2); // exportado com nome tudo.default; // o export default
import * as traz o mesmo objeto do import nomeado, e por isso é raro: quando
se sabe o que quer, import { f } é mais legível.
import() dinâmico e top-level await
O import estático tem duas limitações: precisa estar no começo do arquivo e
escreve o caminho no código. O import() dinâmico é uma expressão — pode
aparecer no meio, receber caminho montado em tempo de execução, e roda só quando
é alcançada. É ele que a documentação chama de import dinâmico, e é a ponte
entre os dois sistemas, porque funciona dentro de CommonJS.
const modulo = await import(caminhoMontadoEmTempoDeExecucao);
O top-level await é a outra metade: em ESM, await funciona no nível do
arquivo, sem envolver tudo numa async function. Em CommonJS isso é
SyntaxError. O exemplo desta aula é CommonJS e usa import() dentro de
main() por esse motivo — em ESM as duas linhas do começo ficariam no topo do
arquivo, sem função nenhuma.
import.meta
import.meta só existe em ESM. import.meta.url é o caminho do arquivo, e
equivale ao __filename do CommonJS, com a diferença de que é uma URL.
console.log(import.meta.url); // file:///caminho/do/arquivo.mjs
Como é uma URL, new URL(import.meta.url).pathname devolve o caminho do
sistema quando o que se quer é passar para uma biblioteca de arquivos.
O que não se mistura
Um arquivo é CommonJS ou ESM, nunca os dois. require em arquivo ESM não
existe, e module.exports em arquivo ESM não exporta nada — sem erro, o que é
pior. Quem decide é o package.json, e a extensão .mjs ganha do package.json
qualquer que seja o type.
Exemplo
'use strict'; // Exemplo da aula 2 do dia 3: ESM, o outro sistema de modulos. // // ESM e `import`/`export`. Ele so funciona em arquivo que o Node NAO trata como // CommonJS: um `.mjs`, ou um `.js` num projeto com `"type": "module"`. // // O `package.json` deste material diz `"type": "commonjs"`, entao ESTE arquivo e // CommonJS e nao aceita `import` nem `await` no nivel do arquivo. E por isso que // o exemplo usa `import()` dinamico: ele funciona de dentro de CommonJS, e e a // ponte entre os dois sistemas. O modulo do outro lado (`modulo-esm.mjs`) e ESM // de verdade, e e ele que a aula importa. async function main() { // `import()` e uma EXPRESSAO: pode estar no meio do codigo, receber um // caminho montado em tempo de execucao, e roda so quando e alcancada. O // `import` estatico, ao contrario, tem de ser a primeira coisa do arquivo e // roda sempre. const modulo = await import('./modulo-esm.mjs'); const { somaDois, versaoMaterial, agora, onde } = modulo; const Mensagem = modulo.default; console.log('--- import nomeado e default ---'); console.log('nomeado somaDois(2, 3) =', somaDois(2, 3)); console.log('default new Mensagem("oi") =', new Mensagem('oi').ver()); console.log('const versaoMaterial =', versaoMaterial); // --- o namespace --- // // O que o `import` entrega e um OBJETO namespace: tudo que foi exportado com // nome, mais o `export default` na chave `default`. Ver a lista do que um // modulo exporta nao exige ler o arquivo dele. console.log(''); console.log('--- o namespace do modulo ---'); console.log('exportados com nome:', Object.keys(modulo).filter((k) => k !== 'default').join(', ')); console.log('export default: default ->', modulo.default.name); console.log('o namespace nao traz variavel local do modulo, nem nada de outro modulo'); // --- alias nos dois lados --- // // `export { x as y }` renomeia o que sai; `import { y as x }` renomeia o que // entra. Sao o mesmo mecanismo apontado em direcoes opostas. console.log(''); console.log('--- alias ---'); console.log('no modulo: const soma = ... ; export { soma as somaDois }'); console.log('aqui: const { somaDois } = await import(...)'); console.log('o nome interno `soma` nao sai do modulo:', modulo.soma === undefined); console.log('o nome exportado `somaDois` sai:', typeof modulo.somaDois); console.log('e a mesma funcao que a aula importou:', modulo.somaDois === somaDois); // `import * as` traz tudo que tem nome; o default fica em `.default`. const { default: Default, ...resto } = modulo; console.log('import * as traz', Object.keys(resto).length, 'exportados com nome e o default a parte'); console.log(' "default" continua acessivel:', new Default('mesma classe').ver()); // --- `import()` com caminho montado --- // // Nada impede que o caminho seja uma variavel. Com `import` estatico isso nao // existe: o caminho e escrito no codigo, e o Node o resolve antes de rodar. const caminho = 'node:' + 'os'; const moduloOs = await import(caminho); console.log(''); console.log('--- import() com caminho construido ---'); console.log('import("' + caminho + '") -> modulo nativo do Node'); console.log('type():', moduloOs.type(), '| platform():', moduloOs.platform()); // --- import.meta: so existe em ESM --- // // O modulo exporta o que ele proprio ve: `import.meta.url` e o caminho do // arquivo de ONDE a funcao foi definida, nao de onde ela foi chamada. No // CommonJS o equivalente e `__filename`. console.log(''); console.log('--- import.meta, do lado de dentro do modulo ---'); console.log('import.meta.url:', onde()); console.log('como caminho: ', new URL(onde()).pathname); console.log('o `await import("./modulo-esm.mjs")` deste arquivo so funciona'); console.log('porque `import()` e valido dentro de uma funcao `async` em CommonJS;'); console.log('em ESM o mesmo codigo nem precisaria dela.'); // --- o que nao se mistura --- console.log(''); console.log('--- a regra que decide tudo ---'); console.log('package.json "type": "commonjs" -> require / module.exports, sem top-level await'); console.log('package.json "type": "module" -> import / export, com top-level await'); console.log('arquivo com extensao .mjs -> ESM, qualquer que seja o type'); console.log('os dois no mesmo arquivo -> nao funciona, e a falha nao avisa'); console.log(''); console.log('este arquivo e CommonJS, e rodou mesmo assim.'); } main().catch((erro) => { console.error('falhou:', erro.code || erro.name, '-', erro.message); process.exit(1); });
Saída real
[modulo-esm.mjs] este arquivo rodou no momento do import, uma vez so
--- import nomeado e default ---
nomeado somaDois(2, 3) = 5
default new Mensagem("oi") = Mensagem: oi
const versaoMaterial = t2/dia03
--- o namespace do modulo ---
exportados com nome: PI, agora, onde, somaDois, versaoMaterial
export default: default -> Mensagem
o namespace nao traz variavel local do modulo, nem nada de outro modulo
--- alias ---
no modulo: const soma = ... ; export { soma as somaDois }
aqui: const { somaDois } = await import(...)
o nome interno `soma` nao sai do modulo: true
o nome exportado `somaDois` sai: function
e a mesma funcao que a aula importou: true
import * as traz 5 exportados com nome e o default a parte
"default" continua acessivel: Mensagem: mesma classe
--- import() com caminho construido ---
import("node:os") -> modulo nativo do Node
type(): Linux | platform(): linux
--- import.meta, do lado de dentro do modulo ---
import.meta.url: file:///root/materiais/node-mysql/codigo/t2/dia03/modulo-esm.mjs
como caminho: /root/materiais/node-mysql/codigo/t2/dia03/modulo-esm.mjs
o `await import("./modulo-esm.mjs")` deste arquivo so funciona
porque `import()` e valido dentro de uma funcao `async` em CommonJS;
em ESM o mesmo codigo nem precisaria dela.
--- a regra que decide tudo ---
package.json "type": "commonjs" -> require / module.exports, sem top-level await
package.json "type": "module" -> import / export, com top-level await
arquivo com extensao .mjs -> ESM, qualquer que seja o type
os dois no mesmo arquivo -> nao funciona, e a falha nao avisa
este arquivo e CommonJS, e rodou mesmo assim.