適材適所

WindowsやPowerShellやネットワーク、IBMなどのシステム系の話など気になったことも載せているブログです。

【JavaScript】iframe内の変数にアクセスする

検証した環境

Google Chrome バージョン: 97.0.4692.71(Official Build) (64 ビット)

iframeの中の変数にアクセスする

iframeは、htmlの中に別のhtmlを埋め込むことができる、そんな機能です。

呼び出し元の親と呼ばれた子の間でデータのやり取りができるととても便利です。

今回は、親ウィンドウのJavaScriptから子iframeの中のJavascript変数へアクセスする方法について、書き方と注意点を書いておこうと思います。

HTMLIFrameElement.contentWindowを使う

親Windowから子のiframeのWindowを扱うことができるcontentWindowプロパティを使うことで親Windowから子のiframeにアクセスすることができます。

親Windowから子iframeのvarGlobalという変数にアクセスする例を示します。

 
<html>
    <head>
        <meta charset="UTF-8">
        <title></title>
    </head>
    <body>
        <iframe src="iframesample1.html" id="iframe_id" ></iframe>
    </body>
   
    <script>
        //読み込み完了してから変数を取得するようにする
        document.addEventListener
        (
            'DOMContentLoaded', event =>
            {
                document.getElementById('iframe_id').onload = () =>
                {
                    //iframe内の変数にアクセスする
                    var vg=document.getElementById('iframe_id').contentWindow.varGlobal;
                    console.log(vg);
                }
            }
        )
    </script>
</html>
 
<!-- 子 -->
<!-- iframeSample1.html -->
<html>
    <head>
        <meta charset="UTF-8">
        <title></title>
    </head>
    <script type="text/javascript">
        var varGlobal='value=varGlobal';
    </script>
</html>

読込結果がこちらになります。

iframeの変数にアクセスすることができました。

contentWindowは変数だけでなく、内部DOMにアクセスしているので、自Windowのように操作することができます。

ただし、これらの属性は読み取り専用です。

変数を操作したいときは、後述するpostMessageを使って処理を組み立てます。

contentWindowを使うときの注意点

contentWindowプロパティを使うと直感的にiframeの変数にアクセスできますが、

いくつか注意点があります。

ローカルファイルだとエラーになる

私もハマった点ですが、webサーバー上に設置したhtmlではなく、ローカル環境に設置したhtmlファイルをブラウザで開いて上記のコードを実行するとエラーが発生します。

Uncaught DOMException: Blocked a frame with origin "null" from accessing a cross-origin frame.

これはセキュリティの観点から、おそらく同一のオリジンじゃない!! 1ということで怒られたものです。

そのため、検証するときは適当なwebサーバーを立てる必要があります。

読み込みタイミングに注意

iframeが読み込まれる前に変数を参照しようとしてもエラーになってしまうので、読み込みが完了してから(ここではDOMContentLoadedのイベントを使いました)変数にアクセスするようにしましょう。

読み取り専用です

アクセスする変数は読み取り専用です。

iframe内での変数の宣言にはvarを使う

注意するのは、変数のスコープです。

変数のスコープを「グローバル」で宣言する必要があります。

JavaScriptでグローバル変数を宣言するにはvarを使います。

var 文は関数スコープまたはグローバルスコープの変数を宣言し、任意でそれをある値に初期化します。

引用元:var - JavaScript | MDN

JavaScriptの変数の宣言にはvar以外にもconstやletがありますが、これらはローカルスコープになります。

let 文はブロックスコープのローカル変数を宣言します。

引用元:let - JavaScript | MDN

定数は、let キーワードを使って定義する変数と同様にブロックスコープを持ちます。

引用元:const - JavaScript | MDN

そのため、親Windowからアクセスしたいiframe内の変数についてはvarで宣言する必要があります。

また、スコープという点から、変数を宣言する場所も重要です。

いくらvarで変数を宣言しても関数の中に宣言してしまうとスコープが関数内に限定されてしまうので、スクリプト内(関数の外)に書きましょう。

スコープごとの変数へのアクセスについて例を示します。

 
<html>
   <head>
        <meta charset="UTF-8">
        <title></title>
    </head>

    <body>
        <iframe src="iframeSample2.html" id="iframe_id" ></iframe>
    </body>
   
    <script>
        //読み込み完了してから変数を取得するようにする
        document.addEventListener
        (
            'DOMContentLoaded', event =>
            {
                document.getElementById('iframe_id').onload = () =>
                {
                    getVariable();
                }
            }
        )

        function getVariable()
        {
            var vg=document.getElementById('iframe_id').contentWindow.varGlobal;
            let lg=document.getElementById('iframe_id').contentWindow.letGlobal;
            const cg=document.getElementById('iframe_id').contentWindow.constGlobal;
           
            var vf=document.getElementById('iframe_id').contentWindow.varFunc;
            let lf=document.getElementById('iframe_id').contentWindow.letFunc;
            const cf=document.getElementById('iframe_id').contentWindow.constFunc;
           
            console.log('*****contentWindow*****');
            console.log('varGlobal:' + vg);
            console.log('letGlobal:' + lg);
            console.log('constGlobal:' + cg);
            console.log('varFunc:' + vf);
            console.log('letFunc:' + lf);
            console.log('constFunc:' + cf);
            console.log('***********************');
        }
    </script>
</html>
 
<!-- 子 -->
<!-- iframeSample2.html -->
<html>
    <head>
        <meta charset="UTF-8">
        <title></title>
    </head>
    <body>
    </body>
   
    <script type="text/javascript">

        var varGlobal='value=varGlobal';
       
        // letとconstで宣言しているので親からアクセスすることはできない
        let letGlobal='value=letGlobal';
        const constGlobal='value=constGlobal';

        function sampleFunction()
        {
            // 関数の中で定義された変数はvarであっても親からアクセスできない
            var varFunc='value=varFunc';
            let letFunc='value=letFunc';
            const constFunc='value=constFunc';
        }      
    </script>
</html>

読込結果がこちらになります。

グローバル変数として定義したvarGlobalだけにアクセスすることができました。

window.postMessageを使う

window.postMessageを使うことで、親と子iframeの間でやり取りできるようになります。

window.postMessageは、異なった複数のページリクエスト間で安全にメッセージのやり取りをできるように考案された仕組みです。

便利な反面、使い方を誤るとセキュリティリスクを埋め込むことになるので注意が必要です。2

window.postMessageはメッセージのやり取り

先述のcontentWindowは親から変数を見に行っていたイメージが強いですが、window.postMessageは子から親にメッセージを送るイメージになります。

子が親にメッセージを送り、親はそれを受け取ります。

今回は触れませんが、その逆も可能です。(書き方は同じです。)

子が能動的にメッセージを送るため、contentWindowによる手法と違い、変数のスコープを意識する必要はありません。3

そのため、柔軟にデータのやりとりをすることができます。

window.postMessageを使って子から親にメッセージを送る

次にlocalhost上でwindow.postMessageを使ったメッセージ送信の例を示します。

 
<html>
    <head>
        <meta charset="UTF-8">
        <title></title>
    </head>

    <body>
        <iframe src="iframeSample3.html" id="iframe_id" ></iframe>
    </body>
   
    <script>
        //メッセージを受け取る関数
        function receiveMsg(event)
        {
            //安全のためオリジンをチェックする
            if(event.origin!='http://localhost'){
                return;
            }
            console.log(event.data);
            let v=event.data
            console.log(event.data.get('varGlobal'));
            console.log(event.data.get('letGlobal'));
            console.log(event.data.get('constGlobal'));
        }

        window.addEventListener('message',receiveMsg,false);

    </script>
</html>
 
<!-- 子 -->
<!-- iframeSample3.html -->
<html>
    <head>
        <meta charset="UTF-8">
        <title></title>
    </head>
    <body>
        <p>iframeSamle3</p>
    </body>
   
    <script type="text/javascript">
        var varGlobal='value=varGlobal';
        let letGlobal='value=letGlobal';
        const constGlobal='value=constGlobal';

        let map=new Map();
        map.set('varGlobal',varGlobal);
        map.set('letGlobal',letGlobal);
        map.set('constGlobal',constGlobal);
       
        //メッセージをポストする
        const targetWindow=window.parent;
        targetWindow.postMessage(map,'http://localhost/');
    </script>
</html>

読込結果がこちらになります。

postするメッセージはmapにする必要はありません。

その辺はよしなにシリアライズしてくれるようです。4

しつこいようですが、受信側(ここでは親)は必ず送信元もオリジンをチェックするようにしましょう。

終わりに

iframeの変数にアクセスする方法について見ましたが、contentWindowは変数のスコープやらイベントの設定など意外と気にすることが多い反面セキュリティは割としっかりしています。

使い方を誤るとセキュリティホールを作る可能性がありますが、window.postMessageはお手軽に、そして柔軟にデータのやり取りができます。

ここまで書いておいてなんですが、なるべく親と子の間の会話は少ないほうが疎結合にできるからいいんですけどね・・・。

現実世界の親と子は会話が多いに越したことはないんですが。

というわけで(?)ここまでお読みいただき、ありがとうございました。

参考

contentWindowについて

HTMLIFrameElement.contentWindow - Web API | MDN

postMessageについて

Window.postMessage() - Web API | MDN

Javascriptのconstについて

const - JavaScript | MDN

セキュリティについて

安全なウェブサイトの作り方 - 1.6 CSRF(クロスサイト・リクエスト・フォージェリ):IPA 独立行政法人 情報処理推進機構

Window.postMessage() - Web API | MDN

重要! まずは「オリジン」を理解しよう:HTML5時代の「新しいセキュリティ・エチケット」(1)(1/2 ページ) - @IT重要! まずは「オリジン」を理解しよう:HTML5時代の「新しいセキュリティ・エチケット」(1)(1/2 ページ) - @IT


  1. fileスキームについてはどこまでを同一オリジンとみなすかはブラウザ依存らしいです。

  2. 具体的なセキュリティ問題については参考サイトをご覧ください。

  3. 関数内の変数は別です。

  4. 「他のウィンドウに送られるデータ。データは the structured clone algorithm (en-US) に従ってシリアル化されます。」とのことです。