Ao injetar um content script em uma SPA ou aplicação web complexa, não é possível saber quais elementos estão presentes no DOM logo após a inicialização. Tentar configurar observadores estreitos imediatamente falhará porque os elementos alvo ainda não existem.
A estratégia de observação DOM em camadas resolve esse problema em duas fases:
- Fase de espera: Observar a raiz do app de forma ampla e aguardar a aparição de um elemento “sinal de prontidão”
- Fase ativa: Assim que o sinal for detectado, alternar para observadores com escopo limitado para cada região. Se uma mudança de URL for detectada, retornar à fase de espera
Código
type ObserverLayer = { selector: string callback: MutationCallback options?: MutationObserverInit}
const activeObservers = new Map<string, MutationObserver>()
const detachAll = (): void => { for (const obs of activeObservers.values()) { obs.disconnect() } activeObservers.clear()}
const attachLayer = (layer: ObserverLayer): void => { const el = document.querySelector(layer.selector) if (!el) return activeObservers.get(layer.selector)?.disconnect() const obs = new MutationObserver(layer.callback) obs.observe(el, layer.options ?? { childList: true, subtree: true }) activeObservers.set(layer.selector, obs)}
export const createLayeredObserver = ( appRootSelector: string, readySelector: string, layers: ObserverLayer[],): (() => void) => { let lastUrl = location.href let phase: 'waiting' | 'active' = 'waiting'
const rootObserver = new MutationObserver(() => { const currentUrl = location.href
if (currentUrl !== lastUrl) { lastUrl = currentUrl phase = 'waiting' detachAll() }
if (phase === 'waiting' && document.querySelector(readySelector)) { phase = 'active' detachAll() for (const layer of layers) { attachLayer(layer) } } })
const root = document.querySelector(appRootSelector) ?? document.body rootObserver.observe(root, { childList: true, subtree: true })
return () => { rootObserver.disconnect() detachAll() }}Uso
const teardown = createLayeredObserver( '#app', // raiz do app (monitoramento amplo) '[data-ready]', // elemento sinal de prontidão [ { selector: '.stream', callback: (mutations) => { for (const m of mutations) console.log('stream:', m.type) }, }, { selector: '.sidebar', callback: (mutations) => { for (const m of mutations) console.log('sidebar:', m.type) }, }, { selector: '[role="dialog"]', callback: (mutations) => { for (const m of mutations) console.log('modal:', m.type) }, }, ],)
// Parar todos os observadoresteardown()Como Funciona
rootObservermonitora a raiz do app de forma ampla com{ childList: true, subtree: true }- A cada invocação do callback,
location.hrefé comparado ao valor anterior; se a URL mudou, todos os observadores de camada são removidos ephaseé redefinido para'waiting' - Quando
phase === 'waiting'ereadySelectoré encontrado no DOM,phasetorna-se'active'e observadores mais estreitos são anexados para cada camada - O observador raiz continua funcionando continuamente, tratando automaticamente a navegação SPA
Aplicações
Este padrão não depende de nenhum framework ou conjunto de seletores específico. Aplica-se a:
- Content scripts de SPA: Reinicializar o processamento em extensões Chrome ou Userscripts em mudanças de rota
- Integrações DOM de terceiros: Anexar observadores a páginas que embarcam um serviço externo
- Overlays de analytics: Monitorar apenas regiões específicas e disparar eventos em mudanças
- Auto-enhancers: Rastrear aparição e desaparecimento de elementos em apps web complexos injetados
hsb.horse