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:
- Wartephase: Die App-Root wird breit beobachtet und auf das Erscheinen eines „Bereitschaftssignal”-Elements gewartet
- 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 stoppenteardown()Funktionsweise
rootObserverüberwacht die App-Root breit mit{ childList: true, subtree: true }- Bei jedem Callback-Aufruf wird
location.hrefmit dem vorherigen Wert verglichen; hat sich die URL geändert, werden alle Layer-Observer entfernt undphaseauf'waiting'zurückgesetzt - Wenn
phase === 'waiting'undreadySelectorim DOM gefunden wird, wirdphaseauf'active'gesetzt und engere Observer für jede Schicht angehängt - 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
hsb.horse