React でウェブアプリ開発をしているのですが、
iframe でページを埋め込み親子間でメッセージをやり取りしたい時がありました。
備忘録がてらメモを残します。
React で iframe
他のHTMLタグと同じように、JSXで書けます。
export default function Page() { return ( <iframe src='/embed' /> ) }
ここでは同じドメインの /embed ページを読み込んでます。
親から子にメッセージを渡す
iframe では window.postMessage() でメッセージのやり取りができます。 ここでは親から子にデータを渡してみましょう。
まずは親のコードです。
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> </> ) }
続いて子のコードです。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 を共有してます。さながら tRPC のようです
React Hooks を使う
window.addEventListener
ですが React Hooks のライブラリがあります
- usehooks-ts の useEventListener
- @react-hook/event の useEvent (参考)
どちらも問題なく動きました
終わりに
メッセージの送受信は工夫の余地があります。 おそらくマイクロフロントエンドの課題感に近いんだろうなと思いました。
【余談】そもそもなぜiframeを使うのか
iframe を使う動機ですが、私の場合 JS のグローバル空間汚染を隔離するためでした。
状況としては、GoのWasmアプリを作ってまして、、、 プログラムの一部に、JavaScriptの変数にアクセスするコードが混じってました。
次のようなイメージです。
func main() { ch := make(chan struct{}) // js のグローバル変数を宣言している js.Global().Set("callName", js.FuncOf(callName)) <-ch }
このコードは JavaScript のグローバル空間を汚染します。そのため iframe で隔離したくなった次第です。