簡體   English   中英

檢測瀏覽器何時接收文件下載

[英]Detect when browser receives file download

我有一個頁面,允許用戶下載動態生成的文件。 生成需要很長時間,所以我想顯示一個“等待”指示器。 問題是,我不知道如何檢測瀏覽器何時收到文件,以便我可以隱藏指示器。

我正在請求一個隱藏的表單,它向服務器發送 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"事件。

另一種方法可能是調用以開始文件創建,然后輪詢服務器直到它准備好,然后下載已經創建的文件。 但我寧願避免在服務器上創建臨時文件。

有沒有人有更好的主意?

一種可能的解決方案是在客戶端使用 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
    );
}

一個非常簡單(和蹩腳)的單行解決方案是使用window.onblur()事件關閉加載對話框。 當然,如果時間太長並且用戶決定做其他事情(例如閱讀電子郵件),加載對話框將關閉。

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

每次下載點擊后,您都會看到正在創建報告的消息,請稍候...

舊線程,我知道...

但是那些由谷歌領導的人可能對我的解決方案感興趣。 它非常簡單,但可靠。 它可以顯示真實的進度消息(並且可以輕松插入現有流程):

處理的腳本(我的問題是:通過 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
    ?>

核心問題是 Web 瀏覽器沒有在取消頁面導航時觸發的事件,但確實有在頁面完成加載時觸發的事件。 直接瀏覽器事件之外的任何事情都將有利有弊。

有四種已知的方法來處理檢測瀏覽器下載何時開始:

  1. 調用 fetch(),檢索整個響應,附加帶有download屬性的a標簽,並觸發點擊事件。 現代 Web 瀏覽器將為用戶提供保存已檢索文件的選項。 這種方法有幾個缺點:
  • 整個數據 blob 存儲在 RAM 中,因此如果文件很大,它將消耗那么多 RAM。 對於小文件,這可能不是一個交易破壞者。
  • 用戶必須等待整個文件下載后才能保存。 在頁面完成之前,他們也不能離開頁面。
  • 不使用內置的 Web 瀏覽器文件下載器。
  • 除非設置了 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 上發送自定義標頭。

如果沒有合適的內置 Web 瀏覽器事件,這里沒有任何完美的解決方案。 但是,根據您的用例,上述四種方法中的一種可能比其他方法更適合。

只要有可能,就立即將響應流式傳輸到客戶端,而不是先在服務器上生成所有內容,然后再發送響應。 可以流式傳輸各種文件格式,例如 CSV、JSON、XML、 ZIP等。這實際上取決於找到支持流式傳輸內容的庫。 當請求一開始就流式傳輸響應時,檢測下載的開始並不重要,因為它幾乎會立即開始。

另一種選擇是預先輸出下載標頭,而不是等待所有內容首先生成。 然后生成內容,最后開始發送給客戶端。 用戶的內置下載器將耐心等待數據開始到達。 缺點是底層網絡連接可能會超時等待數據開始流動(在客戶端或服務器端)。

我使用以下內容下載 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;
}

文件下載對話框關閉后,窗口會重新獲得焦點,從而觸發焦點事件。

我編寫了一個簡單的 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; //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

有關更多示例,請查看存儲庫上的示例文件夾。

如果您正在流式傳輸動態生成的文件,並且還實現了實時服務器到客戶端消息傳遞庫,則可以非常輕松地提醒您的客戶端。

我喜歡並推薦的服務器到客戶端消息傳遞庫是 Socket.io(通過 Node.js)。 在您的服務器腳本生成正在流式傳輸以供下載的文件后,您在該腳本中的最后一行可以向 Socket.io 發出一條消息,該消息向客戶端發送通知。 在客戶端,Socket.io 偵聽從服務器發出的傳入消息並允許您對其進行操作。 與其他方法相比,使用此方法的好處是您可以在流式傳輸完成后檢測到“真正的”完成事件。

例如,您可以在單擊下載鏈接后顯示忙指示符、流式傳輸文件、在流腳本的最后一行中從服務器向 Socket.io 發送消息、偵聽客戶端以獲取通知、接收通知並通過隱藏忙碌指示器來更新您的用戶界面。

我意識到大多數閱讀這個問題的答案的人可能沒有這種類型的設置,但我已經在我自己的項目中使用了這個精確的解決方案,並且效果非常好。

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

“如何檢測瀏覽器何時接收文件下載?”
我遇到了與該配置相同的問題:
支柱 1.2.9
jquery-1.3.2。
jquery-ui-1.7.1.custom
瀏覽器 11
爪哇5


我的 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);

我希望能幫助某人!

我參加聚會已經很晚了,但如果其他人想知道我的解決方案,我會把它放在這里:

我對這個確切的問題進行了真正的斗爭,但我找到了一個使用 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 永遠不會將下載腳本識別為“已加載”。 我知道這可能不一定檢測瀏覽器何時收到文件,但您的問題聽起來與我的相似。

當用戶觸發文件的生成時,您可以簡單地為該“下載”分配一個唯一 ID,並將用戶發送到每隔幾秒鍾刷新(或使用 AJAX 檢查)的頁面。 文件完成后,將其保存在相同的唯一 ID 下,然后...

  • 如果文件已准備好,請進行下載。
  • 如果文件未准備好,請顯示進度。

然后你可以跳過整個 iframe/waiting/browserwindow 的混亂,但有一個非常優雅的解決方案。

問候,我知道這個話題已經過時了,但我留下了一個我在別處看到的解決方案,它奏效了:

/**
 *  download file, show modal
 *
 * @param uri link
 * @param name file name
 */
function downloadURI(uri, name) {
// <------------------------------------------       Do someting (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");

如果您不想在服務器上生成和存儲文件,您是否願意存儲狀態,例如file-in-progress、file-complete? 您的“等待”頁面可以輪詢服務器以了解文件生成何時完成。 您不會確定瀏覽器是否開始下載,但您會有一些信心。

根據我的經驗,有兩種方法可以解決這個問題:

  1. 在下載時設置一個短暫的 cookie,並讓 JavaScript 不斷檢查它的存在。 唯一真正的問題是使 cookie 生命周期正確 - 太短,JS 可能會錯過它,太長,它可能會取消其他下載的下載屏幕。 使用 JS 在發現時刪除 cookie 通常可以解決這個問題。
  2. 使用 fetch/XHR 下載文件。 您不僅可以准確地知道文件下載何時完成,如果您使用 XHR,您還可以使用進度事件來顯示進度條! 在 IE/Edge 中使用msSaveBlob保存生成的 blob,在 Firefox/Chrome 中使用下載鏈接(像這個)。 這種方法的問題在於 iOS Safari 似乎無法正確處理下載 blob - 您可以使用 FileReader 將 blob 轉換為數據 URL 並在新窗口中打開它,但這是打開文件,而不是保存它。

我剛剛遇到了完全相同的問題。 我的解決方案是使用臨時文件,因為我已經生成了一堆臨時文件。 提交表格時:

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 上的 load 事件被觸發。 然后等待消息關閉,然后文件下載將開始。 在 IE7 和 Firefox 上測試。

如果您下載了一個已保存的文件,而不是在文檔中,則無法確定下載何時完成,因為它不在當前文檔的范圍內,而是瀏覽器中的一個單獨過程。

問題是在生成文件時有一個“等待”指示器,然后在文件下載后恢復正常。 我喜歡這樣做的方式是使用隱藏的 iFrame 並掛鈎框架的 onload 事件以讓我的頁面知道何時開始下載。 但是onload 不會在 IE 中觸發文件下載(如附件標頭令牌)。 輪詢服務器有效,但我不喜歡額外的復雜性。 所以這就是我要做的:

  • 像往常一樣定位隱藏的 iFrame。
  • 生成內容。 在 2 分鍾內使用絕對超時緩存它。
  • 將 javascript 重定向發送回調用客戶端,本質上是第二次調用生成器頁面。 注意:這將導致 onload 事件在 IE 中觸發,因為它就像一個普通頁面。
  • 從緩存中刪除內容並將其發送到客戶端。

免責聲明,不要在繁忙的站點上執行此操作,因為緩存可能會增加。 但實際上,如果您的網站忙於長時間運行的過程,無論如何都會使您缺乏線程。

這是代碼隱藏的樣子,這正是您真正需要的。

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

如果您只想在顯示下載對話框之前顯示消息或加載程序 gif,一個快速的解決方案是將消息放入隱藏的容器中,當您單擊生成要下載的文件的按鈕時,您可以使容器可見。 然后使用jquery或者javascript捕捉按鈕的focusout事件來隱藏包含消息的容器

如果帶有 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 error page configured your application for failed requests, check for those dom elemets } }, 1000) //Better if you specify maximun no of intervals

此 Java/Spring 示例檢測到下載的結束,此時它隱藏“正在加載...”指示符。

方法:在 JS 端,設置一個 Max Expiration Age 為 2 分鍾的 Cookie,每秒輪詢一次 cookie過期時間 然后服務器端用更早的過期時間覆蓋這個 cookie——服務器進程的完成。 一旦在 JS 輪詢中檢測到 cookie 過期,就會隱藏“正在加載...”。

JS端

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 JS 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 JS timer detects the expired cookie earlier than 2 min. 
// (coming from the server), or the initial JS-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 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 timer to check for 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 JS 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 JS-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 sec. for any flushing concerns
        myCookie.setPath("/");
        response.addCookie(myCookie);
        //... -- presumably the download is writing to the Output Stream...
        return null;
}

Primefaces 也使用 cookie 輪詢

https://github.com/primefaces/primefaces/blob/32bb00299d00e50b2cba430638468a4145f4edb0/src/main/resources/META-INF/resources/primefaces/core/core.js#L458

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

我已經更新了以下參考代碼。 添加正確的下載 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

參考: https : //developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/progress_event#live_example

您可以依賴瀏覽器的緩存並在文件加載到緩存時觸發同一文件的第二次下載。

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

單擊按鈕/鏈接時創建一個 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);

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM