17. Manutenção e Evolução - Parte 2: Refatoração, Modernização e Monitoramento de Desempenho
Continuação da Parte 1: Estratégias de Versionamento e Gerenciamento de Dependências
Refatoração e Modernização de Código
Identificação de Código Legacy
Antes de iniciar qualquer refatoração, identifique áreas problemáticas:
import { execSync } from 'child_process';import fs from 'fs';import path from 'path';import glob from 'glob';
// Configuraçõesconst SRC_DIR = 'src';const COMPLEXITY_THRESHOLD = 15;const MAX_FILE_SIZE = 500; // linhasconst MAX_FUNCTION_LENGTH = 50; // linhas
// Encontrar arquivos grandesfunction findLargeFiles() { const files = glob.sync(`${SRC_DIR}/**/*.{js,ts,astro}`, { nodir: true });
return files .map(file => { const content = fs.readFileSync(file, 'utf-8'); const lines = content.split('\n').length; return { file, lines }; }) .filter(({ lines }) => lines > MAX_FILE_SIZE) .sort((a, b) => b.lines - a.lines);}
// Encontrar funções complexas (requer ESLint com plugin de complexidade)function findComplexFunctions() { try { const eslintOutput = execSync( `npx eslint ${SRC_DIR} --no-eslintrc --config .eslintrc.complexity.js --format json`, { stdio: ['pipe', 'pipe', 'ignore'] } ).toString();
const results = JSON.parse(eslintOutput);
const complexityIssues = [];
results.forEach(result => { result.messages .filter(msg => msg.ruleId === 'complexity') .forEach(msg => { complexityIssues.push({ file: result.filePath, line: msg.line, column: msg.column, function: msg.message.match(/^Function '(.+)' has/)?.[1] || 'Anonymous', complexity: parseInt(msg.message.match(/complexity of (\d+)\./)?.[1] || '0') }); }); });
return complexityIssues .filter(issue => issue.complexity > COMPLEXITY_THRESHOLD) .sort((a, b) => b.complexity - a.complexity); } catch (error) { console.error('Erro ao executar ESLint:', error.message); return []; }}
// Encontrar funções longasfunction findLongFunctions() { // Esta é uma implementação simplificada // Uma análise mais precisa requer um parser de AST como Babel ou TypeScript
const files = glob.sync(`${SRC_DIR}/**/*.{js,ts}`, { nodir: true }); const longFunctions = [];
files.forEach(file => { const content = fs.readFileSync(file, 'utf-8'); const lines = content.split('\n');
let inFunction = false; let functionStart = 0; let bracketCount = 0; let currentFunction = '';
lines.forEach((line, index) => { // Detecção simplificada de funções if (!inFunction && /function\s+(\w+)/.test(line)) { inFunction = true; functionStart = index; currentFunction = line.match(/function\s+(\w+)/)[1]; bracketCount += (line.match(/{/g) || []).length; bracketCount -= (line.match(/}/g) || []).length; } else if (!inFunction && /(\w+)\s*=\s*function/.test(line)) { inFunction = true; functionStart = index; currentFunction = line.match(/(\w+)\s*=/)[1]; bracketCount += (line.match(/{/g) || []).length; bracketCount -= (line.match(/}/g) || []).length; } else if (!inFunction && /(\w+)\s*[:=]\s*\([^)]*\)\s*=>/.test(line)) { inFunction = true; functionStart = index; currentFunction = line.match(/(\w+)\s*[:=]/)[1]; bracketCount += (line.match(/{/g) || []).length; bracketCount -= (line.match(/}/g) || []).length; } else if (inFunction) { bracketCount += (line.match(/{/g) || []).length; bracketCount -= (line.match(/}/g) || []).length;
if (bracketCount === 0) { const length = index - functionStart + 1; if (length > MAX_FUNCTION_LENGTH) { longFunctions.push({ file, function: currentFunction, startLine: functionStart + 1, endLine: index + 1, length }); } inFunction = false; } } }); });
return longFunctions.sort((a, b) => b.length - a.length);}
// Gerar relatóriofunction generateReport() { console.log('🔍 Analisando código...\n');
const largeFiles = findLargeFiles(); const complexFunctions = findComplexFunctions(); const longFunctions = findLongFunctions();
console.log(`📊 Relatório de Qualidade de Código (${new Date().toLocaleDateString()})\n`);
if (largeFiles.length > 0) { console.log(`\n📄 Arquivos Grandes (> ${MAX_FILE_SIZE} linhas): ${largeFiles.length}`); largeFiles.slice(0, 10).forEach(({ file, lines }) => { console.log(`- ${file}: ${lines} linhas`); }); if (largeFiles.length > 10) { console.log(`... e mais ${largeFiles.length - 10} arquivos`); } } else { console.log('✅ Nenhum arquivo grande encontrado.'); }
if (complexFunctions.length > 0) { console.log(`\n🧩 Funções Complexas (> ${COMPLEXITY_THRESHOLD} complexidade ciclomática): ${complexFunctions.length}`); complexFunctions.slice(0, 10).forEach(({ file, function: fn, line, complexity }) => { console.log(`- ${path.relative('.', file)}:${line} - ${fn}: complexidade ${complexity}`); }); if (complexFunctions.length > 10) { console.log(`... e mais ${complexFunctions.length - 10} funções`); } } else { console.log('✅ Nenhuma função complexa encontrada.'); }
if (longFunctions.length > 0) { console.log(`\n📏 Funções Longas (> ${MAX_FUNCTION_LENGTH} linhas): ${longFunctions.length}`); longFunctions.slice(0, 10).forEach(({ file, function: fn, startLine, endLine, length }) => { console.log(`- ${path.relative('.', file)}:${startLine}-${endLine} - ${fn}: ${length} linhas`); }); if (longFunctions.length > 10) { console.log(`... e mais ${longFunctions.length - 10} funções`); } } else { console.log('✅ Nenhuma função longa encontrada.'); }
// Salvar relatório detalhado const report = { timestamp: new Date().toISOString(), summary: { largeFiles: largeFiles.length, complexFunctions: complexFunctions.length, longFunctions: longFunctions.length }, details: { largeFiles, complexFunctions, longFunctions } };
fs.writeFileSync( 'code-quality-report.json', JSON.stringify(report, null, 2) );
console.log('\nRelatório detalhado salvo em code-quality-report.json');
// Retornar código de erro se houver problemas significativos if ( largeFiles.length > 10 || complexFunctions.length > 20 || longFunctions.length > 20 ) { console.log('\n⚠️ Problemas significativos de qualidade de código detectados.'); return 1; }
return 0;}
process.exit(generateReport());Estratégias de Refatoração
Refatoração Incremental
Divida a refatoração em etapas gerenciáveis:
- Identificar áreas problemáticas com métricas objetivas
- Priorizar baseado em risco, complexidade e valor de negócio
- Planejar mudanças incrementais com testes adequados
- Implementar em pequenos commits verificáveis
- Validar através de testes automatizados e revisão de código
Exemplo: Extrair Componente
---// Antes: Componente grande e monolítico---
<div class="product-card"> <div class="product-image"> <img src={product.image} alt={product.name} /> {product.isNew && <span class="badge new">Novo</span>} {product.discount > 0 && <span class="badge discount">{product.discount}% OFF</span>} </div>
<div class="product-info"> <h3>{product.name}</h3> <div class="product-rating"> {Array.from({ length: 5 }).map((_, i) => ( <span class={i < product.rating ? "star filled" : "star"}>★</span> ))} <span class="rating-count">({product.reviewCount})</span> </div>
<div class="product-price"> {product.discount > 0 && ( <span class="original-price">{formatCurrency(product.originalPrice)}</span> )} <span class="current-price">{formatCurrency(product.price)}</span> </div>
<div class="product-actions"> <button class="add-to-cart" onclick={`addToCart(${product.id})`}> Adicionar ao Carrinho </button> <button class="add-to-wishlist" onclick={`addToWishlist(${product.id})`}> ♥ </button> </div> </div></div>---// Depois: Componentes menores e reutilizáveis
import ProductBadges from './ProductBadges.astro';import ProductRating from './ProductRating.astro';import ProductPrice from './ProductPrice.astro';import ProductActions from './ProductActions.astro';
const { product } = Astro.props;---
<div class="product-card"> <div class="product-image"> <img src={product.image} alt={product.name} /> <ProductBadges isNew={product.isNew} discount={product.discount} /> </div>
<div class="product-info"> <h3>{product.name}</h3> <ProductRating rating={product.rating} reviewCount={product.reviewCount} /> <ProductPrice price={product.price} originalPrice={product.originalPrice} discount={product.discount} /> <ProductActions productId={product.id} /> </div></div>---const { isNew, discount } = Astro.props;---
{isNew && <span class="badge new">Novo</span>}{discount > 0 && <span class="badge discount">{discount}% OFF</span>}---const { rating, reviewCount } = Astro.props;---
<div class="product-rating"> {Array.from({ length: 5 }).map((_, i) => ( <span class={i < rating ? "star filled" : "star"}>★</span> ))} <span class="rating-count">({reviewCount})</span></div>Modernização Progressiva
Migração para Recursos Modernos
Atualize progressivamente para recursos modernos do JavaScript:
// Antes: Código legadovar products = [];
function getProducts() { return $.ajax({ url: '/api/products', method: 'GET', success: function(data) { products = data; renderProducts(); }, error: function(error) { console.error('Erro ao carregar produtos:', error); } });}
function renderProducts() { var container = document.getElementById('products-container'); container.innerHTML = '';
for (var i = 0; i < products.length; i++) { var product = products[i]; var element = document.createElement('div'); element.className = 'product'; element.innerHTML = '<h3>' + product.name + '</h3><p>' + product.price + '</p>'; container.appendChild(element); }}// Depois: Código modernolet products = [];
async function getProducts() { try { const response = await fetch('/api/products');
if (!response.ok) { throw new Error(`Erro HTTP: ${response.status}`); }
products = await response.json(); renderProducts(); } catch (error) { console.error('Erro ao carregar produtos:', error); }}
function renderProducts() { const container = document.getElementById('products-container'); container.innerHTML = '';
products.forEach(product => { const element = document.createElement('div'); element.className = 'product'; element.innerHTML = `<h3>${product.name}</h3><p>${product.price}</p>`; container.appendChild(element); });}Migração para TypeScript
Adicione tipagem gradualmente:
- Configure o TypeScript no projeto:
npm install --save-dev typescript @types/node- Crie um arquivo
tsconfig.json:
{ "extends": "astro/tsconfigs/strict", "compilerOptions": { "target": "ES2020", "module": "ESNext", "moduleResolution": "node", "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, "strict": true, "skipLibCheck": true, "allowJs": true, "checkJs": true }, "include": ["src/**/*"], "exclude": ["node_modules"]}- Migre arquivos JS para TS incrementalmente:
interface Product { id: number; name: string; price: number; originalPrice?: number; discount: number; rating: number; reviewCount: number; isNew: boolean; image: string;}
export function calculateDiscount(product: Product): number { if (!product.originalPrice || product.originalPrice <= product.price) { return 0; }
return Math.round( ((product.originalPrice - product.price) / product.originalPrice) * 100 );}
export function formatCurrency(amount: number): string { return new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(amount);}Monitoramento de Desempenho
Métricas Web Vitals
Implemente monitoramento de Web Vitals para acompanhar o desempenho do site:
<script> // Importar a biblioteca web-vitals import('https://unpkg.com/web-vitals@3/dist/web-vitals.iife.js').then(({ onCLS, onFID, onLCP, onTTFB, onINP }) => { function sendToAnalytics({ name, delta, value, id }) { // Enviar para seu sistema de analytics // Exemplo com Google Analytics 4 if (window.gtag) { gtag('event', name, { value: Math.round(name === 'CLS' ? delta * 1000 : delta), metric_id: id, metric_value: value, metric_delta: delta, }); }
// Registrar no console durante desenvolvimento if (import.meta.env.DEV) { console.log(`Web Vital: ${name}`, { value, delta, id }); }
// Opcional: enviar para um endpoint personalizado if (navigator.sendBeacon) { const payload = { name, value: Math.round(value * 100) / 100, delta: Math.round(delta * 100) / 100, id, page: window.location.pathname, timestamp: Date.now() };
navigator.sendBeacon('/api/vitals', JSON.stringify(payload)); } }
// Monitorar métricas principais onCLS(sendToAnalytics); // Cumulative Layout Shift onFID(sendToAnalytics); // First Input Delay onLCP(sendToAnalytics); // Largest Contentful Paint onTTFB(sendToAnalytics); // Time to First Byte onINP(sendToAnalytics); // Interaction to Next Paint });</script>Monitoramento de Erros
Implemente um sistema de rastreamento de erros:
---const isProd = import.meta.env.PROD;---
<script define:vars={{ isProd }}> class ErrorTracker { constructor() { this.errors = []; this.maxErrors = 10; this.initialize(); }
initialize() { // Capturar erros não tratados window.addEventListener('error', (event) => { this.captureError({ type: 'uncaught', message: event.message, stack: event.error?.stack, source: event.filename, line: event.lineno, column: event.colno, timestamp: Date.now() });
// Não impedir o comportamento padrão return false; });
// Capturar rejeições de promessas não tratadas window.addEventListener('unhandledrejection', (event) => { this.captureError({ type: 'unhandledrejection', message: event.reason?.message || 'Unhandled Promise Rejection', stack: event.reason?.stack, timestamp: Date.now() }); });
// Sobrescrever console.error const originalConsoleError = console.error; console.error = (...args) => { this.captureError({ type: 'console.error', message: args.map(arg => typeof arg === 'object' ? JSON.stringify(arg) : String(arg) ).join(' '), timestamp: Date.now() }); originalConsoleError.apply(console, args); }; }
captureError(error) { // Adicionar erro à lista this.errors.push(error);
// Limitar número de erros armazenados if (this.errors.length > this.maxErrors) { this.errors.shift(); }
// Em produção, enviar para servidor if (isProd) { this.sendToServer(error); } else { console.warn('Error tracked (DEV):', error); } }
sendToServer(error) { // Adicionar informações do usuário e contexto const payload = { ...error, url: window.location.href, userAgent: navigator.userAgent, viewport: { width: window.innerWidth, height: window.innerHeight } };
// Enviar para o servidor usando Beacon API (não bloqueia navegação) if (navigator.sendBeacon) { navigator.sendBeacon('/api/errors', JSON.stringify(payload)); } else { // Fallback para fetch fetch('/api/errors', { method: 'POST', body: JSON.stringify(payload), headers: { 'Content-Type': 'application/json' }, // Usar keepalive para garantir que a requisição seja enviada keepalive: true }).catch(e => { // Silenciar erros do fetch para evitar loops }); } }
getErrors() { return [...this.errors]; } }
// Inicializar e expor globalmente window.errorTracker = new ErrorTracker();</script>Endpoint para Receber Métricas
import { createClient } from '@supabase/supabase-js';
export async function post({ request }) { try { const payload = await request.json();
// Validar payload if (!payload || !payload.name) { return new Response(JSON.stringify({ error: 'Invalid payload' }), { status: 400, headers: { 'Content-Type': 'application/json' } }); }
// Conectar ao Supabase const supabaseUrl = import.meta.env.SUPABASE_URL; const supabaseKey = import.meta.env.SUPABASE_SERVICE_KEY; const supabase = createClient(supabaseUrl, supabaseKey);
// Salvar métrica const { error } = await supabase .from('web_vitals') .insert([{ metric_name: payload.name, metric_value: payload.value, metric_delta: payload.delta, page_url: payload.page, timestamp: new Date().toISOString() }]);
if (error) { console.error('Error saving web vital:', error); return new Response(JSON.stringify({ error: 'Database error' }), { status: 500, headers: { 'Content-Type': 'application/json' } }); }
return new Response(JSON.stringify({ success: true }), { status: 200, headers: { 'Content-Type': 'application/json' } }); } catch (error) { console.error('Error processing web vital:', error);
return new Response(JSON.stringify({ error: 'Server error' }), { status: 500, headers: { 'Content-Type': 'application/json' } }); }}Continua na Parte 3: Atualizações de Segurança, Estratégias de Migração e Documentação Técnica