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の変更を監視します。

developer.mozilla.org

通常、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 の読み込みが完了するのを待って、読み込みが完了したら iframe内の body要素を取得して監視対象にするコードです。

どちらを使ってもいいと思いますが all_frames の設定を有効にする方がコード量が減るのでおすすめです。