logo hsb.horse
← Voltar para o índice de snippets

Snippets

Estratégia de Observação DOM em Camadas

Um padrão para alternar progressivamente para observadores mais estreitos assim que o app estiver pronto. Amplamente reutilizável em content scripts de SPA e integrações DOM de terceiros.

Publicado: Atualizado:

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:

  1. Fase de espera: Observar a raiz do app de forma ampla e aguardar a aparição de um elemento “sinal de prontidão”
  2. 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 observadores
teardown()

Como Funciona

  1. rootObserver monitora a raiz do app de forma ampla com { childList: true, subtree: true }
  2. A cada invocação do callback, location.href é comparado ao valor anterior; se a URL mudou, todos os observadores de camada são removidos e phase é redefinido para 'waiting'
  3. Quando phase === 'waiting' e readySelector é encontrado no DOM, phase torna-se 'active' e observadores mais estreitos são anexados para cada camada
  4. 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

Snippets Relacionados