カテゴリー
Linux

【TypeScript】iframe へ postMessage しそこから postMessage を受け取る、送受信の汎用関数を作った

まとめなど

iframe と送受信する汎用関数のコード

src/utils/postReceiveMessage.ts

/**
 * 受信した MessageEvent.data の型
 * @interface
 */
interface MessageEventData {
    /**
     * レスポンスオブジェクト
     * @type {Object}
     */
    response: {
        /**
         * ステータスコード
         * @type {number}
         */
        status: number;
        // 他のプロパティ
    };
    // 他のプロパティ
}

/**
 * データが MessageEventData 型であるかどうかを検証します。
 * @param {unknown} data - 検証するデータ
 * @returns {data is MessageEventData} - データが MessageEventData 型である場合は true、それ以外の場合は false
 */
const isValidMessageEventData = (data: unknown): data is MessageEventData => {
    return (
        typeof data === "object" &&
        data !== null &&
        "response" in data &&
        typeof (data as MessageEventData).response.status === "number"
    );
};

/**
 * 対象ウィンドウにメッセージを送信し、応答を待ちます。
 * @template T - 送信するメッセージの型。
 * @param {Window} targetWindow - メッセージを送信するウィンドウ。
 * @param {string} targetOrigin - 対象ウィンドウのオリジン。
 * @param {T} message - 送信するメッセージ。
 * @param {number} [timeoutInSeconds=10] - 応答を待つ秒数。タイムアウトするまでの時間。
 * @returns {Promise<MessageEvent<MessageEventData> | Error>} 応答メッセージイベントまたはエラー(タイムアウトした場合)を解決するプロミス。
 */
export const postReceiveMessage = async <T>(
    targetWindow: Window,
    targetOrigin: string,
    message: T,
    timeoutInSeconds: number = 10
): Promise<MessageEvent | Error> => {
    return new Promise((resolve, reject) => {
        const timeoutId = setTimeout(() => {
            window.removeEventListener('message', messageHandler);
            reject(new Error(`${targetOrigin} から ${timeoutInSeconds} 秒応答無く、タイムアウトしました。`));
        }, timeoutInSeconds * 1000);

        const messageHandler = (event: MessageEvent) => {
            if (event.origin !== targetOrigin) {
                return;
            }
            if (!isValidMessageEventData(event.data)) {
                return;
            }

            if (event.data.response.status === 200) {
                resolve(event);
            } else {
                reject(event);
            }

            window.removeEventListener('message', messageHandler);
            clearTimeout(timeoutId);
        };

        window.addEventListener('message', messageHandler);
        targetWindow.postMessage(message, targetOrigin);
    });
}
  • 受信するときのデータ、 MessageEvent.data の構造を isValidMessageEventData 関数でチェックしている。構造の定義は interface MessageEventData で行っている。本関数は汎用関数と謳っているが、プロジェクト等に応じて内容を変更した方が良いだろう。
  • e.data.response.status === 200 を行うことで iframe からの postMessage の内容を精査している。これも本関数は汎用関数と謳っているが、プロジェクト等に応じて内容を変更した方が良いだろう。

使い方の例。 Vue.js を例に。

オリジンは http://localhost:5174 。 汎用関数を使う側。

src/views/PostReceiveMessage.vue

<script setup>
import { onMounted, ref } from "vue";
import { postReceiveMessage } from "../utils/postReceiveMessage";

const authMessageEventHandlerIframe = ref(null);
const idpOrigin = "http://localhost:5173";

const message = "hoge-fuga";

onMounted( () => {
});

const test = async () => {
  const targetIframeWindow = authMessageEventHandlerIframe.value.contentWindow;
  const result = await postReceiveMessage(targetIframeWindow, idpOrigin, message);
  console.log({ result });
};

</script>

<template>
  <button @click="test" />
  <iframe ref="authMessageEventHandlerIframe" src="http://localhost:5173/test1.html"
    style="display: none;"></iframe>
</template>

続いて、オリジンは http://localhost:5173 。 汎用関数から呼び出される側。静的 HTML としている。 Vue.js のページでも機能すると思ったのだが、できなかったので静的な HTML としている。

test1.html

<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document aap1 test1.html</title>
</head>

<body>
    <p>Document aap1 test1.html</p>
</body>

<script>
    console.log("app1 test1.html start");

    window.addEventListener("message", receiveMessage, false);
    function receiveMessage(event) {
        const targetOrigin = "http://localhost:5174";
        if (event.origin !== targetOrigin) {
            return;
        }

        // 色々な処理

        // メッセージ送信
        const message = {
            response: {
                status: 200,
                message: "OK",
            }
        };
        event.source.postMessage(message, targetOrigin);
    }
</script>

</html>

おわりに

前回と比べて、想定するユースケースはより複雑になっています。つまり、前回はただ受け取るだけでしたけれども、今回は、送信して、相手がそれを処理し、結果を受け取る、というようになっています。

複雑にはなっていますけれども、実現する汎用関数としてはコードはあまり違いがありません。むしろ呼び出される側でデータを処理する必要がでてくる、つまり動的になるので呼び出される側が複雑になるかと思います。しかしながら呼び出される側で処理を行うことによって、例えば呼び出されるオリジンでCookieを持たせたり呼び出されるオリジンの localStorage を操作できたりするので、応用例は多くありそうです。

以上です。

コメントを残す