Chrome拡張でiframe内のDOMの変化を監視する
Chrome拡張を作っているとiframe内の DOM の変化を監視したい場面に遭遇することがあると思います。監視する方法はとても簡単です。
manifest.json に "all_frames": true を追記して、すべてのフレームにスクリプトを挿入するように設定します。そして、iframe内の要素を MutationObserver で監視します。
検証環境
検証用のページ
iframe_content.html
<html> <body> ... <h1>Content of an iframe</h1> <button id="addDivButton">Add div element</button> ... </body> <script> const addDivButton = document.getElementById("addDivButton"); addDivButton.addEventListener("click", () => document.body.appendChild(document.createElement("div")) ); </script> </html>
content.html
<html> <body> ... <h1>Content</h1> <iframe src="iframe_content.htmlのurl"></iframe> ... </body> </html>
Chrome拡張
content_script.js
console.log('load content script'); const observer = new MutationObserver(() => console.log("Added div element")); observer.observe(document.body, { childList: true, subtree: true });
manifest.json
{ "name": "ChromeExtension", "version": "1.0.0", "manifest_version": 2, "permissions": ["<all_urls>"], "content_scripts": [ { "matches": ["<all_urls>"], "js": ["js/content_script.js"], "all_frames": true } ] }
all_framesを有効にする
manifest.json に "all_frames": true を設定します。
all_framesを有効にすることで、iframeを含むすべてのフレームでスクリプトが実行されるようになります。
デフォルトは false です。
Optional. Defaults to false, meaning that only the top frame is matched. If specified true, it will inject into all frames, even if the frame is not the topmost frame in the tab. Each frame is checked independently for URL requirements, it will not inject into child frames if the URL requirements are not met.
Chrome Extensions content scripts - Chrome Developers
Chrome拡張をブラウザにインストールして検証用のページをリロードするとload content script
が2回表示されました。
コンテンツとiframe内のコンテンツ両方から実行されていることを確認するために、ログにdocument.body
を出力してみます。
以下、コンソールに出力された要素をマウスホバーした結果です。コンテンツのbody要素と、iframe内のコンテンツのbody要素をそれぞれ取得できていることが分かります。
iframe内のDOMの変化を監視してみる
MutationObserver を使ってDOMの変更を監視します。
通常、iframe の外で iframe内の DOM の変更を監視するためには、iframe内の要素(iframeObject.contentDocument)を取得して、MutationObserver 引数に指定する必要があります。
all_frames を true に設定することで、スクリプトが iframe の外と内でそれぞれ実行されるようになるので、iframeの外で IFrame Object から body 要素を取得しなくてもDOMの監視ができるようになります。
body要素にdiv要素を追加するボタンを押してみるとDOMの変更を監視できていることがわかります。
余談
all_frames の存在を知る前は以下のようなコードを書いてました。
type Callback = (addedEl: HTMLElement) => void; export const addedElementObserver = (targetEl: HTMLElement, callbackList: Callback[]) => { const observer = new MutationObserver((records: MutationRecord[]) => { for (const record of records) { for (const node of Array.from(record.addedNodes)) { const el = node as HTMLElement; if (!el.getElementsByTagName) { continue; } for (const callback of callbackList) { callback(el); } const iframeCollection = el.getElementsByTagName('iframe'); for (const iframeEl of Array.from(iframeCollection)) { iframeEl.contentWindow.onload = () => { const iframeBody = iframeEl.contentDocument.body; for (const callback of callbackList) { callback(iframeBody); } addedElementObserver(iframeBody, callbackList); }; } } } }); observer.observe(targetEl, { childList: true, subtree: true, }); }; addedElementObserver(document.body, [(addedEl: HTMLElement) => console.log(addedEl)]);
なんの変哲もないDOMに非同期でiframeが追加され、その後非同期でiframe内にHTMLが埋め込まれる。このiframe内のDOMに変更があったことを検知するためにどうすればいいか。
— もやし丸 (@kuroppe1819) 2020年2月29日
MutationObserverで呼び出す関数にMutationObserverを書いてゴリ押ししたんだがもっと良い方法はなかったんだろうか.....。
監視対象のDOMに iframe が追加されたら、その iframe の読み込みが完了するのを待って、読み込みが完了したら iframe内の body要素を取得して監視対象にするコードです。
どちらを使ってもいいと思いますが all_frames の設定を有効にする方がコード量が減るのでおすすめです。