Módulos: organizar o código em arquivos.
O problema do escopo global. ES Modules com import e export. Named vs default exports. Organizando script.js em utils.js, api.js, ui.js e main.js.
À medida que o script.js cresce — toggle de tema, curtidas, barra de progresso, carregamento de artigos — ele começa a ficar longo e difícil de navegar. Funções de utilidades se misturam com manipulação do DOM que se mistura com chamadas fetch. Módulos permitem dividir esse código em arquivos com responsabilidades bem definidas.
O problema sem módulos
Antes de módulos existirem, todos os scripts compartilhavam o mesmo escopo global — o objeto window. Cada variável e função declaradas em qualquer <script> ficavam disponíveis em todos os outros:
<!-- script-a.js rodou primeiro e definiu formatarData -->
<script src="script-a.js"></script>
<!-- script-b.js também define formatarData — sobrescreve o anterior silenciosamente -->
<script src="script-b.js"></script> // script-a.js
function formatarData(date) {
return date.toLocaleDateString("pt-BR");
}
// script-b.js — declara a mesma função com implementação diferente
function formatarData(date) {
return date.toISOString(); // sobrescreve a primeira silenciosamente
}
// qualquer código que usa formatarData agora usa a versão do script-b
// sem erro, sem aviso — bug difícil de rastrear Além da colisão de nomes, a ordem dos <script> no HTML importa e é frágil — se você troca a ordem, o código quebra.
ES Modules (ESM)
O sistema de módulos nativo do JavaScript resolve todos esses problemas. Ativa com type="module" no <script>:
<head>
<!-- type="module": habilita import/export, defer implícito, strict mode -->
<script type="module" src="main.js"></script>
<!-- não é necessário declarar os outros arquivos — main.js os importa -->
</head> Com type="module":
- Cada arquivo tem seu próprio escopo — variáveis não vazam para o
window importeexportfuncionamstrict modeé ativado automaticamente —varainda funciona mas erros silenciosos do modo não-strict se tornam erros reais- O script tem
deferimplícito — executa após o HTML estar completamente parseado
Export
Você exporta o que outros módulos podem usar. Há duas formas:
Named export: exporta com um nome específico. Um arquivo pode ter múltiplos named exports. O importador usa o nome exato (ou renomeia com as).
Default export: exporta o valor principal do arquivo. Um arquivo pode ter apenas um default export. O importador pode usar qualquer nome.
// named exports — múltiplos por arquivo
export function formatarData(date) {
return new Intl.DateTimeFormat("pt-BR", {
year: "numeric",
month: "long",
day: "numeric",
}).format(new Date(date));
}
export function calcularTempoLeitura(palavras) {
return Math.ceil(palavras / 238);
}
export function gerarSlug(titulo) {
return titulo
.toLowerCase()
.trim()
.replace(/\s+/g, "-")
.replace(/[^a-z0-9-]/g, "");
}
export function truncar(texto, max = 160) {
if (texto.length <= max) return texto;
return texto.slice(0, max).trimEnd() + "…";
}
// exportar no final — alternativa equivalente
// export { formatarData, calcularTempoLeitura, gerarSlug, truncar }; Import
Para usar o que foi exportado, use import no topo do arquivo:
// named imports — com chaves, usando o nome exato
import { formatarData, calcularTempoLeitura, truncar } from "./utils.js";
// renomear durante o import
import { formatarData as formatar } from "./utils.js";
// importar tudo em um namespace
import * as utils from "./utils.js";
utils.formatarData(new Date()); // acessar via namespace
// default import — qualquer nome funciona
// import minhaFuncao from "./modulo.js"; O caminho no import deve ser relativo e incluir a extensão .js — diferente de bundlers como Vite, onde a extensão é opcional.
Organizando o blog em módulos
Com a estrutura de módulos, o script.js monolítico se divide em quatro arquivos com responsabilidades distintas:
// funções que não dependem do DOM — fáceis de testar isoladamente
export function formatarData(dateString) {
return new Intl.DateTimeFormat("pt-BR").format(new Date(dateString));
}
export function calcularTempoLeitura(palavras) {
return Math.ceil(palavras / 238);
}
export function truncar(texto, max = 160) {
if (texto.length <= max) return texto;
return texto.slice(0, max).trimEnd() + "…";
} export async function buscarArtigos() {
const response = await fetch("/artigos.json");
if (!response.ok) throw new Error(`Erro ${response.status}`);
return response.json();
}
export async function enviarContato(dados) {
const response = await fetch("/contato", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(dados),
});
if (!response.ok) throw new Error(`Erro ${response.status}`);
return response.json();
} import { formatarData, truncar } from "./utils.js";
export function renderizarCards(artigos, curtidos) {
return artigos.map(a => `
<article class="card-artigo" data-id="${a.id}">
<h2>${a.titulo}</h2>
<p>${truncar(a.descricao)}</p>
<time datetime="${a.publicadoEm}">${formatarData(a.publicadoEm)}</time>
<button class="botao-curtir" aria-pressed="${curtidos.includes(a.id)}">
${curtidos.includes(a.id) ? "❤️" : "🤍"} Curtir
</button>
</article>
`).join("");
}
export function exibirNotificacao(tipo, mensagem) {
const notif = document.getElementById("notificacao");
if (!notif) return;
notif.className = `notificacao notificacao-${tipo}`;
notif.textContent = mensagem;
notif.hidden = false;
setTimeout(() => { notif.hidden = true; }, 4000);
} import { buscarArtigos, enviarContato } from "./api.js";
import { renderizarCards, exibirNotificacao } from "./ui.js";
// ============================================================
// TOGGLE DE TEMA
// ============================================================
const botaoTema = document.getElementById("botao-tema");
const html = document.documentElement;
const temaSalvo = localStorage.getItem("tema") ?? "claro";
if (temaSalvo === "dark") {
html.setAttribute("data-theme", "dark");
}
botaoTema?.addEventListener("click", () => {
const temaDark = html.getAttribute("data-theme") === "dark";
const novoTema = temaDark ? "claro" : "dark";
html.setAttribute("data-theme", novoTema === "dark" ? "dark" : "");
if (novoTema === "claro") html.removeAttribute("data-theme");
localStorage.setItem("tema", novoTema);
});
// ============================================================
// CARREGAMENTO DE ARTIGOS
// ============================================================
const listaArtigos = document.getElementById("lista-artigos");
async function inicializar() {
if (!listaArtigos) return;
try {
const artigos = await buscarArtigos();
const curtidos = JSON.parse(localStorage.getItem("curtidos")) ?? [];
listaArtigos.innerHTML = renderizarCards(artigos.filter(a => a.publicado), curtidos);
} catch (erro) {
listaArtigos.innerHTML = "<p>Erro ao carregar artigos.</p>";
}
}
inicializar(); Resumo
- Sem módulos, todos os scripts compartilham o escopo global — colisão de nomes e dependência frágil de ordem de carregamento.
type="module"em<script>habilita ES Modules: escopo isolado por arquivo,import/exportfuncionam, strict mode ativado, defer implícito.- Named export (
export function f) — múltiplos por arquivo, importado com{ chaves }. Default export (export default) — um por arquivo, importado com qualquer nome. - Organização recomendada para o blog:
utils.js(funções puras),api.js(fetch),ui.js(DOM),main.js(ponto de entrada que importa e orquestra). - Escopo de módulo significa que você importa exatamente o que precisa — dependências são explícitas no código, não implícitas na ordem dos
<script>.
O que type='module' faz em uma tag script?
Qual é a diferença entre named export e default export?
Por que módulos têm defer implícito?
Por que organizar o código em múltiplos módulos é vantajoso?
Aula concluída
Quase lá.