はじめに
仕事上の要件において、1つのReactプロジェクトを別のReactプロジェクト内でiframeを使用して表示する必要が生じました。この過程で、プロジェクト間でのデータの送受信が必要でした。近年、セキュリティの観点からiframeの使用は減少していますが、今回かなり有益な経験だったため、記録として残したいと思います。
前提条件
- 本記事ではReactをベースに解説しますが、基本的な考え方は他のWEB技術にも応用可能です。
- TypeScriptを使用します。
- Reactはバージョン18を使っています。
基本的な考え方
基本的な考え方はとても簡単です。データを送信したい側は対象のwindowオブジェクトのpostMessage
を呼び出し、データを受信したい側はwindow
のaddEventListener
でmessageイベントを傍受できるようにします。
import { useEffect } from "react"
type Props = Record<string, unknown>
const Component: React.FC<Props> = ({...props}) => {
const onSendToIFrame = (): void => {
const data: any = {}
// NOTE: データを送信する場合
window.postMessage(data)
}
const handleMessageEvent = (event: MessageEvent<any>): void => {
// NOTE: メッセージを受信した後の処理
// NOTE: event.dataを使用して受信したデータにアクセスできる
}
useEffect(() => {
// NOTE: データを受信する場合
window.addEventListener("message", handleMessageEvent)
return () => {
window.removeEventListener("message", handleMessageEvent)
}
}, [])
return <button onClick={onSendToIFrame}>Send</button>
}
親プロジェクト → 子プロジェクト
ここからより詳しく説明します。
まず、親プロジェクトではiframe
とそれに対してref
の設定を行う必要があります。
import { useRef } from "react"
type Props = Record<string, unknown>
const ParentProject: React.FC<Props> = () => {
const ref = useRef<HTMLIFrameElement | null>(null)
return <React.Fragment>
<iframe ref={ref} />
</React.Fragment>
}
iframeに向けてデータを送信すると仮定します。
すると、以下のようにpostMessage
を使うことでiframeに対してデータを送信することができます。
import { useRef } from "react"
type UserData = {
username: string
}
type PostData = {
request: string,
message: UserData
}
type Props = Record<string, unknown>
const ParentProject: React.FC<Props> = () => {
const ref = useRef<HTMLIFrameElement | null>(null)
const onSendDataToIFrame = (): void => {
// NOTE: refを使ってるので、存在確認をしないとエラーになる可能性がある
if (!ref || !ref.current || !ref.current.contentWindow) {
return
}
// NOTE: 送信するデータは本当になんでもOK
const data: PostData = {
request: "userUpdate",
message: {
username: "John"
}
}
ref.current.contentWindow.postMessage(data, { targetOrigin: "*" })
}
return <React.Fragment>
<button onClick={onSendDataToIFrame}>iframeにデータを送信</button>
<iframe ref={ref} />
</React.Fragment>
}
これを受け取る子プロジェクト側は、iframeで表示されている画面のコンポーネントでmessageイベントを受け取れるようにする。
import { useEffect, useState } from "react"
type UserData = {
username: string
}
type PostData = {
request: string,
message: UserData
}
type Props = Record<string, unknown>
const ChildProject: React.FC<Props> = () => {
const [username, setUsername] = useState("")
// NOTE: MessageEventはReactが用意してる型です
const handleMessageEvent = (event: MessageEvent<PostData>): void => {
const request = event.data.request
const _username = event.data.message.username
if (request === "userUpdate" && !!_username) {
setUsername(_username)
}
}
// NOTE: 送信されているpostMessageを受け取る
useEffect(() => {
window.addEventListener("message", handleMessageEvent)
return () => {
window.removeEventListener("message", handleMessageEvent)
}
}, [])
return <React.Fragment>
{username ? <p>Hello, {username}</p> : null}
</React.Fragment>
}
子プロジェクト → 親プロジェクト
子プロジェクトから親プロジェクトへデータを送信する場合も似たようなアプローチになります。
ただし、親プロジェクトから子プロジェクトへデータを送信する場合はref
に紐づいたwindowオブジェクトのpostMessage
を呼び出していたのに対して、子プロジェクトから親プロジェクトへデータを送信する場合はwindow
オブジェクトにあるparent
プロパティを使うことで親プロジェクトのwindowにアクセスすることができます。
import { useEffect, useState } from "react"
type UserData = {
username: string
}
type PostData = {
request: string,
message: UserData
}
type Props = Record<string, unknown>
const ChildProject: React.FC<Props> = () => {
const [username, setUsername] = useState("")
const onSendDataToParent = (): void => {
const data: PostData = {
request: "requestPermission",
message: "camera",
}
window.parent.postMessage(data, { targetOrigin: "*" })
}
// NOTE: MessageEventはReactが用意してる型です
const handleMessageEvent = (event: MessageEvent<PostData>): void => {
const request = event.data.request
const _username = event.data.message.username
if (request === "userUpdate" && !!_username) {
setUsername(_username)
}
}
// NOTE: 送信されているpostMessageを受け取る
useEffect(() => {
window.addEventListener("message", handleMessageEvent)
return () => {
window.removeEventListener("message", handleMessageEvent)
}
}, [])
return <React.Fragment>
{username ? <p>Hello, {username}</p> : null}
</React.Fragment>
}
※{ targetOrigin: "*" }
はwindow
のオリジンを指定しています。postMessage
を発行するときにwindow
のオリジンが一致しない場合はpostMessage
はメッセージを発行しません。つまり、今回の例のようにどんな親プロジェクトでもデータを送信したい場合は必ず*
を設定する必要があります。特定のオリジンにする場合はURLを指定できます。
このpostMessage
を受け取る親プロジェクトは、子プロジェクトでの受け取り方と全く同じです。
import { useEffect, useRef } from "react"
type UserData = {
username: string
}
type PostData = {
request: string,
message: UserData
}
type Props = Record<string, unknown>
const ParentProject: React.FC<Props> = () => {
const ref = useRef<HTMLIFrameElement | null>(null)
const onSendDataToIFrame = (): void => {
// NOTE: refを使ってるので、そhんざいかくにんをしないとエラーになる可能性がある
if (!ref || !ref.current || !ref.current.contentWindow) {
return
}
// NOTE: 送信するデータは本当になんでもOK
const data: PostData = {
request: "userUpdate",
message: {
username: "John"
}
}
ref.current.contentWindow.postMessage(data, { targetOrigin: "*" })
}
const handleMessageEvent = async (event: MessageEvent<PostData>): Promise<void> => {
const request = event.data.request
if (request === "requestPermission") {
const queryName = event.data.message: as PermissionName
await navigator.permissions.query({ name: queryName })
}
}
useEffect(() => {
window.addEventListener("message", handleMessageEvent)
return () => {
window.removeEventListener("message", handleMessageEvent)
}
}, [])
return <React.Fragment>
<button onClick={onSendDataToIFrame}>iframeにデータを送信</button>
<iframe ref={ref} />
</React.Fragment>
}
サマリー
基本的な考え方としては、
- データの送信 →
postMessage
- データの受信 →
window.addEventListener("message", (event: MessageEvent<any>) => {})
となります。