我有一个允许用户下载动态生成文件的页面。生成它需要很长时间,所以我想显示一个“等待”指示器。问题是,我不知道如何检测浏览器何时接收到文件,以便我可以隐藏指示器。

我请求一个隐藏的表单,它post到服务器,并以一个隐藏的iframe作为其结果的目标。这样我就不会用结果替换整个浏览器窗口。我监听iframe上的“load”事件,希望它在下载完成时触发。

我将文件返回一个“Content-Disposition: attachment”头,这将导致浏览器显示“Save”对话框。但是浏览器不会在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”事件,然后显示可下载文件的“Save”对话框。但它在ie和Safari浏览器上无法运行;Internet Explorer触发“load”事件,但它不下载文件,而Safari下载文件(带有错误的名称和内容类型),但不触发“load”事件。

另一种方法可能是调用启动文件创建,轮询服务器直到准备就绪,然后下载已经创建的文件。但我宁愿避免在服务器上创建临时文件。

我该怎么办?


当前回答

这个Java/Spring示例检测下载的结束,在这一点上它隐藏了“Loading…”指示符。

方法:在JavaScript方面,设置一个最大过期时间为2分钟的cookie,并每秒钟轮询一次cookie过期时间。然后服务器端用更早的过期时间覆盖这个cookie——服务器进程的完成。一旦在JavaScript轮询中检测到cookie过期,“Loading…”就会被隐藏。

JavaScript的一面

function buttonClick() { // Suppose this is the handler for the button that starts
    $("#loadingProgressOverlay").show();  // Show loading animation
    startDownloadChecker("loadingProgressOverlay", 120);
    // Here you launch the download URL...
    window.location.href = "myapp.com/myapp/download";
}

// This JavaScript function detects the end of a download.
// It does timed polling for a non-expired Cookie, initially set on the
// client-side with a default max age of 2 min.,
// but then overridden on the server-side with an *earlier* expiration age
// (the completion of the server operation) and sent in the response.
// Either the JavaScript timer detects the expired cookie earlier than 2 min.
// (coming from the server), or the initial JavaScript-created cookie expires after 2 min.
function startDownloadChecker(imageId, timeout) {

    var cookieName = "ServerProcessCompleteChecker";  // Name of the cookie which is set and later overridden on the server
    var downloadTimer = 0;  // Reference to the timer object

    // The cookie is initially set on the client-side with a specified default timeout age (2 min. in our application)
    // It will be overridden on the server side with a new (earlier) expiration age (the completion of the server operation),
    // or auto-expire after 2 min.
    setCookie(cookieName, 0, timeout);

    // Set a timer to check for the cookie every second
    downloadTimer = window.setInterval(function () {

        var cookie = getCookie(cookieName);

        // If cookie expired (NOTE: this is equivalent to cookie "doesn't exist"), then clear "Loading..." and stop polling
        if ((typeof cookie === 'undefined')) {
            $("#" + imageId).hide();
            window.clearInterval(downloadTimer);
        }

    }, 1000); // Every second
}

// These are helper JavaScript functions for setting and retrieving a Cookie
function setCookie(name, value, expiresInSeconds) {
    var exdate = new Date();
    exdate.setTime(exdate.getTime() + expiresInSeconds * 1000);
    var c_value = escape(value) + ((expiresInSeconds == null) ? "" : "; expires=" + exdate.toUTCString());
    document.cookie = name + "=" + c_value + '; path=/';
}

function getCookie(name) {
    var parts = document.cookie.split(name + "=");
    if (parts.length == 2 ) {
        return parts.pop().split(";").shift();
    }
}

Java/Spring服务器端

    @RequestMapping("/download")
    public String download(HttpServletRequest request, HttpServletResponse response) throws Exception {
        //... Some logic for downloading, returning a result ...

        // Create a Cookie that will override the JavaScript-created
        // Max-Age-2min Cookie with an earlier expiration (same name)
        Cookie myCookie = new Cookie("ServerProcessCompleteChecker", "-1");
        myCookie.setMaxAge(0); // This is immediate expiration, but can also
                               // add +3 seconds for any flushing concerns
        myCookie.setPath("/");
        response.addCookie(myCookie);
        //... -- presumably the download is writing to the Output Stream...
        return null;
}

其他回答

如果您下载了一个文件,该文件是保存的,而不是在文档中,则无法确定下载何时完成,因为它不在当前文档的范围内,而是浏览器中的一个单独进程。

当用户触发文件的生成时,您可以简单地为“下载”分配一个唯一的ID,并将用户发送到每隔几秒钟刷新(或使用AJAX检查)的页面。一旦文件完成,将其保存在相同的唯一ID和…

如果文件已经准备好,请进行下载。 如果文件尚未准备好,请显示进度。

然后你可以跳过整个iframe/waiting/browserwindow的混乱,而有一个真正优雅的解决方案。

核心问题是web浏览器没有在页面导航被取消时触发事件,但有一个在页面完成加载时触发的事件。任何外部的直接浏览器事件都将是一个优点和缺点的黑客。

有四种已知的方法来处理检测浏览器下载何时开始:

调用fetch(),检索整个响应,附加带有下载属性的a标记,并触发单击事件。现代网络浏览器将为用户提供保存已经检索到的文件的选项。这种方法有几个缺点:

整个数据团存储在RAM中,因此如果文件很大,它将消耗同样多的RAM。对于小文件,这可能不是问题。 用户必须等待整个文件下载完成后才能保存。他们也不能离开页面,直到它完成。 未使用内置的web浏览器文件下载器。 除非设置了CORS报头,否则跨域获取可能会失败。

使用iframe +服务器端cookie。如果页面在iframe中加载而不是开始下载,iframe会触发一个加载事件,但如果下载开始,它不会触发任何事件。在web服务器上设置cookie可以被JavaScript循环检测到。这种方法有几个缺点:

服务器和客户机必须协同工作。服务器必须设置cookie。客户端必须检测cookie。 跨域请求将无法设置cookie。 每个域可以设置多少个cookie是有限制的。 不能发送自定义HTTP报头。

使用带有URL重定向的iframe。iframe启动一个请求,一旦服务器准备好文件,它将转储一个HTML文档,该文档执行元刷新到一个新的URL,这将在1秒后触发下载。iframe上的load事件发生在HTML文档加载时。这种方法有几个缺点:

服务器必须维护所下载内容的存储。需要cron作业或类似作业来定期清理目录。 当文件准备好时,服务器必须转储特殊的HTML内容。 在从DOM中删除iframe之前,客户端必须猜测iframe何时实际向服务器发出了第二个请求,以及下载实际何时开始。这可以通过将iframe留在DOM中来解决。 不能发送自定义HTTP报头。

Use an iframe + XHR. The iframe triggers the download request. As soon as the request is made via the iframe, an identical request via XHR is made. If the load event on the iframe fires, an error has occurred, abort the XHR request, and remove the iframe. If a XHR progress event fires, then downloading has probably started in the iframe, abort the XHR request, wait a few seconds, and then remove the iframe. This allows for larger files to be downloaded without relying on a server-side cookie. There are several downsides with this approach:

There are two separate requests made for the same information. The server can distinguish the XHR from the iframe by checking the incoming headers. A cross-domain XHR request will probably fail unless CORS headers are set. However, the browser won't know if CORS is allowed or not until the server sends back the HTTP headers. If the server waits to send headers until the file data is ready, the XHR can roughly detect when the iframe has started to download even without CORS. The client has to guess as to when the download has actually started to remove the iframe from the DOM. This could be overcome by just leaving the iframe in the DOM. Can't send custom headers on the iframe.

如果没有适当的内置web浏览器事件,这里就没有任何完美的解决方案。然而,根据您的用例,上述四种方法中的一种可能比其他方法更适合。

只要可能,动态地将响应流发送到客户端,而不是先在服务器上生成所有内容,然后再发送响应。各种文件格式可以流,如CSV, JSON, XML, ZIP等。这真的取决于找到一个支持流媒体内容的库。当请求一开始就流化响应时,检测下载的开始并不重要,因为它几乎马上就开始了。

另一种选择是预先输出下载标题,而不是等待所有内容先生成。然后生成内容,最后开始发送到客户端。用户内置的下载程序将耐心等待数据到达。缺点是底层网络连接可能会超时等待数据开始流动(无论是在客户端还是服务器端)。

我也遇到过同样的问题。我的解决方案是使用临时文件,因为我已经生成了一堆临时文件。提交表格时:

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上的load事件被触发。然后关闭等待消息,然后开始文件下载。它在Internet Explorer 7和Firefox上进行了测试。

基于Elmer的例子,我准备了自己的解决方案。单击具有定义的“download”类的项目后,浏览器窗口中将显示一条自定义消息。我用焦点触发器隐藏了消息。我用焦点触发器隐藏了消息。

JavaScript

$(function(){$('.download').click(function() { ShowDownloadMessage(); }); })

function ShowDownloadMessage()
{
     $('#message-text').text('Your report is creating. Please wait...');
     $('#message').show();
     window.addEventListener('focus', HideDownloadMessage, false);
}

function HideDownloadMessage(){
    window.removeEventListener('focus', HideDownloadMessage, false);                   
    $('#message').hide();
}

HTML

<div id="message" style="display: none">
    <div id="message-screen-mask" class="ui-widget-overlay ui-front"></div>
    <div id="message-text" class="ui-dialog ui-widget ui-widget-content ui-corner-all ui-front ui-draggable ui-resizable waitmessage">please wait...</div>
</div>

现在你应该实现任何元素来下载:

<a class="download" href="file://www.ocelot.com.pl/prepare-report">Download report</a>

or

<input class="download" type="submit" value="Download" name="actionType">

每次点击下载后,您将看到消息: 您的报告正在创建中。请稍等…