devlog

[React] iframe の親子間で postMessage する

2023-07-10

概要

  • React
  • iframe の親子間でメッセージをやり取りするには window.postMessage() を用いる

React で iframe

そもそも iframe タグですが、他のHTMLタグと同じようにJSXで書けます。

export default function Page() { // 埋め込み return ( <iframe src='/embed' /> ) }

ここでは同じドメインの /embed ページを読み込んでます。

親から子にメッセージを渡す

iframe では window.postMessage() でメッセージのやり取りができます (MDN)
ここでは親から子にデータを渡してみましょう。

まずは親のコードです。

import { useRef, MouseEventHandler } from 'react' export default function Page() { const child = useRef<HTMLIFrameElement>(null) // ボタンをクリックしたら.. const handleClick: MouseEventHandler<HTMLButtonElement> = (e) => { e.preventDefault() // 子に hey と言うメッセージを投げる child.current?.contentWindow?.postMessage('hey', 'http://localhost:3000') } return ( <> <iframe src='/embed' ref={child} /> <button onClick={handleClick}>aa</button> </> ) }

同じドメインの /embed ページを読み込んでます。
そしてボタンを押したら子に hey というメッセージを送ります。

続いて子のコードです。messageイベント で受け取れます

export default function Page() { window.addEventListener('message', (event) => { console.log(event.data) // hey }) return <>a</> }

実行するとコンソールに hey という文字列が流れてきます

   

子から親にメッセージを渡す

逆をしてみます。

まず受信側(親)のコードです

export default function Page() { window.addEventListener('message', (event) => { console.log(event.data) // hey }) return ( <iframe src='/embed' /> ) }

続いて送信側(子)のコードです

export default function Page() { window.parent.postMessage('hey') return <>a</> }

window.parent で親の window への参照を取得できます。

event を絞り込む

何気なく

window.addEventListener('message', (event) => {})

というコードを書いてますが、ライブラリによってはこのイベント (message) を使っており、コールバック関数にそのメッセージが入ってくることがあります。例えば react-devtools のメッセージが入ってきます (参考)

ここでは送信データを工夫します。次のような type を定義しガードします

export type AppMessage = { source: 'me' // 一意になるよう.. text: string } export const isAppEvent = (event: MessageEvent<any>): event is MessageEvent<AppMessage> => { return typeof event.data === 'object' && event.data?.source === 'me' && typeof event.data?.text === 'string' }

ポイントは source です。一意の値を入れ判別できるようにしました

これを使ってみます。まずは送信側(親)です

export default function Page() { const child = useRef<HTMLIFrameElement>(null) const handleClick: MouseEventHandler<HTMLButtonElement> = (e) => { e.preventDefault() // メッセージ const message: AppMessage = {source: 'me', text: 'hey'} child.current?.contentWindow?.postMessage(message, 'http://localhost:3000') } return ( <> <iframe src='/embed' ref={child} /> <button onClick={handleClick}>aa</button> </> ) }

続いて受信側(親)です

export default function Page() { window.addEventListener('message', (event) => { // type guard if (isAppEvent(event)) { console.log(event.data.text) // hey } }) return ( <iframe src='/embed' /> ) }

親と子とで type を共有してます

React Hooks を使う

window.addEventListener ですが React Hooks のライブラリがあります

どちらも問題なく動きました

終わりに

メッセージの送受信は工夫の余地があります。
おそらくマイクロフロントエンドの課題感に近そうに感じます。    

[余談] そもそもなぜ iframe を使うのか

iframe を使う動機ですが、私の場合 JavaScript のグローバル空間汚染を隔離するためでした。

GoでWasmアプリを作ってまして、、
プログラムに JavaScript の変数へアクセスするコードが混じってました。

func main() { ch := make(chan struct{}) // js のグローバル変数を宣言している js.Global().Set("callName", js.FuncOf(callName)) <-ch }

このコードは JavaScript のグローバル空間を汚染します。
そのため iframe で隔離したくなった次第です。

  • 作成日
    2023-07-10
  • 更新日
    2024-06-09