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

我请求一个隐藏的表单,它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”事件。

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

我该怎么办?


当前回答

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

其他回答

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

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

我在这个问题上有一个真正的斗争,但我发现了一个使用iframes的可行解决方案。这很糟糕,但它适用于我遇到的一个简单问题。)

我有一个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将永远不会将下载脚本识别为“已加载”。我知道这可能并不一定是检测浏览器何时接收文件,但您的问题听起来与我的类似。

如果您只想在显示下载对话框之前显示消息或加载器GIF图像,那么一个快速的解决方案是将消息放在一个隐藏的容器中,当您单击生成要下载的文件的按钮时,您将使容器可见。

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

来自其他地方的有效解决方案:

/**
 *  download file, show modal
 *
 * @param uri link
 * @param name file name
 */
function downloadURI(uri, name) {
// <------------------------------------------       Do something (show loading)
    fetch(uri)
        .then(resp => resp.blob())
        .then(blob => {
            const url = window.URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.style.display = 'none';
            a.href = url;
            // the filename you want
            a.download = name;
            document.body.appendChild(a);
            a.click();
            window.URL.revokeObjectURL(url);
            // <----------------------------------------  Detect here (hide loading)
            alert('File detected');
        })
        .catch(() => alert('An error sorry'));
}

你可以使用它:

downloadURI("www.linkToFile.com", "file.name");