10. Padrões de JavaScript
Este documento detalha os padrões e práticas recomendados para escrever JavaScript moderno, limpo e eficiente em projetos Astro 5, com foco em código vanilla sem dependências desnecessárias.
Princípios Fundamentais
Código Limpo e Legível
Priorize a legibilidade e a manutenibilidade do código:
// ❌ Evite código denso e difícil de lerconst fn=a=>a.filter(x=>x>10).map(x=>x*2).reduce((a,b)=>a+b,0);
// ✅ Prefira código claro e legívelconst processNumbers = (numbers) => { const filtered = numbers.filter(num => num > 10); const doubled = filtered.map(num => num * 2); return doubled.reduce((sum, num) => sum + num, 0);};Modularidade
Divida seu código em módulos pequenos e focados:
export function formatDate(date, locale = 'pt-BR') { return new Date(date).toLocaleDateString(locale);}
export function isToday(date) { const today = new Date(); const target = new Date(date); return ( target.getDate() === today.getDate() && target.getMonth() === today.getMonth() && target.getFullYear() === today.getFullYear() );}
// Uso em um componenteimport { formatDate, isToday } from '../utils/date.js';
const publishDate = '2025-03-21';const formattedDate = formatDate(publishDate);const isPublishedToday = isToday(publishDate);Imutabilidade
Prefira operações que não modificam os dados originais:
// ❌ Evite mutar arrays e objetosfunction addItem(items, newItem) { items.push(newItem); // Muta o array original return items;}
// ✅ Prefira operações imutáveisfunction addItem(items, newItem) { return [...items, newItem]; // Cria um novo array}
// ❌ Evite mutar objetosfunction updateUser(user, props) { Object.assign(user, props); // Muta o objeto original return user;}
// ✅ Prefira operações imutáveisfunction updateUser(user, props) { return { ...user, ...props }; // Cria um novo objeto}Sintaxe Moderna
Destructuring
Use destructuring para extrair valores de objetos e arrays:
// Destructuring de objetosconst user = { name: 'João', email: 'joao@exemplo.com', address: { city: 'São Paulo', country: 'Brasil' }};
// Extração básicaconst { name, email } = user;
// Renomeando variáveisconst { name: userName, email: userEmail } = user;
// Valores padrãoconst { age = 30 } = user;
// Destructuring aninhadoconst { address: { city, country } } = user;
// Destructuring de arraysconst colors = ['vermelho', 'verde', 'azul'];
// Extração básicaconst [primaryColor, secondaryColor] = colors;
// Ignorando elementosconst [, , tertiaryColor] = colors;
// Rest operatorconst [first, ...rest] = colors;
// Valores padrãoconst [main = 'preto', accent = 'branco'] = [];Spread Operator
Use o spread operator para combinar arrays e objetos:
// Combinando arraysconst fruits = ['maçã', 'banana'];const vegetables = ['cenoura', 'brócolis'];const food = [...fruits, ...vegetables];
// Adicionando elementosconst extendedFruits = [...fruits, 'laranja', 'morango'];
// Clonando arraysconst fruitsCopy = [...fruits];
// Combinando objetosconst userBasic = { name: 'Maria', email: 'maria@exemplo.com' };const userDetails = { age: 28, city: 'Rio de Janeiro' };const userComplete = { ...userBasic, ...userDetails };
// Sobrescrevendo propriedadesconst updatedUser = { ...userBasic, email: 'nova.maria@exemplo.com' };
// Clonando objetosconst userCopy = { ...userBasic };Optional Chaining
Use optional chaining para acessar propriedades aninhadas com segurança:
const user = { name: 'Pedro', address: { city: 'Belo Horizonte' }};
// ❌ Sem optional chaining (propenso a erros)const country = user.address && user.address.country;
// ✅ Com optional chainingconst country = user.address?.country;
// Combinando com operador nullish coalescingconst country = user.address?.country ?? 'Brasil';
// Em arraysconst firstCategory = user.categories?.[0];
// Em funçõesconst result = user.getDetails?.();Nullish Coalescing
Use o operador nullish coalescing para valores padrão:
// ❌ Operador OR pode causar problemas com valores falsyconst count = userCount || 10; // Se userCount for 0, count será 10
// ✅ Nullish coalescing só substitui null ou undefinedconst count = userCount ?? 10; // Se userCount for 0, count será 0
// Combinando com destructuringconst { items = [], count = 0 } = data ?? {};Funções
Arrow Functions
Use arrow functions para código mais conciso:
// Função tradicionalfunction add(a, b) { return a + b;}
// Arrow functionconst add = (a, b) => a + b;
// Com corpo de blococonst calculateTotal = (items) => { const subtotal = items.reduce((sum, item) => sum + item.price, 0); const tax = subtotal * 0.1; return subtotal + tax;};
// Em métodos de arrayconst numbers = [1, 2, 3, 4, 5];const doubled = numbers.map(num => num * 2);const even = numbers.filter(num => num % 2 === 0);Parâmetros Padrão
Use parâmetros padrão para valores opcionais:
// ❌ Verificação manual de parâmetrosfunction createUser(name, role) { role = role || 'user'; // ...}
// ✅ Parâmetros padrãofunction createUser(name, role = 'user') { // ...}
// Com destructuringfunction fetchData({ url, method = 'GET', headers = {} } = {}) { // ...}Rest Parameters
Use rest parameters para funções com número variável de argumentos:
// ❌ Usando arguments (não recomendado)function sum() { let total = 0; for (let i = 0; i < arguments.length; i++) { total += arguments[i]; } return total;}
// ✅ Usando rest parametersfunction sum(...numbers) { return numbers.reduce((total, num) => total + num, 0);}
// Combinando com parâmetros regularesfunction logItems(category, ...items) { console.log(`Categoria: ${category}`); items.forEach(item => console.log(`- ${item}`));}Padrões Assíncronos
Promises
Use Promises para operações assíncronas:
// Criando uma Promisefunction fetchUserData(userId) { return new Promise((resolve, reject) => { // Operação assíncrona fetch(`/api/users/${userId}`) .then(response => { if (!response.ok) { throw new Error('Falha ao buscar usuário'); } return response.json(); }) .then(data => resolve(data)) .catch(error => reject(error)); });}
// Encadeamento de PromisesfetchUserData(123) .then(user => fetchUserPosts(user.id)) .then(posts => { // Processar posts }) .catch(error => { // Tratar erro });
// Promise.all para operações paralelasPromise.all([ fetchUserData(123), fetchUserPosts(123), fetchUserComments(123)]) .then(([user, posts, comments]) => { // Todos os dados disponíveis }) .catch(error => { // Se qualquer Promise falhar });
// Promise.allSettled para operações independentesPromise.allSettled([ fetchUserData(123), fetchUserPosts(123), fetchUserComments(123)]) .then(results => { // results contém status e valor/razão para cada Promise });Async/Await
Use async/await para código assíncrono mais legível:
// Função assíncrona básicaasync function getUserData(userId) { try { const response = await fetch(`/api/users/${userId}`);
if (!response.ok) { throw new Error('Falha ao buscar usuário'); }
return await response.json(); } catch (error) { console.error('Erro:', error); throw error; }}
// Chamando função assíncronaasync function displayUserProfile(userId) { try { const user = await getUserData(userId); const posts = await getUserPosts(userId);
// Renderizar perfil com user e posts } catch (error) { // Tratar erro }}
// Operações paralelas com async/awaitasync function getUserProfile(userId) { try { // Executa em paralelo const [user, posts, comments] = await Promise.all([ getUserData(userId), getUserPosts(userId), getUserComments(userId) ]);
return { user, posts, comments }; } catch (error) { console.error('Erro ao buscar perfil:', error); throw error; }}Padrões de Design
Module Pattern
Organize código relacionado em módulos:
const storage = (() => { // Variáveis privadas const PREFIX = 'app_';
// Métodos privados function getKey(key) { return `${PREFIX}${key}`; }
// API pública return { get(key) { const value = localStorage.getItem(getKey(key)); try { return JSON.parse(value); } catch { return value; } },
set(key, value) { const serialized = typeof value === 'object' ? JSON.stringify(value) : value; localStorage.setItem(getKey(key), serialized); },
remove(key) { localStorage.removeItem(getKey(key)); },
clear() { Object.keys(localStorage) .filter(key => key.startsWith(PREFIX)) .forEach(key => localStorage.removeItem(key)); } };})();
export default storage;Factory Pattern
Crie objetos com uma interface consistente:
function createApiClient(baseUrl, options = {}) { const defaultHeaders = { 'Content-Type': 'application/json', ...options.headers };
async function request(endpoint, method, data = null, customHeaders = {}) { const url = `${baseUrl}${endpoint}`; const headers = { ...defaultHeaders, ...customHeaders };
const config = { method, headers, ...options.fetchOptions };
if (data) { config.body = JSON.stringify(data); }
try { const response = await fetch(url, config);
if (!response.ok) { throw new Error(`API error: ${response.status}`); }
// Verifica se a resposta é JSON const contentType = response.headers.get('content-type'); if (contentType && contentType.includes('application/json')) { return await response.json(); }
return await response.text(); } catch (error) { console.error('Request failed:', error); throw error; } }
return { get: (endpoint, customHeaders) => request(endpoint, 'GET', null, customHeaders),
post: (endpoint, data, customHeaders) => request(endpoint, 'POST', data, customHeaders),
put: (endpoint, data, customHeaders) => request(endpoint, 'PUT', data, customHeaders),
patch: (endpoint, data, customHeaders) => request(endpoint, 'PATCH', data, customHeaders),
delete: (endpoint, customHeaders) => request(endpoint, 'DELETE', null, customHeaders) };}
// Usoconst api = createApiClient('https://api.exemplo.com');const users = await api.get('/users');const newUser = await api.post('/users', { name: 'Ana', email: 'ana@exemplo.com' });Observer Pattern
Implemente um sistema de eventos para comunicação entre componentes:
function createEventBus() { const events = {};
return { subscribe(event, callback) { if (!events[event]) { events[event] = []; }
events[event].push(callback);
// Retorna função para cancelar inscrição return () => { events[event] = events[event].filter(cb => cb !== callback); }; },
publish(event, data) { if (!events[event]) { return; }
events[event].forEach(callback => { callback(data); }); },
clear(event) { if (event) { delete events[event]; } else { Object.keys(events).forEach(key => { delete events[key]; }); } } };}
// Usoconst eventBus = createEventBus();
// Componente Aconst unsubscribe = eventBus.subscribe('user:updated', (user) => { console.log('Usuário atualizado:', user); updateUI(user);});
// Componente Bfunction updateUser(userId, data) { api.put(`/users/${userId}`, data) .then(updatedUser => { eventBus.publish('user:updated', updatedUser); });}
// Cancelar inscrição quando não for mais necessáriounsubscribe();Manipulação de DOM
Seletores Modernos
Use métodos modernos para selecionar elementos do DOM:
// Selecionar um único elementoconst header = document.querySelector('.header');
// Selecionar múltiplos elementosconst buttons = document.querySelectorAll('.btn');
// Verificar se um elemento corresponde a um seletorconst isActive = element.matches('.active');
// Encontrar o elemento pai mais próximo que corresponda a um seletorconst card = element.closest('.card');
// Selecionar elementos filhosconst items = container.querySelectorAll(':scope > .item');Manipulação de Elementos
Métodos eficientes para manipular o DOM:
// Criar elementosconst button = document.createElement('button');button.textContent = 'Clique aqui';button.classList.add('btn', 'btn-primary');
// Adicionar atributosbutton.setAttribute('data-id', '123');button.id = 'submit-btn';
// Adicionar estilosbutton.style.backgroundColor = '#0077ff';button.style.padding = '8px 16px';
// Adicionar ao DOMcontainer.appendChild(button);
// Inserir em posição específicacontainer.insertBefore(button, container.firstChild);
// Métodos modernos de inserçãocontainer.append(button); // Adiciona ao finalcontainer.prepend(button); // Adiciona ao inícioexistingElement.before(button); // Insere antesexistingElement.after(button); // Insere depoisexistingElement.replaceWith(button); // Substitui
// Remover elementosbutton.remove();
// Clonar elementosconst clone = button.cloneNode(true); // true para clonar também os filhosManipulação de Classes
Trabalhe com classes de forma eficiente:
// Adicionar/remover classeselement.classList.add('active');element.classList.remove('disabled');element.classList.toggle('expanded');element.classList.replace('old-class', 'new-class');
// Verificar se possui uma classeif (element.classList.contains('selected')) { // ...}
// Adicionar várias classeselement.classList.add('card', 'shadow', 'rounded');Delegação de Eventos
Use delegação de eventos para melhor performance:
// ❌ Adicionar eventos a múltiplos elementos (menos eficiente)document.querySelectorAll('.btn').forEach(button => { button.addEventListener('click', handleClick);});
// ✅ Usar delegação de eventos (mais eficiente)document.addEventListener('click', (event) => { if (event.target.matches('.btn')) { handleClick(event); }});
// Função de manipulação de eventosfunction handleClick(event) { const button = event.target; const id = button.dataset.id;
// Lógica específica console.log(`Botão ${id} clicado`);}Otimizações de Performance
Debounce e Throttle
Limite a frequência de execução de funções:
// Debounce: executa a função apenas após um período de inatividadefunction debounce(func, delay = 300) { let timeout;
return function(...args) { clearTimeout(timeout); timeout = setTimeout(() => { func.apply(this, args); }, delay); };}
// Throttle: limita a execução a uma vez a cada períodofunction throttle(func, limit = 300) { let inThrottle;
return function(...args) { if (!inThrottle) { func.apply(this, args); inThrottle = true; setTimeout(() => { inThrottle = false; }, limit); } };}
// Uso com evento de scrollconst handleScroll = () => { // Lógica de scroll console.log('Scroll position:', window.scrollY);};
// Versão com debounce (executa apenas quando o usuário para de rolar)const debouncedScroll = debounce(handleScroll, 200);
// Versão com throttle (executa no máximo a cada 200ms durante rolagem)const throttledScroll = throttle(handleScroll, 200);
window.addEventListener('scroll', throttledScroll);Memoização
Cache de resultados de funções para melhor performance:
// Função de memoização simplesfunction memoize(fn) { const cache = new Map();
return function(...args) { const key = JSON.stringify(args);
if (cache.has(key)) { return cache.get(key); }
const result = fn.apply(this, args); cache.set(key, result); return result; };}
// Função custosafunction calculateFactorial(n) { if (n <= 1) return 1; return n * calculateFactorial(n - 1);}
// Versão memoizadaconst memoizedFactorial = memoize(calculateFactorial);
// Primeira chamada (calcula)console.time('first call');memoizedFactorial(20);console.timeEnd('first call');
// Segunda chamada (usa cache)console.time('second call');memoizedFactorial(20);console.timeEnd('second call');Lazy Loading
Carregue recursos apenas quando necessário:
// Lazy loading de módulosasync function loadFeature() { try { // Carrega o módulo apenas quando necessário const { initFeature } = await import('./features/special-feature.js');
// Inicializa o recurso initFeature(); } catch (error) { console.error('Falha ao carregar o recurso:', error); }}
// Botão que carrega o recursodocument.querySelector('#load-feature').addEventListener('click', loadFeature);
// Lazy loading de imagens com Intersection Observerfunction setupLazyImages() { const images = document.querySelectorAll('img[data-src]');
const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { const img = entry.target; img.src = img.dataset.src; img.removeAttribute('data-src'); observer.unobserve(img); } }); }, { rootMargin: '100px 0px' // Carrega imagens 100px antes de entrarem na viewport });
images.forEach(img => observer.observe(img));}
// Inicializar lazy loadingdocument.addEventListener('DOMContentLoaded', setupLazyImages);Integração com Astro
Scripts em Componentes Astro
Adicione JavaScript a componentes Astro:
---const { tabs } = Astro.props;---
<div class="tabs"> <div class="tabs-header"> {tabs.map((tab, index) => ( <button class="tab-button" data-tab={index} aria-selected={index === 0 ? 'true' : 'false'} > {tab.label} </button> ))} </div>
<div class="tabs-content"> {tabs.map((tab, index) => ( <div class="tab-panel" data-tab-content={index} hidden={index !== 0} > {tab.content} </div> ))} </div></div>
<script> // Este script será hidratado automaticamente function setupTabs() { const tabsContainer = document.querySelector('.tabs'); if (!tabsContainer) return;
const buttons = tabsContainer.querySelectorAll('.tab-button'); const panels = tabsContainer.querySelectorAll('.tab-panel');
buttons.forEach(button => { button.addEventListener('click', () => { const tabIndex = button.dataset.tab;
// Atualiza os botões buttons.forEach(btn => { btn.setAttribute('aria-selected', btn === button ? 'true' : 'false'); });
// Atualiza os painéis panels.forEach(panel => { panel.hidden = panel.dataset.tabContent !== tabIndex; }); }); }); }
// Executa quando o DOM estiver pronto document.addEventListener('DOMContentLoaded', setupTabs);
// Também executa quando o documento é carregado via View Transitions document.addEventListener('astro:page-load', setupTabs);</script>Comunicação entre Componentes
Use eventos personalizados para comunicação:
// Componente A dispara eventofunction notifyUpdate(data) { const event = new CustomEvent('data-updated', { detail: data, bubbles: true // Permite que o evento borbulhe para cima na árvore DOM });
document.dispatchEvent(event);}
// Componente B escuta o eventofunction listenForUpdates() { document.addEventListener('data-updated', (event) => { const data = event.detail; console.log('Dados atualizados:', data); updateUI(data); });}
// Inicializar listenerlistenForUpdates();Persistência de Estado
Mantenha o estado entre navegações:
export function createPersistentState(key, initialValue) { // Tenta recuperar o estado do localStorage let storedValue;
try { const item = localStorage.getItem(key); storedValue = item ? JSON.parse(item) : initialValue; } catch (error) { console.error('Erro ao recuperar estado:', error); storedValue = initialValue; }
// Função para atualizar o estado function setState(value) { try { // Suporta função para cálculo do novo estado const newValue = typeof value === 'function' ? value(storedValue) : value;
// Atualiza o valor em memória storedValue = newValue;
// Persiste no localStorage localStorage.setItem(key, JSON.stringify(newValue));
// Dispara evento de atualização window.dispatchEvent( new CustomEvent(`${key}:updated`, { detail: newValue }) );
return newValue; } catch (error) { console.error('Erro ao salvar estado:', error); return storedValue; } }
// Função para observar mudanças function subscribe(callback) { const handler = (event) => callback(event.detail);
window.addEventListener(`${key}:updated`, handler);
// Retorna função para cancelar inscrição return () => { window.removeEventListener(`${key}:updated`, handler); }; }
return { get value() { return storedValue; }, set: setState, subscribe };}
// Usoconst userPreferences = createPersistentState('userPreferences', { theme: 'light', fontSize: 'medium'});
// Obter valorconsole.log('Tema atual:', userPreferences.value.theme);
// Atualizar valoruserPreferences.set({ ...userPreferences.value, theme: 'dark'});
// Observar mudançasconst unsubscribe = userPreferences.subscribe((newPrefs) => { console.log('Preferências atualizadas:', newPrefs); applyTheme(newPrefs.theme);});Conclusão
Os padrões e práticas de JavaScript apresentados neste documento fornecem uma base sólida para o desenvolvimento de soluções web modernas e eficientes com Astro 5. Ao seguir estas recomendações, você pode escrever código mais limpo, manutenível e performático, aproveitando ao máximo os recursos do JavaScript moderno sem depender de frameworks pesados.
Lembre-se de adaptar estes padrões às necessidades específicas do seu projeto e de manter-se atualizado com as melhores práticas da linguagem.
Os próximos documentos detalharão outros aspectos do desenvolvimento:
- Testes e Qualidade: Estratégias de teste e garantia de qualidade
- Acessibilidade: Práticas para tornar seu site acessível
- SEO e Performance: Otimização para mecanismos de busca e performance
Última atualização: 21 de março de 2025