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

我请求一个隐藏的表单,它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;
}

其他回答

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

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上进行了测试。

如果你不想在服务器上生成和存储文件,你是否愿意存储状态,例如文件进行中,文件完成?您的“等待”页面可以轮询服务器以了解文件生成何时完成。您不能确定是浏览器启动了下载,但您有一定的信心。

这个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;
}

这个解决方案非常简单,但很可靠。并且它可以显示真实的进度消息(并且可以轻松地插入到现有流程中):

处理的脚本(我的问题是:通过HTTP检索文件并将其作为ZIP传递)将状态写入会话。

该状态每秒轮询一次。这就是全部(好吧,这不是。您必须注意很多细节(例如,并发下载),但这是一个开始的好地方;-))。

下载页面:

<a href="download.php?id=1" class="download">DOWNLOAD 1</a>
<a href="download.php?id=2" class="download">DOWNLOAD 2</a>

...

<div id="wait">
    Please wait...
    <div id="statusmessage"></div>
</div>

<script>

    // This is jQuery
    $('a.download').each(function()
    {
        $(this).click(
            function() {
                $('#statusmessage').html('prepare loading...');
                $('#wait').show();
                setTimeout('getstatus()', 1000);
            }
            );
        });
    });

    function getstatus() {
        $.ajax({
            url: "/getstatus.php",
            type: "POST",
            dataType: 'json',
            success: function(data) {
                $('#statusmessage').html(data.message);
                if(data.status == "pending")
                    setTimeout('getstatus()', 1000);
                else
                    $('#wait').hide();
                }
        });
    }
</script>

文件getstatus.php

<?php
    session_start();
    echo json_encode($_SESSION['downloadstatus']);
?>

文件download.php

<?php
    session_start();
    $processing = true;
    while($processing) {
        $_SESSION['downloadstatus'] = array("status" =>"pending", "message" => "Processing".$someinfo);
        session_write_close();
        $processing = do_what_has_2Bdone();
        session_start();
    }

    $_SESSION['downloadstatus'] = array("status" => "finished", "message" => "Done");
    // And spit the generated file to the browser
?>

您可以依赖浏览器的缓存,并在文件加载到缓存时触发同一文件的第二次下载。

$('#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();
});