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

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个回答

0

如果您只想显示一条消息或载入动画,直到下载对话框显示出来,一个快速的解决方案是将消息放在隐藏容器中,当您单击生成要下载的文件的按钮时,使容器可见。

然后使用jQuery或JavaScript捕获按钮的 focusout 事件以隐藏包含消息的容器。


0
如果使用带有Blob的XMLHttpRequest不可行,则可以在新窗口中打开文件,并通过间隔检查该窗口主体中是否填充任何元素。

var form = document.getElementById("frmDownlaod");
form.setAttribute("action", "downoad/url");
form.setAttribute("target", "downlaod");
var exportwindow = window.open("", "downlaod", "width=800,height=600,resizable=yes");
form.submit();

var responseInterval = setInterval(function() {
    var winBody = exportwindow.document.body
    if(winBody.hasChildNodes()) // Or 'downoad/url' === exportwindow.document.location.href
    {
        clearInterval(responseInterval);
        // Do your work.
        // If there is an error page configured in your application
        // for failed requests, check for those DOM elements.
    }
}, 1000)
// Better if you specify the maximum number of intervals


0

这个Java/Spring示例可以检测下载结束,并在此时隐藏“加载中...”指示器。

方法:在JavaScript端,设置一个最大过期时间为2分钟的cookie,并每秒轮询cookie过期。然后服务器端使用一个更早的过期时间覆盖此cookie -- 即服务器进程完成。一旦在JavaScript轮询中检测到cookie过期,就会隐藏“加载中...”。

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

Cookie是由JS脚本创建的,但它并没有被控制器更新,它保持着原始值(0),我该如何在不刷新页面的情况下更新Cookie的值? - Shessuky
很奇怪 - 你能确保名称是完全正确的吗?如果名称匹配,它将覆盖cookie。让我知道。 - gene b.
原始值不为0。在JS中设置的原始值为2分钟。服务器应该修改的新值为0。 - gene b.
还有,你正在执行这个操作吗: myCookie.setPath("/"); response.addCookie(myCookie); - gene b.
我发现(出于某种原因),在执行 **response.getOutputStream();**(获取响应输出流以追加下载文件)之前,我应该添加 cookies,如果在此步骤之后再添加,则不会被考虑。 - Shessuky

-2
当单击按钮或链接时,创建一个iframe,并将其附加到网页主体中。
$('<iframe />')
    .attr('src', url)
    .attr('id', 'iframe_download_report')
    .hide()
    .appendTo('body');

创建一个带有延迟的 iframe,并在下载后将其删除。
var triggerDelay =   100;
var cleaningDelay =  20000;
var that = this;
setTimeout(function() {
    var frame = $('<iframe style="width:1px; height:1px;" class="multi-download-frame"></iframe>');
    frame.attr('src', url + "?" + "Content-Disposition: attachment ; filename=" + that.model.get('fileName'));
    $(ev.target).after(frame);
    setTimeout(function() {
        frame.remove();
    }, cleaningDelay);
}, triggerDelay);

1
这里缺少信息,而且它并没有解决“何时隐藏加载”问题。 - Tom Roggero

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