📡

postMessageを使用した親・子(iframe)間のクロスドメイン通信の実現方法

2024/09/26に公開

はじめに

この記事では、postMessageの使い方や処理の流れをシンプルに確認する方法を記載しています。
記載内容に誤りなどございましたら、お手数ですがご指摘いただけますと幸いです。

この記事の対象者

・クロスドメインについて何となく知りたい方
・postMessageについて何となく知りたい方
・実際に環境を作り、postMessageの使い方や処理の流れを確認してみたい方

クロスドメインとは?

Web開発において、異なるドメイン間でリソースやデータをやり取りする際の制約やそれを解決する技術のことを指します。通常、Webブラウザにはセキュリティ上の理由から同一生成元ポリシー(Same-Origin Policy)という制約があり、あるドメインのWebページが、異なるドメイン(例: https://example.com から https://api.anotherdomain.com)のリソースに直接アクセスすることができません。この制約は、ユーザーの個人情報やセッション情報を保護するために設けられています。
【参考記事】
https://wa3.i-3-i.info/word15224.html

postMessageとは?

Window オブジェクト間で安全にオリジン間通信を可能にするためのメソッドです。
postMessageでやりとりする画面は、iframeが関係する場合「親(iframeタグを設定している側)」と「子(iframeタグのsrc属性に設定されている側)」といった表現をするそうです。
【参考記事】
https://developer.mozilla.org/ja/docs/Web/API/Window/postMessage

開発環境

・macOS 14.6.1
・HTML
・JavaScript
・IDE : VSCode

前提

・HTMLやJavaScript、ドメインやポートなどの基礎的な内容を理解している
・homebrewがインストール済みである
・vimまたはそれに近いものを触ったことがある(必須ではない)

プロジェクト作成

パッケージ構成

/project-root/     # 任意のフォルダ名(プロジェクト名)
  ├── parent/
  │   └── index.html     # 親ページ
  └── child/
      └── index.html     # 子ページ (iframeで読み込む)

実際のプロジェクトの例

ファイル内容

・parent/index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Parent Page</title>
</head>

<body>
    <h1>Parent Page</h1>
    <button id="sendMessageBtn">Send Message to Child</button>
    
    <!-- iframeで子ページを読み込む -->
    <iframe id="childFrame" src="http://child.local:3001" width="600" height="400"></iframe>
    
    <script>
        let dataToChild = {
            id: '',
            message: ''
        }
    
        // メッセージ送信追跡フラグ(無限ループ防止策)
        let messageSentToChildFlg = false;
        // 未知オリジンメッセージ表示フラグ
        let unknownOriginMessageShownFlg = false;

        // メッセージ送信: 親から子へ
        sendMessageBtn.addEventListener('click', () => {
            // 初回のみ送信
            if (!messageSentToChildFlg) {
                dataToChild.id = 'toChild_001';
                dataToChild.message = 'Hello from Parent!';
                // postMessage('送信対象のデータ', '送信先のURL') 
                document.getElementById('childFrame').contentWindow.postMessage(dataToChild, 'http://child.local:3001');
                console.log('子に対してメッセージ送信!');
                // 送信後にフラグを立てる
                messageSentToChildFlg = true;
            }
        });
    
        // メッセージ受信: 子から親へ
        window.addEventListener('message', (event) => {
            const data = event.data;
            // 安全のため、メッセージの送信元ドメインを確認
            if (event.origin === 'http://child.local:3001') {
                // メッセージのフォーマット確認
                if (data && data.id && data.message) {
                    console.log('Message from child(data):', data);
                    console.log('Message from child(data.id):', data.id);
                    console.log('Message from child(data.message):', data.message);
                }
            } else {
                if (!unknownOriginMessageShownFlg && (data.id != undefined || data.message != undefined)) {
                    console.log('Unknown origin:' + event.origin);
                    unknownOriginMessageShownFlg = true;
                }
            }
		});
    
    </script>
</body>

</html>

・child/index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Child Page</title>
</head>

<body>
    <h1>Child Page</h1>
    
    <script>
        let dataToParent = {
            id: '',
            message: ''
        }
    
        // メッセージ送信追跡フラグ(無限ループ防止用)
        let messageSentToParentFlg = false;
        // 未知オリジンメッセージ表示フラグ
        let unknownOriginMessageShownFlg = false;
    
        // メッセージ受信: 親から子へ
        window.addEventListener('message', (event) => {
            // 安全のため、メッセージの送信元ドメインを確認
            if (event.origin === 'http://parent.local:3000') {
                const data = event.data;
                if (data && data.id && data.message) {
                    console.log('Message from parent(data):', data);
                    console.log('Message from parent(data.id):', data.id);
                    console.log('Message from parent(data.message):', data.message);
                }
    
                // 初回のみ返信
                if (!messageSentToParentFlg) {
                    dataToParent.id = 'toParent_001';
                    dataToParent.message = 'Hello from Child!';
                    // 親へメッセージを返信_postMessage('送信対象のデータ', '送信先のURL')
                    window.parent.postMessage(dataToParent, event.origin);
                    console.log('親に対してメッセージ返信!');
                    // 送信後にフラグを立てる
                    messageSentToParentFlg = true;
				}
            } else {
                if (!unknownOriginMessageShownFlg) {
                    console.log('Unknown origin:' + event.origin);
                    unknownOriginMessageShownFlg = true;
                }
            }
        });
    </script>
</body>

</html>

環境構築手順

\textcolor{red}{※①については、必須ではありません。}
(ドメインを区別することで、動作確認をしやすくする意図のため)

①親と子で別々のサーバを使用するためのエントリ追加\textcolor{red}{※}

/etc/hosts ファイルにアクセス

  1. ターミナルを開く
    Spotlight(画面右上の虫眼鏡アイコン)をクリックし、「ターミナル」と検索して開きます。
  2. hosts ファイルを開く
    nano や vim などのテキストエディタを使用して、/etc/hosts ファイルを開きます。
    管理者権限が必要なため、sudo コマンドを使います。

vimを使用する場合、下記コマンドを実行

sudo vim /etc/hosts

エントリを追加

「i」を押下し、insertモードになっている状態が前提(左下にINSERTの記載があればOK)
【参考記事】
https://zenn.dev/masatotezuka/articles/vim_command_220225

127.0.0.1    parent.local
127.0.0.1    child.local

Esc キーを押してコマンドモードに入り、:wq と入力して保存し、vim を終了します。

②Node.js の http-server パッケージを使用するための手順

  1. Node.js のインストール
brew install node
  1. http-server パッケージのインストール
npm install -g http-server

動作確認

①サーバーの起動

サーバーを起動したいフォルダに移動して、http-server を実行します。

親ページ用 (ポート 3000) のサーバーを起動する場合:

親ページ(parent.local)のHTMLファイルなどを含むフォルダに移動して、次のコマンドを実行します。

cd parent
http-server -p 3000

子ページ用 (ポート 3001) のサーバーを起動する場合:

子ページ(child.local)のHTMLファイルなどを含むフォルダに移動して、次のコマンドを実行します。

cd child
http-server -p 3001

②画面表示

ブラウザで親ページにアクセス:
http://parent.local:3000 にアクセスし画面を表示します。

環境構築手順①にて、エントリ追加を行わなかった場合は下記にアクセス
http://localhost:3000(デフォルトのドメイン名)
※コード上のドメインも下記の通り修正が必要
 http://parent.local:3000 ⇨ http://localhost:3000
 http://child.local:3001 ⇨ http://localhost:3001


⇧ 親(Parent Page)のiframe内に子(Child Page)を設定しているイメージ)

③実行から結果確認までの流れ

開発者ツールを使用(F12押下)し、コンソールタブを選択します。
親ページの「Send Message to Child」ボタンをクリックすると、iframe 内の子ページにメッセージが送信されます。
子ページがそのメッセージを受け取り、応答を返します。
正常に処理が行われると、ブラウザの開発者ツール(コンソール)に、親と子の間でやり取りした内容やデータが下記のように出力されます。

最後に

postMessageについて書かれている記事はネットに転がっているものの、具体的な環境構築から動作確認まで行う方法が書かれたサイトが見当たらなかったため、ローカル環境で実現可能な方法を今回記事にしてみました。
簡易サーバーを用意するのが少し手間ですが、一度用意すればあとは起動するだけなので興味がある方は是非試していただけると嬉しいです!
最後までお読みいただきありがとうございました。

Discussion