检测浏览器何时接收文件下载

577
我有一个页面,允许用户下载动态生成的文件。它需要很长时间来生成,因此我想显示一个“等待”指示器。问题是,我无法确定浏览器何时接收到文件,以便我可以隐藏指示器。
我请求一个隐藏表单,POST到服务器,并针对其结果定位到一个隐藏的iframe。这样,我就不会用结果替换整个浏览器窗口。我监听iframe上的“load”事件,希望在下载完成时触发它。
我返回一个带有文件的“Content-Disposition: attachment”头,这会导致浏览器显示“保存”对话框。但浏览器不会在iframe中触发“load”事件。
我尝试的一种方法是使用多部分响应。因此,它将发送一个空的HTML文件以及附加的可下载文件。
例如:
Content-type: multipart/x-mixed-replace;boundary="abcde"

--abcde
Content-type: text/html

--abcde
Content-type: application/vnd.fdf
Content-Disposition: attachment; filename=foo.fdf

file-content
--abcde

这在Firefox中可行;它接收空的HTML文件,触发“load”事件,然后显示可下载文件的“保存”对话框。但在Internet ExplorerSafari上失败了;Internet Explorer触发“load”事件,但不会下载文件,Safari下载文件(带有错误的名称和内容类型),并且不会触发“load”事件。
另一种方法可能是调用以开始创建文件,轮询服务器直到准备就绪,然后下载已经创建的文件。但我宁愿避免在服务器上创建临时文件。
我该怎么办?

4
IE的任何版本都不支持multipart/x-mixed-replace格式。 - EricLaw
2
这是一个简单的解决方案:http://www.bennadel.com/blog/2533-tracking-file-download-events-using-javascript-and-coldfusion.htm - Mateen
我希望浏览器制造商能够更明显地显示请求正在进行中。 - Matthew Lock
1
@mateen 谢谢兄弟!它真的很简单。 - Fai Zal Dong
如果我想使用多部分方法,那么客户端是否需要特殊处理?还是HTML部分将进入表单,附件将由浏览器下载? - Mark
显示剩余3条评论
24个回答

5
我曾经遇到过这个问题,但是我找到了一个可行的解决方案,使用了iframe(我知道,这很糟糕,但对于我遇到的简单问题来说,它起作用了)。
我有一个HTML页面,启动了一个单独的PHP脚本,生成文件,然后下载它。在HTML页面中,我在html头部使用了以下jQuery代码(你还需要包括jQuery库):
<script>
    $(function(){
        var iframe = $("<iframe>", {name: 'iframe', id: 'iframe',}).appendTo("body").hide();
        $('#click').on('click', function(){
            $('#iframe').attr('src', 'your_download_script.php');
        });
        $('iframe').load(function(){
            $('#iframe').attr('src', 'your_download_script.php?download=yes'); <!-- On first iframe load, run script again but download file instead -->
            $('#iframe').unbind(); <!-- Unbinds the iframe. Helps prevent against infinite recursion if the script returns valid html (such as echoing out exceptions) -->
        });
    });
</script>

在文件your_download_script.php中,包含以下内容:
function downloadFile($file_path) {
    if (file_exists($file_path)) {
        header('Content-Description: File Transfer');
        header('Content-Type: text/csv');
        header('Content-Disposition: attachment; filename=' . basename($file_path));
        header('Expires: 0');
        header('Cache-Control: must-revalidate');
        header('Pragma: public');
        header('Content-Length: ' . filesize($file_path));
        ob_clean();
        flush();
        readfile($file_path);
        exit();
    }
}

$_SESSION['your_file'] = path_to_file; // This is just how I chose to store the filepath

if (isset($_REQUEST['download']) && $_REQUEST['download'] == 'yes') {
    downloadFile($_SESSION['your_file']);
} else {
    // Execute logic to create the file
}

为了解释这个问题,jQuery首先在一个iframe中启动你的PHP脚本。一旦文件生成,iframe就会被加载。然后,jQuery使用一个请求变量再次启动脚本,告诉脚本下载文件。
不能一次完成下载和文件生成的原因是由于php header()函数。如果使用header(),你正在将脚本更改为其他类型而不是网页,jQuery将永远无法识别下载脚本是否已“加载”。我知道这可能不一定是检测浏览器接收文件的方法,但你的问题听起来与我的类似。

4
当用户触发文件生成时,您可以给该“下载”分配一个唯一的ID,并将用户发送到一个页面,每隔几秒钟刷新一次(或使用AJAX检查)。一旦文件生成完毕,请使用相同的唯一ID保存它,然后执行以下操作:
  • 如果文件已准备好,请进行下载。
  • 如果文件未准备好,请显示进度。
这样,您就可以跳过整个iframe /等待/浏览器窗口混乱,但仍拥有非常优雅的解决方案。

这听起来像是我之前提到的临时文件方法。如果我的想法不可行,我可能会采取类似的做法,但我希望避免这种情况。 - JW.
1
大多数情况下,无法使用Ajax下载文件,特别是如果您正在使用像Flask这样的框架。 - greendino

3

根据我的经验,处理这个问题有两种方法:

  1. 在下载时设置一个短暂的cookie,并使用JavaScript不断检查其是否存在。唯一的问题是确保cookie的生命周期正确 - 如果太短,JavaScript可能会错过它;如果太长,可能会影响其他下载屏幕的显示。通常使用JavaScript在发现后删除cookie可以解决这个问题。
  2. 使用fetch/XHR下载文件。不仅可以确切知道文件下载何时完成,如果使用XHR,则可以使用进度事件来显示进度条!在Internet ExplorerEdge中使用msSaveBlob将结果保存为blob,并在Firefox和Chrome中使用下载链接(像这样的链接)。但是,此方法的问题在于iOS Safari似乎无法正确处理下载blob-您可以使用FileReader将blob转换为数据URL并在新窗口中打开,但那只是打开文件,而不是保存它。

3

如果您不想在服务器上生成和存储文件,是否愿意存储状态,例如:文件处理中、文件已完成?您的“等待”页面可以轮询服务器以了解文件生成何时完成。您可能无法确定浏览器是否开始下载,但您会有一定的信心。


2
我刚遇到了完全相同的问题。我的解决方案是使用临时文件,因为我已经生成了一堆临时文件。表单是通过以下方式提交的:
var microBox = {
    show : function(content) {
        $(document.body).append('<div id="microBox_overlay"></div><div id="microBox_window"><div id="microBox_frame"><div id="microBox">' +
        content + '</div></div></div>');
        return $('#microBox_overlay');
    },

    close : function() {
        $('#microBox_overlay').remove();
        $('#microBox_window').remove();
    }
};

$.fn.bgForm = function(content, callback) {
    // Create an iframe as target of form submit
    var id = 'bgForm' + (new Date().getTime());
    var $iframe = $('<iframe id="' + id + '" name="' + id + '" style="display: none;" src="about:blank"></iframe>')
        .appendTo(document.body);
    var $form = this;
    // Submittal to an iframe target prevents page refresh
    $form.attr('target', id);
    // The first load event is called when about:blank is loaded
    $iframe.one('load', function() {
        // Attach listener to load events that occur after successful form submittal
        $iframe.load(function() {
            microBox.close();
            if (typeof(callback) == 'function') {
                var iframe = $iframe[0];
                var doc = iframe.contentWindow.document;
                var data = doc.body.innerHTML;
                callback(data);
            }
        });
    });

    this.submit(function() {
        microBox.show(content);
    });

    return this;
};

$('#myForm').bgForm('Please wait...');

生成文件的脚本末尾有以下内容:
header('Refresh: 0;url=fetch.php?token=' . $token);
echo '<html></html>';

这将触发 iframe 上的加载事件。然后等待消息被关闭,文件下载将开始。它已在Internet Explorer 7和Firefox上进行了测试。

2
你可以依赖浏览器的缓存,并在文件加载到缓存时触发对同一文件的第二次下载。
$('#link').click(function(e) {
    e.preventDefault();

    var url = $(this).attr('href');
    var request = new XMLHttpRequest();
    request.responseType = "blob";
    request.open("GET", url);

    var self = this;
    request.onreadystatechange = function () {
        if (request.readyState === 4) {
            var file = $(self).data('file');
            var anchor = document.createElement('a');
            anchor.download = file;
            console.log(file);
            console.log(request);
            anchor.href = window.URL.createObjectURL(request.response);
            anchor.click();
            console.log('Completed. Download window popped up.');
        }
    };
    request.send();
});

1
如果您已经下载了一个文件并将其保存,而不是放在文档中,那么由于它不在当前文档的范围内,而是在浏览器中的一个单独进程中,因此无法确定下载何时完成。

11
我应该澄清一下 -- 我并不太关心下载何时会完成。如果我能确定下载开始的时间,那就足够了。 - JW.
你需要按照 OP 说的阅读标题 - greendino

0

PrimeFaces 也使用了 cookie 轮询。

monitorDownload()

    monitorDownload: function(start, complete, monitorKey) {
        if(this.cookiesEnabled()) {
            if(start) {
                start();
            }

            var cookieName = monitorKey ? 'primefaces.download_' + monitorKey : 'primefaces.download';
            window.downloadMonitor = setInterval(function() {
                var downloadComplete = PrimeFaces.getCookie(cookieName);

                if(downloadComplete === 'true') {
                    if(complete) {
                        complete();
                    }
                    clearInterval(window.downloadMonitor);
                    PrimeFaces.setCookie(cookieName, null);
                }
            }, 1000);
        }
    },

0
问题是在生成文件时有一个“等待”指示器,一旦文件下载完成就返回正常状态。我喜欢使用隐藏的iFrame并挂钩框架的onload事件来让我的页面知道何时开始下载。
但是,在Internet Explorer中,像附件头令牌一样的文件下载不会触发onload。轮询服务器可以工作,但我不喜欢额外的复杂性。所以这是我做的:
- 像往常一样定位隐藏的iFrame。 - 生成内容。将其缓存并设置绝对超时为2分钟。 - 发送JavaScript重定向回调用客户端,实质上是第二次调用生成器页面。注意:这将使Internet Explorer中的onload事件触发,因为它的行为类似于常规页面。 - 从缓存中删除内容并将其发送给客户端。
免责声明:不要在繁忙的网站上执行此操作,因为缓存可能会增加。但是,如果您的网站非常繁忙,长时间运行的过程将使您无法获得线程。

这里是code-behind的样式,这已经足够了。

public partial class Download : System.Web.UI.Page
{
    protected System.Web.UI.HtmlControls.HtmlControl Body;

    protected void Page_Load( object sender, EventArgs e )
    {
        byte[ ] data;
        string reportKey = Session.SessionID + "_Report";

        // Check is this page request to generate the content
        //    or return the content (data query string defined)
        if ( Request.QueryString[ "data" ] != null )
        {
            // Get the data and remove the cache
            data = Cache[ reportKey ] as byte[ ];
            Cache.Remove( reportKey );

            if ( data == null )
                // send the user some information
                Response.Write( "Javascript to tell user there was a problem." );
            else
            {
                Response.CacheControl = "no-cache";
                Response.AppendHeader( "Pragma", "no-cache" );
                Response.Buffer = true;

                Response.AppendHeader( "content-disposition", "attachment; filename=Report.pdf" );
                Response.AppendHeader( "content-size", data.Length.ToString( ) );
                Response.BinaryWrite( data );
            }
            Response.End();
        }
        else
        {
            // Generate the data here. I am loading a file just for an example
            using ( System.IO.FileStream stream = new System.IO.FileStream( @"C:\1.pdf", System.IO.FileMode.Open ) )
                using ( System.IO.BinaryReader reader = new System.IO.BinaryReader( stream ) )
                {
                    data = new byte[ reader.BaseStream.Length ];
                    reader.Read( data, 0, data.Length );
                }

            // Store the content for retrieval
            Cache.Insert( reportKey, data, null, DateTime.Now.AddMinutes( 5 ), TimeSpan.Zero );

            // This is the key bit that tells the frame to reload this page
            //   and start downloading the content. NOTE: Url has a query string
            //   value, so that the content isn't generated again.
            Body.Attributes.Add("onload", "window.location = 'binary.aspx?data=t'");
        }
    }

0
我已经更新了以下参考代码。请添加一个正确的下载链接并试用。

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <style type="text/css">
            body {
                padding: 0;
                margin: 0;
            }

            svg:not(:root) {
                display: block;
            }

            .playable-code {
                background-color: #F4F7F8;
                border: none;
                border-left: 6px solid #558ABB;
                border-width: medium medium medium 6px;
                color: #4D4E53;
                height: 100px;
                width: 90%;
                padding: 10px 10px 0;
            }

            .playable-canvas {
                border: 1px solid #4D4E53;
                border-radius: 2px;
            }

            .playable-buttons {
                text-align: right;
                width: 90%;
                padding: 5px 10px 5px 26px;
            }
        </style>

        <style type="text/css">
            .event-log {
                width: 25rem;
                height: 4rem;
                border: 1px solid black;
                margin: .5rem;
                padding: .2rem;
            }

            input {
                width: 11rem;
                margin: .5rem;
            }

        </style>

        <title>XMLHttpRequest: progress event - Live_example - code sample</title>
    </head>

    <body>
        <div class="controls">
            <input class="xhr success" type="button" name="xhr" value="Click to start XHR (success)" />
            <input class="xhr error" type="button" name="xhr" value="Click to start XHR (error)" />
            <input class="xhr abort" type="button" name="xhr" value="Click to start XHR (abort)" />
        </div>

        <textarea readonly class="event-log"></textarea>

        <script>
            const xhrButtonSuccess = document.querySelector('.xhr.success');
            const xhrButtonError = document.querySelector('.xhr.error');
            const xhrButtonAbort = document.querySelector('.xhr.abort');
            const log = document.querySelector('.event-log');

            function handleEvent(e) {
                if (e.type == 'progress')
                {
                    log.textContent = log.textContent + `${e.type}: ${e.loaded} bytes transferred Received ${event.loaded} of ${event.total}\n`;
                }
                else if (e.type == 'loadstart')
                {
                    log.textContent = log.textContent + `${e.type}: started\n`;
                }
                else if  (e.type == 'error')
                {
                    log.textContent = log.textContent + `${e.type}: error\n`;
                }
                else if (e.type == 'loadend')
                {
                    log.textContent = log.textContent + `${e.type}: completed\n`;
                }
            }

            function addListeners(xhr) {
                xhr.addEventListener('loadstart', handleEvent);
                xhr.addEventListener('load', handleEvent);
                xhr.addEventListener('loadend', handleEvent);
                xhr.addEventListener('progress', handleEvent);
                xhr.addEventListener('error', handleEvent);
                xhr.addEventListener('abort', handleEvent);
            }

            function runXHR(url) {
                log.textContent = '';

                const xhr = new XMLHttpRequest();

                var request = new XMLHttpRequest();
                addListeners(request);
                request.open('GET', url, true);
                request.responseType = 'blob';
                request.onload = function (e) {
                    var data = request.response;
                    var blobUrl = window.URL.createObjectURL(data);
                    var downloadLink = document.createElement('a');
                    downloadLink.href = blobUrl;
                    downloadLink.download = 'download.zip';
                    downloadLink.click();
                };
                request.send();
                return request
            }

            xhrButtonSuccess.addEventListener('click', () => {
                runXHR('https://abbbbbc.com/download.zip');
            });

            xhrButtonError.addEventListener('click', () => {
                runXHR('http://i-dont-exist');
            });

            xhrButtonAbort.addEventListener('click', () => {
                runXHR('https://raw.githubusercontent.com/mdn/content/main/files/en-us/_wikihistory.json').abort();
            });
        </script>

    </body>
</html>

Return to post

参考:XMLHttpRequest:进度事件,实时示例


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接