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

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

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

我该怎么办?


当前回答

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

其他回答

我在这个配置中遇到了同样的问题:

Struts 1.2.9 jQuery 1.3.2。 jQuery UI 1.7.1.custom Internet Explorer 11 Java 5

我用饼干的解决方案:

客户端:

在提交表单时,调用JavaScript函数隐藏页面并加载等待的旋转器

function loadWaitingSpinner() {
    ... hide your page and show your spinner ...
}

然后,调用一个函数,每500毫秒检查一次cookie是否来自服务器。

function checkCookie() {
    var verif = setInterval(isWaitingCookie, 500, verif);
}

如果找到cookie,停止每500毫秒检查一次,使cookie过期并调用函数返回页面并删除等待spinner (removeWaitingSpinner())。如果您希望能够再次下载另一个文件,过期cookie是很重要的!

function isWaitingCookie(verif) {
    var loadState = getCookie("waitingCookie");
    if (loadState == "done") {
        clearInterval(verif);
        document.cookie = "attenteCookie=done; expires=Tue, 31 Dec 1985 21:00:00 UTC;";
        removeWaitingSpinner();
    }
}

function getCookie(cookieName) {
    var name = cookieName + "=";
    var cookies = document.cookie
    var cs = cookies.split(';');
    for (var i = 0; i < cs.length; i++) {
        var c = cs[i];
        while(c.charAt(0) == ' ') {
            c = c.substring(1);
        }
        if (c.indexOf(name) == 0) {
            return c.substring(name.length, c.length);
        }
    }
    return "";
}

function removeWaitingSpinner() {
    ... come back to your page and remove your spinner ...
}

服务器端:

在服务器进程结束时,向响应添加一个cookie。当您的文件准备好下载时,cookie将被发送到客户端。

Cookie waitCookie = new Cookie("waitingCookie", "done");
response.addCookie(waitCookie);

如果XMLHttpRequest和一个blob不是一个选项,那么你可以在一个新窗口中打开你的文件,并检查是否有任何元素被填充到该窗口体中。

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

一种可能的解决方案是在客户机上使用JavaScript。

客户端算法:

生成一个随机的唯一令牌。 提交下载请求,并在GET/POST字段中包含令牌。 显示“等待”指示灯。 启动一个计时器,每隔一秒左右,查找一个名为“fileDownloadToken”的cookie(或任何您决定的cookie)。 如果cookie存在,并且它的值与令牌匹配,则隐藏“等待”指示器。

服务器算法:

在请求中查找GET/POST字段。 如果它有一个非空值,删除一个cookie(例如。"fileDownloadToken"),并将其值设置为令牌的值。


客户端源代码(JavaScript):

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

function expireCookie( cName ) {
    document.cookie = 
        encodeURIComponent(cName) + "=deleted; expires=" + new Date( 0 ).toUTCString();
}

function setCursor( docStyle, buttonStyle ) {
    document.getElementById( "doc" ).style.cursor = docStyle;
    document.getElementById( "button-id" ).style.cursor = buttonStyle;
}

function setFormToken() {
    var downloadToken = new Date().getTime();
    document.getElementById( "downloadToken" ).value = downloadToken;
    return downloadToken;
}

var downloadTimer;
var attempts = 30;

// Prevents double-submits by waiting for a cookie from the server.
function blockResubmit() {
    var downloadToken = setFormToken();
    setCursor( "wait", "wait" );

    downloadTimer = window.setInterval( function() {
        var token = getCookie( "downloadToken" );

        if( (token == downloadToken) || (attempts == 0) ) {
            unblockSubmit();
        }

        attempts--;
    }, 1000 );
}

function unblockSubmit() {
  setCursor( "auto", "pointer" );
  window.clearInterval( downloadTimer );
  expireCookie( "downloadToken" );
  attempts = 30;
}

服务器代码示例(PHP):

$TOKEN = "downloadToken";

// Sets a cookie so that when the download begins the browser can
// unblock the submit button (thus helping to prevent multiple clicks).
// The false parameter allows the cookie to be exposed to JavaScript.
$this->setCookieToken( $TOKEN, $_GET[ $TOKEN ], false );

$result = $this->sendFile();

地点:

public function setCookieToken(
    $cookieName, $cookieValue, $httpOnly = true, $secure = false ) {

    // See: http://stackoverflow.com/a/1459794/59087
    // See: http://shiflett.org/blog/2006/mar/server-name-versus-http-host
    // See: http://stackoverflow.com/a/3290474/59087
    setcookie(
        $cookieName,
        $cookieValue,
        2147483647,            // expires January 1, 2038
        "/",                   // your path
        $_SERVER["HTTP_HOST"], // your domain
        $secure,               // Use true over HTTPS
        $httpOnly              // Set true for $AUTH_COOKIE_NAME
    );
}

我写了一个简单的JavaScript类,实现了一种类似于bulltorious回答中描述的技术。我希望它能对在座的人有用。

GitHub项目被称为response-monitor.js。

默认情况下,它使用spin.js作为等待指示器,但它也为自定义指示器的实现提供了一组回调。

jQuery是支持的,但不是必需的。

显著的特征

简单的集成 没有依赖关系 jQuery插件(可选) js集成(可选) 用于监视事件的可配置回调 同时处理多个请求 服务器端错误检测 超时检测 跨浏览器

示例使用

HTML

<!-- The response monitor implementation -->
<script src="response-monitor.js"></script>

<!-- Optional jQuery plug-in -->
<script src="response-monitor.jquery.js"></script>

<a class="my_anchors" href="/report?criteria1=a&criteria2=b#30">Link 1 (Timeout: 30s)</a>
<a class="my_anchors" href="/report?criteria1=b&criteria2=d#10">Link 2 (Timeout: 10s)</a>

<form id="my_form" method="POST">
    <input type="text" name="criteria1">
    <input type="text" name="criteria2">
    <input type="submit" value="Download Report">
</form>

客户端(纯JavaScript)

// Registering multiple anchors at once
var my_anchors = document.getElementsByClassName('my_anchors');
ResponseMonitor.register(my_anchors); // Clicking on the links initiates monitoring

// Registering a single form
var my_form = document.getElementById('my_form');
ResponseMonitor.register(my_form); // The submit event will be intercepted and monitored

客户端(jQuery)

$('.my_anchors').ResponseMonitor();
$('#my_form').ResponseMonitor({timeout: 20});

带有回调函数的客户端

// When options are defined, the default spin.js integration is bypassed
var options = {
    onRequest: function(token) {
        $('#cookie').html(token);
        $('#outcome').html('');
        $('#duration').html('');
    },
    onMonitor: function(countdown) {
        $('#duration').html(countdown);
    },
    onResponse: function(status) {
        $('#outcome').html(status==1 ? 'success' : 'failure');
    },
    onTimeout: function() {
        $('#outcome').html('timeout');
    }
};

// Monitor all anchors in the document
$('a').ResponseMonitor(options);

服务器(PHP)

$cookiePrefix = 'response-monitor'; // Must match the one set on the client options
$tokenValue = $_GET[$cookiePrefix];
$cookieName = $cookiePrefix.'_'.$tokenValue; // Example: response-monitor_1419642741528

// This value is passed to the client through the ResponseMonitor.onResponse callback
$cookieValue = 1; // For example, "1" can interpret as success and "0" as failure

setcookie(
    $cookieName,
    $cookieValue,
    time() + 300,          // Expire in 5 minutes
    "/",
    $_SERVER["HTTP_HOST"],
    true,
    false
);

header('Content-Type: text/plain');
header("Content-Disposition: attachment; filename=\"Response.txt\"");

sleep(5); // Simulate whatever delays the response
print_r($_REQUEST); // Dump the request in the text file

有关更多示例,请检查存储库中的examples文件夹。

问题是在生成文件时有一个“等待”指示器,然后在文件下载后恢复正常。我喜欢这样做的方式是使用一个隐藏的iFrame和钩子帧的onload事件,让我的页面知道什么时候开始下载。

但是onload不会在Internet Explorer中触发文件下载(就像使用附件头令牌一样)。轮询服务器是可行的,但我不喜欢这种额外的复杂性。这就是我所做的:

目标隐藏iFrame像往常一样。 生成内容。用 2分钟内绝对超时。 发送JavaScript重定向回 调用客户端,本质上调用 第二次生成页面。注意:这将导致在Internet Explorer中触发onload事件,因为它就像一个普通页面一样。 从缓存中删除内容并 发送给客户端。

免责声明:不要在繁忙的站点上这样做,因为缓存会增加。但实际上,如果您的站点非常繁忙,那么长时间运行的进程无论如何都会耗尽您的线程。

下面是隐藏代码的样子,这是您真正需要的。

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'");
        }
    }