Lors de l’injection d’un script de contenu dans une SPA ou une application web complexe, il est impossible de savoir quels éléments sont présents dans le DOM juste après le démarrage. Tenter de configurer des observateurs ciblés immédiatement échouera car les éléments cibles n’existent pas encore.
La stratégie d’observation DOM en couches résout ce problème en deux phases :
- Phase d’attente : Observer l’ensemble de la racine de l’application et attendre l’apparition d’un élément « signal de disponibilité »
- Phase active : Une fois le signal détecté, basculer vers des observateurs à portée limitée pour chaque région. Si un changement d’URL est détecté, revenir à la phase d’attente
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() }}Utilisation
const teardown = createLayeredObserver( '#app', // racine de l'application (surveillance large) '[data-ready]', // élément signal de disponibilité [ { 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) }, }, ],)
// Arrêter tous les observateursteardown()Fonctionnement
rootObserversurveille la racine de l’application largement avec{ childList: true, subtree: true }- À chaque invocation du callback,
location.hrefest comparé à la valeur précédente ; si l’URL a changé, tous les observateurs de couche sont supprimés etphaseest réinitialisé à'waiting' - Lorsque
phase === 'waiting'et quereadySelectorest trouvé dans le DOM,phasedevient'active'et des observateurs plus ciblés sont attachés pour chaque couche - L’observateur racine continue de fonctionner en permanence, gérant ainsi automatiquement la navigation SPA
Applications
Ce pattern ne dépend d’aucun framework ou ensemble de sélecteurs spécifique. Il s’applique à :
- Scripts de contenu SPA : Réinitialiser le traitement dans les extensions Chrome ou Userscripts lors des changements de route
- Intégrations DOM tierces : Attacher des observateurs à une page qui embarque un service externe
- Overlays analytiques : Écouter uniquement des régions spécifiques et déclencher des événements sur les changements
- Auto-enhancers : Suivre l’apparition et la disparition d’éléments dans des applications web complexes injectées
hsb.horse