logo hsb.horse
← Zur Snippets-Übersicht

Snippets

Geschichtete DOM-Beobachtungsstrategie

Ein Muster für den schrittweisen Wechsel zu engeren Observern sobald die App bereit ist. Breit einsetzbar in SPA-Content-Scripts und Drittanbieter-DOM-Integrationen.

Veröffentlicht: Aktualisiert:

Beim Einschleusen eines Content-Scripts in eine SPA oder eine komplexe Web-App ist unmittelbar nach dem Start unbekannt, welche Elemente im DOM vorhanden sind. Der Versuch, sofort enge Observer einzurichten, scheitert, weil die Zielelemente noch nicht existieren.

Die geschichtete DOM-Beobachtungsstrategie löst dieses Problem in zwei Phasen:

  1. Wartephase: Die App-Root wird breit beobachtet und auf das Erscheinen eines „Bereitschaftssignal”-Elements gewartet
  2. Aktivphase: Sobald das Signal erkannt wird, wird auf bereichsbegrenzte Observer für jede Region gewechselt. Bei einer URL-Änderung wird in die Wartephase zurückgekehrt

Code

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()
}
}

Verwendung

const teardown = createLayeredObserver(
'#app', // App-Root (breite Überwachung)
'[data-ready]', // Bereitschaftssignal-Element
[
{
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)
},
},
],
)
// Alle Observer stoppen
teardown()

Funktionsweise

  1. rootObserver überwacht die App-Root breit mit { childList: true, subtree: true }
  2. Bei jedem Callback-Aufruf wird location.href mit dem vorherigen Wert verglichen; hat sich die URL geändert, werden alle Layer-Observer entfernt und phase auf 'waiting' zurückgesetzt
  3. Wenn phase === 'waiting' und readySelector im DOM gefunden wird, wird phase auf 'active' gesetzt und engere Observer für jede Schicht angehängt
  4. Der Root-Observer läuft durchgehend weiter, sodass SPA-Navigation automatisch behandelt wird

Anwendungsgebiete

Dieses Muster ist von keinem bestimmten Framework oder Selektor-Set abhängig. Es eignet sich für:

  • SPA-Content-Scripts: Verarbeitung in Chrome-Extensions oder Userscripts bei Routenwechseln neu initialisieren
  • Drittanbieter-DOM-Integrationen: Observer an Seiten anhängen, die einen externen Dienst einbetten
  • Analytics-Overlays: Nur bestimmte Bereiche beobachten und bei Änderungen Ereignisse auslösen
  • Auto-Enhancer: Erscheinen und Verschwinden von Elementen in injizierten komplexen Web-Apps verfolgen

Verwandte Snippets