2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

iframeを使用した親子プロジェクト間の通信

Last updated at Posted at 2024-03-30

はじめに

仕事上の要件において、1つのReactプロジェクトを別のReactプロジェクト内でiframeを使用して表示する必要が生じました。この過程で、プロジェクト間でのデータの送受信が必要でした。近年、セキュリティの観点からiframeの使用は減少していますが、今回かなり有益な経験だったため、記録として残したいと思います。

前提条件

  • 本記事ではReactをベースに解説しますが、基本的な考え方は他のWEB技術にも応用可能です。
  • TypeScriptを使用します。
  • Reactはバージョン18を使っています。

基本的な考え方

基本的な考え方はとても簡単です。データを送信したい側は対象のwindowオブジェクトのpostMessageを呼び出し、データを受信したい側はwindowaddEventListenerで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>) => {})

となります。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?