1. 記事一覧 >
  2. ブログ記事
category logo

iframeを使ったダウンロードでChromeの場合、完了イベントを検知できない

(更新) (公開)

JavaScriptでiframeを使って、ダウンロードをすると、準備完了時にイベントが発生し、検知できます。ただし、Chromeではイベントが発生しないため、検知できず、Cookieを使うといった工夫が必要です。
大前提として、ダウンロード中に他の操作をされたら困るという要件があり、その間画面をブロックする仕様とします。正確には、ダウンロード中と言うか、ダウンロード準備中です。ダウンロードが始まれば、あとはブラウザが裏でダウンロードします。
準備完了とは、ブラウザの保存ダイアログが表示されたり、ブラウザの制御下に入るタイミングのことです。
iframeを使ってダウンロード


マウスオーバーでアクションが起きる緑、青、赤、グレーのバーは、ブロックされていることを示すためで、意味は有りません。


【検証環境】

Windows10 Pro 64bit

 IE11

 Edge 40.15063.0.0(非Chromium)

 Edge(Chromium) 93.0.961.38

 Chrome 93.0.4577.63

 Firefox 78.13.0esr

 Windows版Safari 5.0.5

CentOS Linux release 8.3.2011

 Apache/2.4.37

 PHP 7.4.6


まとめ

結論から書きますと、iframeを使ったダウンロードでChromeの場合、準備完了イベントを検知できない問題は、Cookieを使うと解決します。


iframeのイベントを使ってダウンロード準備完了を検知

ブラウザダウンロード準備完了検知
IE11
Edge(非Chromium)
Edge(Chromium)×
Chrome×
Firefox
Windows版Safari

ChromeとEdge(Chromium)は、ダウンロード準備完了を検知できません。

Cookieを使ってダウンロード準備完了を検知

ブラウザダウンロード準備完了検知
IE11
Edge(非Chromium)
Edge(Chromium)
Chrome
Firefox
Windows版Safari

Cookieを使ってダウンロード準備完了を検知できます。さらに、イベントを検知するわけでは無いため、iframeを使う必要が無くなります。
アンカーリンク(aタグ)でダウンロードしても目的(画面ブロック解除)は達成します。
Cookieを使ってダウンロード準備完了を検知


イベントの検知

各ブラウザで以下のようにダウンロード準備完了イベントの検知を試みた結果です。
<iframe src="/download.php" onReadyStateChange="readyStateChange()" onLoad="loaded()"></iframe>


iframeのイベントを使ってダウンロード準備完了を検知

ブラウザダウンロード準備完了イベント
IE11onReadyStateChange
Edge(非Chromium)無し
Edge(Chromium)無し
Chrome無し
FirefoxonLoad
Windows版Safari無し

単純にイベントだけではカバーしきれないため、
document.getElementById('download').contentDocument.readyState;
の値を調べます。

ブラウザreadyState
IE11loading → interactive
Edge(非Chromium)loading → interactive
Edge(Chromium)completeのまま
Chromecompleteのまま
Firefoxuninitialized → complete
Windows版Safariloading → complete

ダウンロード準備中→ダウンロード準備完了 で、Edge(非Chromium)は、interactive、Safariは、completeを見ると分かります。
ChromeとEdge(Chromium)は、この方法でもずっとcompleteのため、ダウンロード準備完了を検知できません。


Cookieで検知のしくみ

以下のようにデータ送出と同時にCookieが送られるようにして、Cookieの有無でダウンロード準備完了を検知します。
Cookieで検知のしくみ


アンカーリンクでのダウンロード

既存システムの改修などでもなく、特に縛りが無い場合、iframeである必要は無くなります。 アンカーリンク(aタグ)にして、クッキー検知で統一すれば良いです。


Cookieを使ってダウンロード準備完了を検知

ブラウザダウンロード準備完了検知
IE11
Edge(非Chromium)
Edge(Chromium)
Chrome
Firefox
Windows版Safari

Cookieを使ってダウンロード準備完了を検知


動画gifのソースコード

画面側ソースコード:

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Download via iframe</title>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.0.0/jquery.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-modal/0.9.1/jquery.modal.min.js"></script>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jquery-modal/0.9.1/jquery.modal.min.css" />
    <script>
        var intervalID;
        function readyStateChange() {
            $.modal.close();
        }
        window.onload = function () {
            $("#anchorlink").on("click", function () {
                $("#blockui").modal({
                    escapeClose: false,
                    clickClose: false,
                    showClose: false
                });
                document.cookie = "downloaded=; path=/; expires=" + new Date(0).toUTCString() + ";";//Thu, 01 Jan 1970 00:00:00 GMT
                intervalID = setInterval(function () {
                    if (document.cookie.indexOf("downloaded") >= 0) {
                        document.cookie = "downloaded=; path=/; expires=" + new Date(0).toUTCString() + ";";
                        clearInterval(intervalID);
                        $.modal.close();
                    }
                }, 500);
            });
            $("#btn").on("click", function () {
                $("#blockui").modal({
                    escapeClose: false,
                    clickClose: false,
                    showClose: false
                });
                if (!!window.chrome && /Chrome/.test(navigator.userAgent)) {//Chrome
                    document.cookie = "downloaded=; path=/; expires=" + new Date(0).toUTCString() + ";";
                    var html = '<iframe id="download" src="/download.php">';
                    document.getElementById("frame").innerHTML = html;
                    intervalID = setInterval(function () {
                        if (document.cookie.indexOf("downloaded") >= 0) {
                            document.cookie = "downloaded=; path=/; expires=" + new Date(0).toUTCString() + ";";
                            clearInterval(intervalID);
                            $.modal.close();
                        }
                    }, 500);
                } else if (/*@cc_on!@*/ false || !!document.documentMode) {//IE11
                    var html = '<iframe id="download" src="/download.php" onReadyStateChange="readyStateChange()" width=0 height=0></iframe>';
                    document.getElementById("frame").innerHTML = html;
                } else if (/constructor/i.test(window.HTMLElement) ||
                    (function (p) {
                        return p.toString() === '[object SafariRemoteNotification]';
                    })(!window['safari'] || (typeof safari !== 'undefined' && safari.pushNotification)) ||//Safari
                    (!(/*@cc_on!@*/ false || !!document.documentMode) && !!window.StyleMedia)) {//Edge
                    var html = '<iframe id="download" src="/download.php">';
                    document.getElementById("frame").innerHTML = html;
                    intervalID = setInterval("timer_event()", 200);
                } else {
                    var iframe = document.createElement("iframe");
                    iframe.src = "/download.php";
                    if (document.all) {
                        iframe.onreadystatechange = function () {
                            switch (this.readyState) {
                                case "complete":
                                case "interactive":
                                    $.modal.close();
                                    this.onreadystatechange = null;
                            }
                        }
                    } else {
                        iframe.onload = function () {
                            $.modal.close();
                        }
                    }
                    document.getElementById("frame").appendChild(iframe);
                }
            });
        }
        function timer_event() {
            var iframe = document.getElementById("download");
            var status;
            status = iframe.contentDocument.readyState;
            if (status == "complete" || status == "interactive") {
                clearInterval(intervalID);
                $.modal.close();
            }
        }
    </script>
    <style>
        html {
            font-family: ヒラギノ角ゴ Pro, Hiragino Kaku Gothic Pro, メイリオ, Meiryo, Osaka, MS Pゴシック, MS PGothic, sans-serif;
        }
        #sidenav a {position: absolute; left: -80px; transition: 0.3s; padding: 15px; width: 100px; text-decoration: none; font-size: 20px; color: white; border-radius: 0 5px 5px 0;}
        #sidenav a:hover {left: 0;}
        #about {top: 20px; background-color: #04AA6D;}
        #blog {top: 80px; background-color: #2196F3;}
        #projects {top: 140px; background-color: #f44336;}
        #contact {top: 200px; background-color: #555}
        #btn {position: absolute; top: 300px; background-color: DodgerBlue; border: none; color: white; padding: 12px 30px; cursor: pointer; font-size: 20px;}
        #btn:hover {background-color: RoyalBlue;}
        #blockui>p {text-align: center}
        #anchorlink {position: absolute; top: 400px;}
    </style>
</head>
<body>
    <div id="sidenav" class="sidenav">
        <a href="#" id="about">About</a>
        <a href="#" id="blog">Blog</a>
        <a href="#" id="projects">Projects</a>
        <a href="#" id="contact">Contact</a>
    </div>
    <button id="btn"><i class=" fa fa-download"></i> Download</button>
    <div id="blockui" style="display: none;">
        <p>Please wait</p>
    </div>
    <div id="anchorlink"><a href="/download.php">Download</a></div>
    <div id="frame" style="display: none;"></div>
</body>
</html>

サーバー側ソースコード(php):

<?php

sleep(2);//何らかの時間がかかる処理
$file = 'test.dat';

setcookie('downloaded', 'true', time() + 3600, '/');
header('Content-type: application/octet-stream');
header('Content-Disposition: attachment; filename='.basename($file));
header('Content-Length: '.filesize($file));
mb_http_output('pass');
readfile($file);
echo filesize($file);
exit;

Cookieが残ったままになるとまずいため、expires(クッキーの有効期限)を一時間後に設定しています。※念のための処置で、ダウンロード準備に一時間かかると検知できなくなるわけではありません。


ajaxを使う場合

ajaxを使ってダウンロードする方法を試しました。
参考:Handle file download from ajax post(stackoverflow)

・ダウンロード準備完了ではなく、本当にダウンロード完了の時までブロックになりました。

・ダウンロード対象の容量が大きいと動作しないようです。

・Windows版Safariは動きませんでした。

この例では、jquery.blockUI.jsで画面をブロックしています。
$(document).ajaxStop(stopWaiting);のところで画面ブロックを解除しています。

【 ajaxStop 】

ajaxリクエストの送信終了時に実行される関数を引数に指定します。


ajaxでファイルダウンロード


ajaxを使ってダウンロードのサンプルコード:
※php側は同じ実装とします。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Download from ajax</title>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.0.0/jquery.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery.blockUI/2.70/jquery.blockUI.js"></script>
    <script>
        function startWaiting() {
            $.blockUI({
                message: 'Please wait...',
                css: {
                    width: '250px',
                    padding: '30px 0px',
                    border: 'none',
                    cursor: 'wait'
                }
            });
        }
        function stopWaiting() {
            $.unblockUI();
        }
        $(document).ajaxStop(stopWaiting);
        window.onload = function () {
            $('#btn').on('click', function () {
                startWaiting();
                $.ajax({
                    type: 'GET',
                    url: '/download.php',
                    xhrFields: {
                        responseType: 'blob' // to avoid binary data being mangled on charset conversion
                    },
                    success: function (blob, status, xhr) {
                        // check for a filename
                        var filename = "";
                        var disposition = xhr.getResponseHeader('Content-Disposition');
                        if (disposition && disposition.indexOf('attachment') !== -1) {
                            var filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/;
                            var matches = filenameRegex.exec(disposition);
                            if (matches != null && matches[1]) filename = matches[1].replace(/['"]/g, '');
                        }
                        if (typeof window.navigator.msSaveBlob !== 'undefined') {
                            // IE workaround for "HTML7007: One or more blob URLs were revoked by closing the blob for which they were created. These URLs will no longer resolve as the data backing the URL has been freed."
                            window.navigator.msSaveBlob(blob, filename);
                        } else {
                            var URL = window.URL || window.webkitURL;
                            var downloadUrl = URL.createObjectURL(blob);
                            if (filename) {
                                // use HTML5 a[download] attribute to specify filename
                                var a = document.createElement("a");
                                // safari doesn't support this yet
                                if (typeof a.download === 'undefined') {
                                    window.location.href = downloadUrl;
                                } else {
                                    a.href = downloadUrl;
                                    a.download = filename;
                                    document.body.appendChild(a);
                                    a.click();
                                }
                            } else {
                                window.location.href = downloadUrl;
                            }
                            setTimeout(function () { URL.revokeObjectURL(downloadUrl); }, 100); // cleanup
                        }
                    }
                });
            });
        }
    </script>
    <style>
        html {font-family: ヒラギノ角ゴ Pro, Hiragino Kaku Gothic Pro, メイリオ, Meiryo, Osaka, MS Pゴシック, MS PGothic, sans-serif;}
        #btn {position: absolute; top: 100px; background-color: DodgerBlue; border: none; color: white; padding: 12px 30px; cursor: pointer; font-size: 20px;}
        #btn:hover {background-color: RoyalBlue;}
    </style>
</head>
<body>
    <button id="btn"><i class=" fa fa-download"></i> Download</button>
</body>
</html>
loading...