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

IT技术 javascript http mime
2021-01-15 21:32:21

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

我正在请求一个隐藏的表单,该表单向服务器发送 POST 请求,并针对其结果定位一个隐藏的 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"下载文件对话框。但它在 IE 和 Safari 上失败;IE 触发“加载”事件但不下载文件和Safari downloads文件(名称和内容类型错误)并且不触发"load"事件。

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

有没有人有更好的主意?

6个回答

一种可能的解决方案是在客户端使用 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
    );
}
很棒的主意,我用它作为这个关于使用 jQuery/C# 下载多个文件的答案的基本框架
2021-03-19 21:32:21
正如其他人指出的那样,该解决方案仅解决了部分问题,即等待服务器准备文件的时间。问题的另一部分(取决于文件的大小和连接速度)是在客户端上实际获取整个文件所需的时间。而这个解决方案并没有解决这个问题。
2021-03-25 21:32:21
@bullltorious 在深入了解您的解决方案之前,我想知道它是否适用于跨域文件下载请求。您认为它会,还是 cookie 限制会损害它?
2021-03-27 21:32:21
太棒了 - 我在 100 年内都不会想到您可以将 cookie 作为文件下载的一部分。谢谢!!
2021-03-29 21:32:21
提醒其他人:如果 document.cookies 不包含 downloadToken,请检查 cookie 路径。就我而言,即使路径默认为空白,我也必须在服务器端将路径设置为“/”(例如 Java 中的 cookie.setPath("/"))。有一段时间我认为问题是特殊的“本地主机”域 cookie 处理(stackoverflow.com/questions/1134290/...),但最终这不是问题。对于其他人来说可能是这样,尽管如此值得一读。
2021-04-04 21:32:21

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

@Lucky 这只是默认情况下。Chrome 用户完全有可能指定应保存下载的位置,从而查看对话框
2021-03-12 21:32:21
这是一种简单的方法,非常适合摆脱使用onbeforeunload谢谢触发的文件下载的加载覆盖
2021-03-16 21:32:21
坏主意,因为您激活了 tabchange 上的模糊,或窗口外的任何操作
2021-03-17 21:32:21
Chrome 和其他此类浏览器会自动下载此情况将失败的文件。
2021-03-22 21:32:21
这不适用于所有浏览器(有些浏览器不会在下载工作流程中保留/模糊当前窗口,例如 Safari、某些 IE 版本等)。
2021-04-04 21:32:21

旧线程,我知道...

但是那些由谷歌领导的人可能对我的解决方案感兴趣。它非常简单,但可靠。它可以显示真实的进度消息(并且可以轻松插入现有流程):

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

获取状态文件

<?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():当头已经发送时无法启动会话
2021-03-14 21:32:21
但是如果用户打开了多个窗口或下载?你也会在这里得到一个对服务器的冗余调用
2021-03-20 21:32:21
您不需要.each()用于事件注册。说啊$('a.download').click()
2021-03-20 21:32:21
不要 eval 里面的代码setTimeout('getstatus()', 1000);直接使用 fn:setTimeout(getstatus, 1000);
2021-03-22 21:32:21
如果您有来自一个用户的多个连接,它们都将等待其他连接结束,因为 session_start() 会锁定用户的会话并阻止所有其他进程访问它。
2021-04-08 21:32:21

根据 Elmer 的示例,我准备了自己的解决方案。在使用定义的下载单击元素后,它可以在屏幕上显示自定义消息。我使用焦点触发器来隐藏消息。

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

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

如果用户点击窗口怎么办?
2021-03-22 21:32:21
这正是我一直在寻找的,非常感谢!!
2021-03-25 21:32:21
伟大的。为我工作,下载只有几行代码的 pdf
2021-03-27 21:32:21
我的案例正在处理 JSP,点击下载 csv。有用。谢谢。
2021-03-31 21:32:21
在我的情况下没有调用 hide()
2021-04-07 21:32:21

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

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

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

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

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

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

优秀的回答伙伴,感谢您清楚地列出每个解决方案的所有缺点,非常好。
2021-04-07 21:32:21