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

JW.*_*JW. 464 javascript mime http

我有一个页面,允许用户下载动态生成的文件.生成需要很长时间,所以我想显示一个"等待"指标.问题是,我无法弄清楚如何检测浏览器何时收到文件,所以我可以隐藏指标.

我正在以隐藏的形式发出请求,该请求POST到服务器,并针对其结果定位隐藏的iframe.这样我就不会用结果替换整个浏览器窗口.我在iframe上监听"加载"事件,希望在下载完成后它会触发.

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

我尝试过的一种方法是使用多部分响应.因此它会发送一个空的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
Run Code Online (Sandbox Code Playgroud)

这适用于Firefox; 它接收空的HTML文件,触发"load"事件,然后显示可下载文件的"Save"对话框.但它在IE和Safari上失败了; IE触发"加载"事件但不下载文件,Safari下载文件(名称和内容类型错误),并且不会触发"加载"事件.

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

有没有人有更好的主意?

bul*_*ous 432

一种可能的解决方案是在客户端使用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;
}
Run Code Online (Sandbox Code Playgroud)

示例服务器代码(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();
Run Code Online (Sandbox Code Playgroud)

哪里:

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
    );
}
Run Code Online (Sandbox Code Playgroud)

  • 为其他人提醒:如果document.cookies不包含downloadToken,请检查cookie路径.在我的情况下,我必须在服务器端设置路径为"/"(例如Java中的cookie.setPath("/")),即使路径默认为空.有一段时间我认为问题是特殊的'localhost'域cookie处理(http://stackoverflow.com/questions/1134290/cookies-on-localhost-with-explicit-domain)但这不是问题所在.结束.可能对其他人来说虽然如此值得一读. (6认同)
  • 很棒 - 在100年内,我不会想到你可以将cookie作为文件下载的一部分.谢谢!! (5认同)
  • 正如其他人所指出的,这个解决方案只解决了部分问题,等待服务器准备文件时间.问题的另一部分,取决于文件的大小和连接速度,可能是相当长的,是实际获取客户端上的整个文件所需的时间.这个解决方案并没有解决这个问题. (5认同)
  • 很棒的想法,我用它作为基本框架[关于这个答案](http://stackoverflow.com/a/9049986/585552)关于使用jQuery/C#下载多个文件 (4认同)
  • @bulltorious在深入研究解决方案之前,我想知道它是否适用于跨域文件下载请求.你认为它会,或者cookie限制会妥协吗? (2认同)

小智 27

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

  • 这在所有浏览器中都不起作用(有些不会将当前窗口留下/模糊,作为下载工作流程的一部分,例如Safari,某些IE版本等). (5认同)
  • Chrome和其他此类浏览器会自动下载文件,这种情况将失败。 (4认同)
  • 不好的想法,因为你激活tabchange上的模糊,或窗口外的任何动作 (2认同)

byt*_*ate 13

老线程,我知道......

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

处理的脚本(我的问题是:通过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>
Run Code Online (Sandbox Code Playgroud)

getstatus.php

<?php
session_start();
echo json_encode($_SESSION['downloadstatus']);
?>
Run Code Online (Sandbox Code Playgroud)

的download.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
    ?>
Run Code Online (Sandbox Code Playgroud)

  • 但如果用户有多个窗口或下载打开?你也可以在这里拨打服务器的冗余电话 (3认同)
  • 如果您有一个用户的多个连接,则它们将全部等待其他连接终止,因为session_start()会为用户锁定会话并阻止所有其他进程访问它。 (2认同)
  • 您不需要使用`.each()`进行事件注册。只是说`$('a.download')。click()` (2认同)

Elm*_*mer 11

我使用以下内容下载blob并在下载后撤消object-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;
}
Run Code Online (Sandbox Code Playgroud)

关闭文件下载对话框后,窗口会将焦点恢复,以便触发焦点事件.

  • 像Chrome这样下载到底部托盘的浏览器永远不会模糊/重新聚焦窗口. (8认同)

Cub*_*oft 11

核心问题是 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等。这实际上取决于找到支持流式传输内容的库。当请求一开始就流式传输响应时,检测下载的开始并不重要,因为它几乎会立即开始。

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

  • 优秀的答案伙伴,感谢您清楚地列出每个解决方案的所有缺点,非常好。 (2认同)

Man*_*ota 10

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

/**
 *  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');
            a.remove(); // remove element
        })
        .catch(() => alert('An error sorry'));
}
Run Code Online (Sandbox Code Playgroud)

你可以使用它:

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

Run Code Online (Sandbox Code Playgroud)


Jer*_*ler 8

基于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();
}
Run Code Online (Sandbox Code Playgroud)

的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>
Run Code Online (Sandbox Code Playgroud)

现在,您应该实现任何要下载的元素:

<a class="download" href="file://www.ocelot.com.pl/prepare-report">Download report</a>
Run Code Online (Sandbox Code Playgroud)

要么

<input class="download" type="submit" value="Download" name="actionType">
Run Code Online (Sandbox Code Playgroud)

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

  • 如果用户单击窗口怎么办? (2认同)

小智 8

我写了一个简单的JavaScript类,它实现了一种类似于在背叛答案中描述的技术.我希望这对某人有用.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>
Run Code Online (Sandbox Code Playgroud)

客户端(纯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
Run Code Online (Sandbox Code Playgroud)

客户端(JQuery)

$('.my_anchors').ResponseMonitor();
$('#my_form').ResponseMonitor({timeout: 20});
Run Code Online (Sandbox Code Playgroud)

带回调的客户端(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);
Run Code Online (Sandbox Code Playgroud)

服务器(PHP)

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

//this value is passed to the client through the ResponseMonitor.onResponse callback
$cookieValue = 1; //for ex, "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
Run Code Online (Sandbox Code Playgroud)

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


小智 7

“如何检测浏览器何时接收文件下载?”
我在那个配置中遇到了同样的问题:
struts 1.2.9
jquery-1.3.2。
jquery-ui-1.7.1.custom
IE 11
java 5


我的 cookie 解决方案:
- 客户端:
提交表单时,调用 javascript 函数来隐藏页面并加载等待的微调器

function loadWaitingSpinner(){
... hide your page and show your spinner ...
}
Run Code Online (Sandbox Code Playgroud)

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

function checkCookie(){
    var verif = setInterval(isWaitingCookie,500,verif);
}
Run Code Online (Sandbox Code Playgroud)

如果找到 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 ...
}
Run Code Online (Sandbox Code Playgroud)

- 服务器端:
在服务器进程结束时,将 cookie 添加到响应中。当您的文件可供下载时,该 cookie 将发送到客户端。

Cookie waitCookie = new Cookie("waitingCookie", "done");
response.addCookie(waitCookie);
Run Code Online (Sandbox Code Playgroud)

我希望能帮助某人!


Wal*_*Boh 5

我参加聚会已经很晚了,但如果其他人想知道我的解决方案,我会把它放在这里:

我对这个确切的问题进行了真正的斗争,但我找到了一个使用 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>
Run Code Online (Sandbox Code Playgroud)

在 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*
}
Run Code Online (Sandbox Code Playgroud)

为了解决这个问题,jquery 首先在 iframe 中启动您的 php 脚本。一旦文件生成,就会加载 iframe。然后 jquery 再次使用请求变量启动脚本,告诉脚本下载文件。

无法一次性完成下载和文件生成的原因是 php header() 函数。如果您使用 header(),您将脚本更改为网页以外的内容,并且 jquery 永远不会将下载脚本识别为“已加载”。我知道这可能不一定检测浏览器何时收到文件,但您的问题听起来与我的相似。


Art*_*gel 5

如果您正在流式传输要动态生成的文件,并且已实现了实时的服务器到客户端消息传递库,则可以非常轻松地向客户端发出警报。

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

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

我知道大多数阅读此问题答案的人可能没有这种类型的设置,但是我在自己的项目中使用了这种精确的解决方案来产生了很大的效果,并且效果很好。

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


归档时间:

查看次数:

283721 次

最近记录:

6 年,1 月 前