まとめなど
<iframe>
から送られてくる postMessage を受信したいときに使用する。- 認証状態をチェックするときに使うと便利。
- セキュリティについて、汎用関数内で postMessage の送信元をチェックするようにしているが、これ以外にも例えば
<iframe>
で指定する URL 側でも措置を別途考慮する必要がある点に注意。 - 汎用関数の引数に指定する
<iframe>
の URLは、静的な HTML をレスポンスする場合は想定通りの動きをした。しかし、 Vue Router で定義したページを指定した場合には想定通りの動きをせず、使い物にならなかった。
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 そのものについて理解を深められたので、コードを完成させながらも理解を深めることがよりしやすくなっている実感がありました。
以上です。