カテゴリー
Linux

【TypeScript】iframe からの postMessage を受け取る汎用関数を作った

まとめなど

iframe からの postMessage を受け取る汎用関数のコード

src/utils/receiveMessageViaIframe.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"
    );
};

/**
* iframe を使用してメッセージを受信します。
* @param {string} iframeSrc - iframe の src 属性に設定する URL
* @param {string} sourceOrigin - メッセージを受信するオリジン
* @param {number} [timeoutInSeconds=60] - タイムアウトまでの時間(秒)
* @returns {Promise<MessageEvent<MessageEventData>|Error>} - メッセージイベントまたはエラーを返す Promise
*/
export const receiveMessageViaIframe = (
    iframeSrc: string,
    sourceOrigin: string,
    timeoutInSeconds: number = 10
): Promise<MessageEvent<MessageEventData> | Error> => {
    return new Promise<MessageEvent<MessageEventData> | Error>((resolve, reject) => {
        const iframe = window.document.createElement('iframe');

        iframe.setAttribute('width', '0');
        iframe.setAttribute('height', '0');
        iframe.style.display = 'none';

        const removeIframe = () => {
            if (window.document.body.contains(iframe)) {
                window.document.body.removeChild(iframe);
                window.removeEventListener('message', iframeEventHandler);
            }
        };

        const timeoutSetTimeoutId = setTimeout(() => {
            reject(new Error(`${sourceOrigin} から ${timeoutInSeconds} 秒応答無く、タイムアウトしました。`));
            removeIframe();
        }, timeoutInSeconds * 1000);

        const iframeEventHandler = (e: MessageEvent<MessageEventData>) => {
            if (e.origin != sourceOrigin) return;
            if (!isValidMessageEventData(e.data)) return;

            e.data.response.status !== 200
                ? reject(e)
                : resolve(e);

            clearTimeout(timeoutSetTimeoutId);
            window.removeEventListener('message', iframeEventHandler);

            // Delay the removal of the iframe to prevent hanging loading status
            // in Chrome: https://github.com/auth0/auth0-spa-js/issues/240
            const cleanupIframeTimeoutInSeconds = 2;
            setTimeout(removeIframe, cleanupIframeTimeoutInSeconds * 1000);
        };

        window.addEventListener('message', iframeEventHandler);
        window.document.body.appendChild(iframe);
        iframe.setAttribute('src', iframeSrc);
    });
};
  • receiveMessageViaIframe 関数が理解できれば良い。他は枝葉である。
  • iframe からの postMessage を受け取った時のデータ、 MessageEvent.data の構造を isValidMessageEventData 関数でチェックしている。構造の定義は interface MessageEventData で行っている。本関数は汎用関数と謳っているが、プロジェクト等に応じて内容を変更した方が良いだろう。
  • e.data.response.status !== 200 を行うことで iframe からの postMessage の内容を精査している。これも本関数は汎用関数と謳っているが、プロジェクト等に応じて内容を変更した方が良いだろう。

使い方の例。 Vue.js を例に。セキュリティチェックのロジックなどは省いた動きを確認するためのコード

オリジンは http://localhost:5174 。 汎用関数を使う側、 <iframe> を呼び出す側。

src/views/ReceiveMessageViaIframe.vue

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

const message = ref(null);

onMounted(async () => {
  const iframeSrc = "http://localhost:5173/test.html";
  const sourceOrigin = "http://localhost:5173";
  message.value = await receiveMessageViaIframe(iframeSrc, sourceOrigin);
  console.log(message.value);
});
</script>

<template>
    <h1>ReceiveMessageViaIframe</h1>
    <code>{{ JSON.stringify(message) }}</code>
</template>

オリジンは http://localhost:5173 。 <iframe> src 属性値に設定された、呼び出される HTML 。実際に使うときは静的な HTML では無く動的にし、セキュリティチェックをはじめビジネスロジックを経由して HTML をレスポンスすることになると思う。

test.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 test.html</title>
</head>

<body>
    <p>Document aap1 test.html</p>
</body>
<script>
    console.log("app1 test.html");
    const message = {
        response: {
            status: 200,
            message: "OK",
        }
    };
    const targetOrigin = "http://localhost:5174";

    this.parent.postMessage(message, targetOrigin);
</script>
</html>

おわりに

本ページの内容は、 auth0 の iframe を使ったトークンの保持の手法を調べたので、Django と Next で実装してみた | きむそん.dev に掲載されたやり方の元となる auth0-spa-js/utils.ts at master · auth0/auth0-spa-js について、これをベースに自分なりに改良したものです。

初めて TypeScript を書いてみました。 JavaScript からすんなり書き直せた、コードの一文だけ書き直すことができた、徐々に TypeScript 化できた、ということから体験がかなり良かったと感じました。おそらく積極的に使っていくと思います。

また、 Bing AI にリファクタリングの提案を受けながら作ってみました。やり取りの中で、 TypeScript での書き方や、 TypeScript そのものについて理解を深められたので、コードを完成させながらも理解を深めることがよりしやすくなっている実感がありました。

以上です。

コメントを残す