🍨

postMessageを使って、ブラウザ上でオリジン間のデータ共有を実施

いばらき2023/03/13に公開

やりたいこと

ブラウザ上で、あるページ(仮にsrc.jp/src.htmlとする)から異なるオリジンのページ(仮にpopup.jp/popup.htmlとする)を別ウインドウで開き、その後2つのページ間でデータ共有をしたい。

課題

オリジン(ドメイン)が違うのでセキュリティ上の制約でLocal StorageやCookieの共有は出来ません。もちろんdomの操作も出来ません。
API作ってサーバー側で処理すればやりようがあるけど、面倒くさいのでやりたくないです。フロントエンドだけで完結させたいです。

解決策

Window.postMessage()を使いました。
https://developer.mozilla.org/ja/docs/Web/API/Window/postMessage

window.postMessage() は、 Window オブジェクト間で安全にオリジン間通信を可能にするためのメソッドです。例えば、ポップアップとそれを表示したページの間や、iframe とそれが埋め込まれたページの間での通信に使うことができます。

とのことで、まさにやりたいことを実現する為の関数が用意されていました。

余談

postMessageの世の中のメジャーな活用事例としては、
Youtubeをiframeで埋め込んだ時にプレイヤーを制御するのに使われるみたいです。
なるほどと思いました。

postMessageの使い方

送る側

こんな感じでデータを送れます。

(送信先のWindow).postMessage({
	action: 'データを送るよ!',
	message: 'メッセージだよ!',
}, 'http://(送信先のドメイン):(port)')
  • 第一引数は任意のオブジェクトです。送信元と送信先で合意して、フォーマットを決めてください。
  • 第二引数は、送信先のオリジンです。*も使えますがお勧めはしないです。
  • (送信先のWindow)は下記のように取れます。
    • 自分が開いたwindow(親→子にデータを送る時)
      • window.open(openURL)の戻り値
    • 自分を開いたwindow(子→親にデータを送る時)
      • window.opener
    • 受け取ったメッセージに返信する時
      • event.source

受け取る側

こんな感じでデータを受け取れます。

window.addEventListener('message', (event) => {
	if (event.origin !== 'http:(送信元のドメイン:(port)')
		return;
	if (event.data.action == 'データを送るよ!') {
		console.log(event.data.message);
	}
});
  • window.addEventListenerを呼び出して、第一引数にmessage、第二引数にメッセージを受け取った時に動く関数を書きます。
  • メッセージを受け取ったら、最初に送信元のオリジンをチェックしてください。
    • チェックが漏れると怪しいサイトのiframeに埋め込まれて不審なデータを送り込まれた場合でも、受け入れて処理が動いてしまうので要注意です。
  • dataに送信されたオブジェクトが入っているので、送信元と送信先で合意したフォーマットに従って、中身を取り出してください。

サンプル実装

クロスオリジンのサンプルを実現する為に複数のドメインを用意するは面倒くさかったので、
http://127.0.0.1:8080/src.htmlからhttp://localhost:8080/popup.htmlをポップアップで開いて、その後データを送りつけるサンプルを書いてみました。両方同じじゃねーかと思うかもしれませんが、127.0.0.1とlocalhostは別オリジンなのでサンプルとして問題なしです。

サンプルの処理フロー

こんな感じのフローで書きました。

最初は4と5の処理を書かずに実装したのですが、popupを開く処理とデータを送信する処理が非同期動くので画面が開く前にデータを送りつけるという状態になりました。この為4と5をハンドシェイク的な処理として追加しています。親→子のデータ送信のサンプルを作るつもりが、結果的に相互通信になりました。

サンプルコード

src.html
<!DOCTYPE html>
<html>

<!-- http://127.0.0.1:8080/src.html で実行してください -->

<head>
  <meta charset="UTF-8">
  <title>送信元</title>
  <script>
    //通信する相手を指定
    const sendDomain = "http://localhost:8080";
    const openURL = sendDomain + "/popup.html";

    // 新規タブを開く
    const popup = () => {
      window.open(openURL);
    }

    // データを送る
    const send = (w, m = "") => {
      console.log('データを送信するよ')
      w.postMessage({
        action: 'SyncMessage',
        message: m,
      }, sendDomain);
    }

    // 通知を受け取ったら、データを送りつける
    window.addEventListener('message', (event) => {
      if (event.origin !== sendDomain)
        return;
      if (event.data.action == 'Notification') {
        console.log('データを受け取ったよ');
        const input = document.getElementById("input").value;
        send(event.source, input);
      }
    });
  </script>
</head>

<body>
  <h1>送信元</h1>
  <textarea id="input">このテキストを送るよ!</textarea>
  <input type="button" value="別タブを開く" onclick="popup();" />
</body>

</html>
popup.html
<!DOCTYPE html>
<html>

<!-- http://localhost:8080/popup.html で実行してください -->

<head>
  <meta charset="UTF-8">
  <title>受け取り側</title>
</head>

<body>
  <h1>受け取り側</h1>
  <p id="text">データが来てないよ</p>
</body>

<script>
  //通信する相手を指定
  const srcDomain = "http://127.0.0.1:8080";

  // データを受け取る処理
  window.addEventListener('message', (event) => {
    if (event.origin !== srcDomain)
      return;
    if (event.data.action == 'SyncMessage') {
      console.log('データを受け取ったよ');
      console.log(event.data.message);
      document.getElementById("text").innerText = event.data.message;
    }
  });

  // ファイルが読み込まれたら親に通知する
  console.log('親に通知するよ');
  console.log(window.opener)
  try {
    window.opener.postMessage({
      action: 'Notification',
      message: 'ok',
    }, srcDomain);
  } catch (e) {
    console.log('エラーだよ!');
    document.getElementById("text").innerText = '親が見つからない。。。';
  }
</script>

</html>

動かしてみた

全角50万文字を送信してみました。 ※gifアニメです↓

最後に

久しぶりにバニラのjavascript+htmlで書いた。

NCDCエンジニアブログ

NCDC株式会社( https://ncdc.co.jp/ )のエンジニアチームです。

Discussion

ログインするとコメントできます