SPIDERPLUS Tech Blog

建設SaaS「スパイダープラス」のエンジニアとデザイナーのブログ

iframeをデータ受け渡しのために使ってみた

こんにちは、技術推進部の谷黒と申します。


スパイダープラスでは日々の業務での出来事や向き合った技術的課題を定期的にブログとして発信しております。
さて、今回はiframeをテーマにします。こちらは外部サイトなどをHTML内に埋め込みたい時などに使うことが一般的です。


ご存じの方もいらっしゃると思いますが、iframeはフレーム内の値をフレームの外の世界に渡すことができます。
今回、私たちはヘッドレスなAPIを使わずiframeをデータ受け渡し専用に用いた手法を採用し、機能開発を行いました。その方法を紹介いたします。

背景

私が開発を担当しているBIM(*Building Information Modelingの略で図面上により詳しい属性情報などがついたモデリングのこと)のサービスは、基本機能を提供するSPIDERPLUSとはマイクロサービス的な関係にあり、SPIDERPLUSとは独立したサーバーとして動作しています。

今回はSPIDERPLUSからBIMサーバーに対してcsvのダウンロード処理を依頼するという要件に対して、iframeとformを用いる手法を採用しました。

APIを用いてcsvダウンロードを行わなかった理由は、csvダウンロード処理がフロントエンドで動作するJavaScriptライブラリに依存しているためです。そのため、フロントエンドに対してcsvダウンロードの依頼を行いたかったからです。

また、ヘッドレスなAPIの実装ではなくiframeを採用した理由は、Reactの状態管理をiframeの呼び出し側に送ることで、簡単に呼び出し側でステータスを受け取れるという便利な面があったからです。

サンプル

example_a/index.html

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Example_a</title>
    <script>
        function submitForm() {
            const form = document.getElementById('myForm');
            form.submit();
        }

        // exampleBで発火したイベントを受け取る
        window.addEventListener('message', function(event) {
            if (event.origin !== 'http://localhost:3000') {
                // セキュリティのため、信頼できるオリジンからのメッセージのみを受け入れる
                return;
            }
            console.log('Received status:', event.data);
            // 受け取ったデータをinnerTextに割り当てて表示する
            document.getElementById('receivedStatus').innerText =  event.data;
        });

    </script>
</head>
<body>
    <h1>Example_a</h1>
    <p>↓Example_bからの出力を表示</p>
    <!-- BIMサーバー側にbim_id=12345678を送る -->
    <form id="myForm" action="http://localhost:3000/example_b" method="GET" target="myIframe">
        <input type="hidden" name="bim_id" value="12345678" />
    </form>
    <iframe id="myIframe" name="myIframe" style="display: none;"></iframe>
    <div id="myDiv">
        <h1 id="receivedStatus"></h1>
    </div>
    <div>
        <button onclick="submitForm()">送信</button>
    </div>
</body>
</html>

example_b/ExampleB.jsx

import React from 'react';
import { useLocation } from 'react-router-dom';
import useSWR from 'swr';

function ExampleB() {
    const location = useLocation();
    const params = new URLSearchParams(location.search);
    const bimId = params.get('bim_id');

    const downloadCsv = () => new Promise((resolve, reject) => {
        // 2秒後にダウンロード完了したつもりで結果を返す(またはエラー)
        setTimeout(() => {
            if (Math.random() > 0.5) {
                alert('csvをダウンロードしました。');
                resolve('ダウンロード完了');
            } else {
                alert('csvのダウンロードに失敗しました。');
                reject('ダウンロード失敗')
            }
        }, 2000);
    });
    // { revalidateOnFocus: false }を指定しないとダウンロード処理が繰り返し行われてしまう
    const { data, error, isLoading } = useSWR(bimId ? 'downloadKey' : null, downloadCsv, { revalidateOnFocus: false });

    return (
        <div>
            {isLoading && window.parent.postMessage('ダウンロード中', '*')}
            {data && window.parent.postMessage(data, '*')}
            {error && window.parent.postMessage(error, '*')}
        </div>
    );
}

export default ExampleB;

サンプルの解説

iframeを用いることで、フロントエンドのみでダウンロード処理を行っております。
reactとSWRを用いてダウンロードを管理し、その値をwindow.parent.postMessage()を介してexample_aに戻しています。


呼び出す側ではiframeを表示しておりません。iframeはデータを受け渡すために用いて、連携したいシステムとの連携部品のような役割を担っています。


今回、csvデータのダウンロードに関しての説明は割愛させていただきます。ダウンロード処理を行った個所についてはコメントしておりますので、「ここでダウンロード処理があるんだ」程度にご認識くださったら幸いです。

window.parent.postMessage()

※参照: 

swr.vercel.app

window.parent.postMessage() をつかうことで、iframeを使っている側はexample_aにイベントを発火させることでメッセージを送ることができます。
メッセージは構造化複製アルゴリズムに従ってシリアル化されるので様々な形式のデータをJSON.stringfy等の変換の必要なく安全に渡すことができます。
example_bで発火させたイベントはexample_aで window.addEventListener('message', function(event) {} で受け取ります。
これを応用して、このメッセージにstatusを入れてexample_aに送ります。
すると、example_aでiframeを通して表示していたexample_bの値を取得できます。
受け取ったメッセージはexample_aのHTMLElement内で表示すれば良いので、iframeでexample_bを表示する必要がありません。

ここで<iframe>は非表示にしてexample_bから値を受け取るだけの窓としての役割になります。

revalidateOnFocus: false

※参照:

swr.vercel.app

SWR はページにフォーカスを合わせるかタブを切りかえると、自動的にデータを再検証します。これは状態を常に最新に保つための機能なのですが、今回のケースですと期待しない挙動をしてしまうことがあります。


サンプルの例では revalidateOnFocus: false を指定せず実行すると、タブやウィンドウを切り替えるたびにダウンロード処理が走ってしまいます。
今回のケースでは自動検証を無効化するために revalidateOnFocus: false を指定しました。
useSWRImmutable(bimId ? 'downloadKey' : null, downloadCsv);
でも同じことが実現できましたので、興味のある方は両方お試しください。
以上がサンプルを使った解説です。

メリット

データ受け渡しのためにiframeを使うことで得られるメリットは以下のとおりです。

フロントエンドの実装で実現できる

iframeを用いてAPIを使わず処理をできるケースがあります。csv作成処理については今回触れておりませんが、実際に私たちは当該処理をフロントエンドで行っております。こういったケースではバックエンドのAPIを作成せず実現することができます。

デザインに統一性を持たせることができる

今回のケースにおいて副次的なメリットではありますが、iframeを表示しないことでiframeを呼び出す側のデザインに統一性(サンプルではexample_a)を持たせることができ、コードの見通しが良くなりました。

デメリット

大抵のデータは送ることができると申し上げましたが、 window.parent.postMessage() で指定できるメッセージは関数やエラーオブジェクト等、一部使用できないものがあります。これらを送りたいときはこの手法は有効ではありません。その点に関しては注意が必要です。

まとめ

iframeによるデータの受け渡しによってフロントエンド側の技術のみで実装可能というメリットが享受できました。限定的な条件下でしか有効でありませんが、iframeを連携したいシステムとの連携部品として使うことはトリッキーで面白い手法なので今回紹介いたしました。
今回と同様にこの手法を使えるケースは限られると思いますが、場合によっては強力な手法になる可能性があります。この方法が皆様の課題解決の役に立てると幸いです。

スパイダープラスでは仲間を募集中です。 スパイダープラスにちょっと興味が出てきたなという方がいらっしゃったらお気軽にご連絡ください。

最後までご覧くださり、ありがとうございます。