logo hsb.horse
← スニペット一覧に戻る

Snippets

段階的 DOM 観測戦略

アプリ準備完了後に細粒度オブザーバーへ段階的に切り替えるパターン。SPA コンテンツスクリプトやサードパーティ DOM 統合に広く応用できる。

公開日: 更新日:

SPA や複雑な Web アプリにコンテンツスクリプトを注入する場合、起動直後はどの要素が DOM に存在するかわからない。最初から細粒度のオブザーバーを設定しようとしても、対象要素がまだ存在しないため空振りする。

段階的 DOM 観測戦略はこの問題を 2 フェーズで解決する。

  1. 待機フェーズ:アプリルート全体を広く監視し、「準備完了シグナル」となる要素の出現を待つ
  2. アクティブフェーズ:シグナルが検出されたら、各領域にスコープを絞ったオブザーバーへ切り替える。URL 変化を検知したら待機フェーズへ戻る

コード

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

使用例

const teardown = createLayeredObserver(
'#app', // アプリルート(広域監視)
'[data-ready]', // 準備完了シグナル要素
[
{
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)
},
},
],
)
// すべてのオブザーバーを停止
teardown()

仕組み

  1. rootObserver はアプリルートを { childList: true, subtree: true } で広く監視する
  2. コールバックが呼ばれるたびに location.href を前回値と比較し、URL が変わっていればルート変更とみなして全レイヤーオブザーバーを破棄し、phase'waiting' へ戻す
  3. phase === 'waiting' かつ readySelector が DOM に存在すれば phase'active' にし、各レイヤーに MutationObserver を付け直す
  4. ルートオブザーバーは常に稼働し続けるため、SPA ナビゲーションにも追従できる

応用

このパターンは特定のフレームワークやセレクターセットに依存しない。以下のユースケースに応用できる。

  • SPA コンテンツスクリプト:Chrome Extension や Userscript でルート変更に連動して処理を再初期化する
  • サードパーティ DOM 統合:外部サービスが埋め込まれたページに後乗りでオブザーバーを設定する
  • アナリティクスオーバーレイ:特定領域の変化だけを監視してイベントを送信する
  • オートエンハンサー:複雑な Web アプリに注入して要素の出現・消滅を追う

関連スニペット