【问题标题】:检测浏览器何时收到文件下载
【发布时间】:2010-11-09 12:48:51
【问题描述】:

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

我正在向服务器请求一个隐藏的表单,POSTs,并针对其结果定位一个隐藏的 iframe。这是,所以我不会用结果替换整个浏览器窗口。我在 iframe 上侦听“加载”事件,希望下载完成后它会触发。

我在文件中返回一个“Content-Disposition: attachment”标头,这会导致浏览器显示“保存”对话框。但浏览器不会在 iframe 中触发“加载”事件。

我尝试的一种方法是使用multi-part 响应。所以它会发送一个空的 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" 对话框。但它在Internet ExplorerSafari 上失败; Internet Explorer 触发“加载”事件,但不下载文件,Safari 下载文件(名称和内容类型错误)并且不触发 "加载” 事件。

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

我该怎么办?

【问题讨论】:

  • 没有IE版本支持multipart/x-mixed-replace。
  • 谢谢埃里克——很高兴知道这一点。我不会再用这种方法浪费时间了。
  • 唯一可靠的方法似乎是服务器推送通知(对于 ASP.NET 人员的 SignalR)。
  • bennadel.com/blog/… -- 这是一个简单的解决方案
  • @mateen 谢谢老兄!真的很简单

标签: javascript http mime


【解决方案1】:

一个possible solution 在客户端使用JavaScript。

客户端算法:

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

服务器算法:

  1. 在请求中查找 GET/POST 字段。
  2. 如果它具有非空值,则删除一个 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
    );
}

【讨论】:

  • 好主意,我用它作为基本框架for this answer 用jQuery/C#下载多个文件
  • 提醒他人:如果 document.cookies 不包含 downloadToken,请检查 cookie 路径。就我而言,我必须在服务器端将路径设置为“/”(例如 Java 中的 cookie.setPath("/")),即使路径默认为空白。有一段时间我认为问题是特殊的“本地主机”域 cookie 处理 (stackoverflow.com/questions/1134290/…),但最终这不是问题。对其他人来说可能是这样,尽管值得一读。
  • @bulltorious 在深入了解您的解决方案之前,我想知道它是否适用于跨域文件下载请求。你认为它会,还是 cookie 限制会损害它?
  • 太棒了 - 100 年后我都不会想到您可以将 cookie 作为文件下载的一部分包含在内。谢谢!!
  • 正如其他人所指出的,这个解决方案只解决了部分问题,等待服务器准备文件的时间。取决于文件大小和连接速度的问题的另一部分是在客户端上实际获取整个文件需要多长时间。这个解决方案没有解决这个问题。
【解决方案2】:

一个非常简单(且蹩脚)的单行解决方案是使用window.onblur() 事件来关闭加载对话框。当然,如果时间过长而用户决定做其他事情(例如阅读电子邮件),加载对话框将关闭。

【讨论】:

  • 这是一种简单的方法,非常适合摆脱使用 onbeforeunload 触发的文件下载的加载覆盖。谢谢。
  • 这不适用于所有浏览器(有些浏览器不会在下载工作流程中离开/模糊当前窗口,例如 Safari、某些 IE 版本等)。
  • Chrome 和其他此类浏览器会自动下载这种情况会失败的文件。
  • @Lucky 仅默认设置。 Chrome 的用户完全有可能指定下载的保存位置,因此会看到对话框
  • 坏主意,因为您激活了 tabchange 上的模糊,或窗口外的任何操作
【解决方案3】:

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

处理脚本(我的问题是:通过 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']);
?>

文件下载.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
?>

【讨论】:

  • 但如果用户有多个窗口或下载打开?您还可以在这里获得对服务器的冗余调用
  • 如果您有来自一个用户的多个连接,他们都将等待其他连接结束,因为 session_start() 会锁定用户的会话并阻止所有其他进程访问它。
  • 您无需使用.each() 进行活动注册。就说$('a.download').click()
  • 不要评估 setTimeout('getstatus()', 1000); 内的代码。直接使用fn:setTimeout(getstatus, 1000);
  • session_start(): 标头已发送时无法启动会话
【解决方案4】:

核心问题是网络浏览器没有在页面导航被取消时触发的事件,但确实有在页面完成加载时触发的事件。直接浏览器事件之外的任何事情都是有利有弊的。

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

  1. 调用 fetch(),检索整个响应,附加一个带有download 属性的a 标记,并触发点击事件。然后,现代网络浏览器将为用户提供保存已检索文件的选项。这种方法有几个缺点:
  • 整个数据 blob 都存储在 RAM 中,因此如果文件很大,它将消耗那么多 RAM。对于小文件,这可能不会破坏交易。
  • 用户必须等待整个文件下载后才能保存。在页面完成之前,他们也不能离开页面。
  • 未使用内置的网络浏览器文件下载器。
  • 除非设置了 CORS 标头,否则跨域提取可能会失败。
  1. 使用 iframe + 服务器端 cookie。如果页面在 iframe 中加载而不是开始下载,则 iframe 会触发 load 事件,但如果下载开始,它不会触发任何事件。然后,JavaScript 可以循环检测使用 Web 服务器设置 cookie。这种方法有几个缺点:
  • 服务器和客户端必须协同工作。服务器必须设置一个cookie。客户端必须检测 cookie。
  • 跨域请求将无法设置 cookie。
  • 每个域可以设置的 cookie 数量存在限制。
  • 无法发送自定义 HTTP 标头。
  1. 使用带有 URL 重定向的 iframe。 iframe 启动请求,一旦服务器准备好文件,它会将执行元刷新的 HTML 文档转储到新 URL,这会在 1 秒后触发下载。 iframe 上的 load 事件在 HTML 文档加载时发生。这种方法有几个缺点:
  • 服务器必须为正在下载的内容维护存储。需要 cron 作业或类似作业才能定期清理目录。
  • 当文件准备好时,服务器必须转储出特殊的 HTML 内容。
  • 在从 DOM 中删除 iframe 之前,客户端必须猜测 iframe 何时实际向服务器发出第二个请求,以及何时实际开始下载。这可以通过将 iframe 留在 DOM 中来克服。
  • 无法发送自定义 HTTP 标头。
  1. 使用 iframe + XHR。 iframe 触发下载请求。一旦通过 iframe 发出请求,就会通过 XHR 发出相同的请求。如果 iframe 上的 load 事件触发,则发生错误,中止 XHR 请求,并删除 iframe。如果 XHR progress 事件触发,则下载可能已在 iframe 中开始,中止 XHR 请求,等待几秒钟,然后删除 iframe。这允许在不依赖服务器端 cookie 的情况下下载更大的文件。这种方法有几个缺点:
  • 有两个单独的请求针对相同的信息。服务器可以通过检查传入的标头来区分 XHR 和 iframe。
  • 除非设置了CORS 标头,否则跨域XHR 请求可能会失败。但是,在服务器发回 HTTP 标头之前,浏览器不会知道是否允许使用 CORS。如果服务器在文件数据准备好之前等待发送标头,即使没有 CORS,XHR 也可以粗略地检测到 iframe 何时开始下载。
  • 客户端必须猜测下载实际上何时开始从DOM 中删除 iframe。这可以通过将 iframe 留在 DOM 中来克服。
  • 无法在 iframe 上发送自定义标头。

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

尽可能将响应流式传输到客户端,而不是先在服务器上生成所有内容,然后再发送响应。可以流式传输各种文件格式,例如 CSV、JSON、XML、ZIP 等。这实际上取决于找到支持流式传输内容的库。当请求一开始就流式传输响应时,检测下载的开始并不重要,因为它几乎会立即开始。

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

【讨论】:

  • 优秀的答案伙伴,感谢您清楚地列出每个解决方案的所有缺点,非常好。
【解决方案5】:

基于Elmer's example,我准备了自己的解决方案。在元素单击定义的 download 类后,它可以在屏幕上显示自定义消息。我使用了 focus 触发器来隐藏消息。

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>

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

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

【讨论】:

  • 如果用户点击窗口怎么办?
  • 这正是我想要的,非常感谢!
  • 在我的情况下没有调用 hide()
  • 我的案例在JSP上,点击下载csv。有用。谢谢。
  • 如果我从 JS 代码触发点击事件,则 hide() 部分在我的情况下不起作用。
【解决方案6】:

我使用以下内容下载 blob 并在下载后撤销对象 URL。它适用于 Chrome 和 Firefox!

function download(blob){
    var url = URL.createObjectURL(blob);
    console.log('create ' + url);

    window.addEventListener('focus', window_focus, false);
    function window_focus(){
        window.removeEventListener('focus', window_focus, false);
        URL.revokeObjectURL(url);
        console.log('revoke ' + url);
    }
    location.href = url;
}

文件下载对话框关闭后,窗口重新获得焦点,因此触发焦点事件。

【讨论】:

  • 还有切换窗口返回的问题,会导致modal隐藏。
  • 像 Chrome 这样下载到底部托盘的浏览器永远不会模糊/重新聚焦窗口。
【解决方案7】:

如果您正在流式传输您正在动态生成的文件,并且还实现了一个实时服务器到客户端消息传递库,那么您可以很容易地提醒您的客户端。

我喜欢并推荐的服务器到客户端消息传递库是 Socket.io(通过 Node.js)。在您的服务器脚本完成生成正在流式传输以供下载的文件后,您在该脚本中的最后一行可以向 Socket.io 发出一条消息,该消息向客户端发送一条通知。在客户端,Socket.io 侦听从服务器发出的传入消息,并允许您对它们采取行动。与其他方法相比,使用此方法的好处是您能够在流式传输完成后检测到“真正的”完成事件。

例如,您可以在单击下载链接后显示忙碌指示符,流式传输文件,在流式传输脚本的最后一行从服务器向 Socket.io 发送消息,在客户端侦听通知,通过隐藏忙碌指示器来接收通知并更新您的 UI。

我意识到大多数阅读这个问题的答案的人可能没有这种类型的设置,但我已经在我自己的项目中使用了这种精确的解决方案,效果很好,而且效果很好。

Socket.io 非常容易安装和使用。查看更多:http://socket.io/

【讨论】:

    【解决方案8】:

    我编写了一个简单的 JavaScript 类,它实现了一种类似于 bulltorious' answer 中描述的技术。我希望它对这里的人有用。

    GitHub 项目名为response-monitor.js

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

    支持jQuery,但不是必需的。

    显着特点

    • 简单集成
    • 无依赖关系
    • jQuery 插件(可选)
    • Spin.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});
    

    带有回调的客户端 (jQuery)

    // 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
    

    如需更多示例,请查看存储库中的the examples folder

    【讨论】:

      【解决方案9】:

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

      我的 cookie 解决方案:

      客户端:

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

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

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

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

      如果找到 cookie,则停止每 500 毫秒检查一次,使 cookie 过期并调用您的函数以返回您的页面并移除等待的微调器 (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);
      

      【讨论】:

      • 完美运行。感谢这个美丽的样本。
      【解决方案10】:

      我确实为这个确切的问题而苦苦挣扎,但我找到了一个使用 iframe 的可行解决方案(我知道,我知道。这很糟糕,但它适用于我遇到的一个简单问题。)

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

      【讨论】:

        【解决方案11】:

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

        /**
         *  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");
        
        

        【讨论】:

        • 工作,但基本上将数据转换为内存上的 Base64,然后再转换为二进制文件并下载。不推荐用于大文件
        • 如何将最终下载的文件名设置为从url获取的文件名?
        【解决方案12】:

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

        • 如果文件准备好了,请下载。
        • 如果文件尚未准备好,请显示进度。

        然后你可以跳过整个 iframe/等待/浏览器窗口的混乱,但有一个非常优雅的解决方案。

        【讨论】:

        • 这听起来像我上面提到的临时文件方法。如果事实证明我的想法是不可能的,我可能会这样做,但我希望避免它。
        【解决方案13】:

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

        【讨论】:

          【解决方案14】:

          根据我的经验,有两种方法可以解决这个问题:

          1. 在下载时设置一个短期 cookie,并让 JavaScript 不断检查它的存在。唯一真正的问题是正确设置 cookie 的生命周期 - 太短,JavaScript 可能会错过它,太长,它可能会取消其他下载的下载屏幕。在发现时使用 JavaScript 删除 cookie 通常可以解决此问题。
          2. 使用 fetch/XHR 下载文件。您不仅可以准确地知道文件下载何时完成,如果您使用 XHR,您还可以使用进度事件来显示进度条!使用Internet ExplorerEdge 中的msSaveBlob 和Firefox 和Chrome 中的下载链接(like this one) 保存生成的blob。这种方法的问题是 iOS Safari 似乎没有正确处理下载 blob - 您可以使用 FileReader 将 blob 转换为数据 URL 并在新窗口中打开它,但这是打开文件,而不是保存它。

          【讨论】:

            【解决方案15】:

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

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

            【讨论】:

              【解决方案16】:

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

              【讨论】:

              • 我应该澄清一下——我不太关心下载何时完成。如果我能确定下载何时开始,那就足够了。
              【解决方案17】:

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

              onload 在 Internet Explorer 中不会触发文件下载(如附件标头令牌)。轮询服务器有效,但我不喜欢额外的复杂性。所以这就是我所做的:

              • 照常定位隐藏的 iFrame。
              • 生成内容。缓存它 2 分钟后绝对超时。
              • 将 JavaScript 重定向发送回 调用客户端,本质上是调用 生成器页面第二次。 注意:这将导致在 Internet Explorer 中触发 onload 事件,因为它的行为类似于常规页面。
              • 从缓存中删除内容并 发送给客户。

              免责声明:请勿在繁忙的网站上执行此操作,因为缓存可能会累加。但实际上,如果您的网站如此繁忙,那么长时间运行的进程无论如何都会让您饿死。

              这就是code-behind 的样子,这就是您真正需要的。

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

              【讨论】:

                【解决方案18】:

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

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

                【讨论】:

                  【解决方案19】:

                  如果带有 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

                  【讨论】:

                    【解决方案20】:

                    这个 Java/Spring 示例检测到下载结束,此时它隐藏了“正在加载...”指示器。

                    方法:在 JavaScript 端,设置一个最长过期时间为 2 分钟的 cookie,并每秒轮询 cookie 过期。然后服务器端用 earlier 过期时间覆盖此 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值?
                    • 这很奇怪 - 您能确保名称完全正确正确吗?如果名称匹配,它将覆盖 cookie。告诉我
                    • 原值不为0。JS中设置的原值为2分钟。服务器应该修改的新值是 0。
                    • 另外,你在这样做吗:myCookie.setPath("/"); response.addCookie(myCookie);
                    • 我发现(出于某种原因),我应该在执行 response.getOutputStream(); (获取响应输出流以附加下载文件)之前添加 cookie,它是在那一步之后我做的时候没有考虑到
                    【解决方案21】:

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

                    【讨论】:

                      【解决方案22】:

                      我已更新以下参考代码。添加正确的下载 URL 链接并尝试一下。

                      <!DOCTYPE html>
                      <html>
                          <head>
                              <meta charset="utf-8">
                              <style type="text/css">
                                  body {
                                      padding: 0;
                                      margin: 0;
                                  }
                      
                                  svg:not(:root) {
                                      display: block;
                                  }
                      
                                  .playable-code {
                                      background-color: #F4F7F8;
                                      border: none;
                                      border-left: 6px solid #558ABB;
                                      border-width: medium medium medium 6px;
                                      color: #4D4E53;
                                      height: 100px;
                                      width: 90%;
                                      padding: 10px 10px 0;
                                  }
                      
                                  .playable-canvas {
                                      border: 1px solid #4D4E53;
                                      border-radius: 2px;
                                  }
                      
                                  .playable-buttons {
                                      text-align: right;
                                      width: 90%;
                                      padding: 5px 10px 5px 26px;
                                  }
                              </style>
                      
                              <style type="text/css">
                                  .event-log {
                                      width: 25rem;
                                      height: 4rem;
                                      border: 1px solid black;
                                      margin: .5rem;
                                      padding: .2rem;
                                  }
                      
                                  input {
                                      width: 11rem;
                                      margin: .5rem;
                                  }
                      
                              </style>
                      
                              <title>XMLHttpRequest: progress event - Live_example - code sample</title>
                          </head>
                      
                          <body>
                              <div class="controls">
                                  <input class="xhr success" type="button" name="xhr" value="Click to start XHR (success)" />
                                  <input class="xhr error" type="button" name="xhr" value="Click to start XHR (error)" />
                                  <input class="xhr abort" type="button" name="xhr" value="Click to start XHR (abort)" />
                              </div>
                      
                              <textarea readonly class="event-log"></textarea>
                      
                              <script>
                                  const xhrButtonSuccess = document.querySelector('.xhr.success');
                                  const xhrButtonError = document.querySelector('.xhr.error');
                                  const xhrButtonAbort = document.querySelector('.xhr.abort');
                                  const log = document.querySelector('.event-log');
                      
                                  function handleEvent(e) {
                                      if (e.type == 'progress')
                                      {
                                          log.textContent = log.textContent + `${e.type}: ${e.loaded} bytes transferred Received ${event.loaded} of ${event.total}\n`;
                                      }
                                      else if (e.type == 'loadstart')
                                      {
                                          log.textContent = log.textContent + `${e.type}: started\n`;
                                      }
                                      else if  (e.type == 'error')
                                      {
                                          log.textContent = log.textContent + `${e.type}: error\n`;
                                      }
                                      else if (e.type == 'loadend')
                                      {
                                          log.textContent = log.textContent + `${e.type}: completed\n`;
                                      }
                                  }
                      
                                  function addListeners(xhr) {
                                      xhr.addEventListener('loadstart', handleEvent);
                                      xhr.addEventListener('load', handleEvent);
                                      xhr.addEventListener('loadend', handleEvent);
                                      xhr.addEventListener('progress', handleEvent);
                                      xhr.addEventListener('error', handleEvent);
                                      xhr.addEventListener('abort', handleEvent);
                                  }
                      
                                  function runXHR(url) {
                                      log.textContent = '';
                      
                                      const xhr = new XMLHttpRequest();
                      
                                      var request = new XMLHttpRequest();
                                      addListeners(request);
                                      request.open('GET', url, true);
                                      request.responseType = 'blob';
                                      request.onload = function (e) {
                                          var data = request.response;
                                          var blobUrl = window.URL.createObjectURL(data);
                                          var downloadLink = document.createElement('a');
                                          downloadLink.href = blobUrl;
                                          downloadLink.download = 'download.zip';
                                          downloadLink.click();
                                      };
                                      request.send();
                                      return request
                                  }
                      
                                  xhrButtonSuccess.addEventListener('click', () => {
                                      runXHR('https://abbbbbc.com/download.zip');
                                  });
                      
                                  xhrButtonError.addEventListener('click', () => {
                                      runXHR('http://i-dont-exist');
                                  });
                      
                                  xhrButtonAbort.addEventListener('click', () => {
                                      runXHR('https://raw.githubusercontent.com/mdn/content/main/files/en-us/_wikihistory.json').abort();
                                  });
                              </script>
                      
                          </body>
                      </html>
                      
                      Return to post

                      参考:XMLHttpRequest: progress event, Live example

                      【讨论】:

                        【解决方案23】:

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

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

                        【讨论】:

                          【解决方案24】:

                          单击按钮/链接时创建一个 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);
                          

                          【讨论】:

                          • 这缺乏信息,并没有解决“何时隐藏加载”问题。
                          猜你喜欢
                          • 2015-06-29
                          • 2023-03-26
                          • 1970-01-01
                          • 1970-01-01
                          • 1970-01-01
                          相关资源
                          最近更新 更多