Erkennen, wann der Browser den Dateidownload erhält


488

Ich habe eine Seite, auf der der Benutzer eine dynamisch generierte Datei herunterladen kann. Das Generieren dauert lange, daher möchte ich einen "Warte" -Indikator anzeigen. Das Problem ist, dass ich nicht herausfinden kann, wie ich feststellen kann, wann der Browser die Datei empfangen hat, sodass ich den Indikator ausblenden kann.

Ich mache die Anfrage in einer versteckten Form, die an den Server POSTs und zielt auf einen versteckten Iframe für seine Ergebnisse. Dies ist so, dass ich nicht das gesamte Browserfenster durch das Ergebnis ersetze. Ich warte auf ein "Lade" -Ereignis im Iframe, in der Hoffnung, dass es ausgelöst wird, wenn der Download abgeschlossen ist.

Ich gebe einen "Content-Disposition: Anhang" -Header mit der Datei zurück, wodurch der Browser das Dialogfeld "Speichern" anzeigt. Der Browser löst jedoch kein "Lade" -Ereignis im Iframe aus.

Ein Ansatz, den ich ausprobiert habe, ist die Verwendung einer mehrteiligen Antwort. Es würde also eine leere HTML-Datei sowie die angehängte herunterladbare Datei senden. Zum Beispiel:

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

Dies funktioniert in Firefox. Es empfängt die leere HTML-Datei, löst das Ereignis "Laden" aus und zeigt dann das Dialogfeld "Speichern" für die herunterladbare Datei an. Aber es schlägt auf IE und Safari fehl; IE löst das Ereignis "Laden" aus, lädt die Datei jedoch nicht herunter, und Safari lädt die Datei herunter (mit dem falschen Namen und Inhaltstyp) und löst das Ereignis "Laden" nicht aus.

Ein anderer Ansatz könnte darin bestehen, einen Anruf zu tätigen, um die Dateierstellung zu starten, dann den Server abzufragen, bis er fertig ist, und dann die bereits erstellte Datei herunterzuladen. Ich möchte jedoch lieber vermeiden, temporäre Dateien auf dem Server zu erstellen.

Hat jemand eine bessere Idee?


4
Keine IE-Version unterstützt Multipart / X-Mixed-Replace.
EricLaw

Danke Eric - das ist gut zu wissen. Ich werde mit diesem Ansatz keine Zeit mehr verschwenden.
JW.

Der einzig verlässliche Weg scheint die Server-Push-Benachrichtigung zu sein (SignalR für ASP.NET-Leute).
dudeNumber4

1
bennadel.com/blog/… - dies ist eine einfache Lösung
Mateen

1
@mateen danke Kumpel! es ist wirklich einfach
Fai Zal Dong

Antworten:


451

Eine mögliche Lösung verwendet JavaScript auf dem Client.

Der Client-Algorithmus:

  1. Generieren Sie ein zufälliges eindeutiges Token.
  2. Senden Sie die Download-Anfrage und fügen Sie das Token in ein GET / POST-Feld ein.
  3. Zeigen Sie die Anzeige "Warten" an.
  4. Starten Sie einen Timer und suchen Sie jede Sekunde nach einem Cookie mit dem Namen "fileDownloadToken" (oder was auch immer Sie sich entscheiden).
  5. Wenn das Cookie vorhanden ist und sein Wert mit dem Token übereinstimmt, verbergen Sie die Anzeige "Warten".

Der Server-Algorithmus:

  1. Suchen Sie in der Anfrage nach dem Feld GET / POST.
  2. Wenn der Wert nicht leer ist, löschen Sie ein Cookie (z. B. "fileDownloadToken") und setzen Sie den Wert auf den Wert des Tokens.

Client-Quellcode (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;
}

Beispiel für einen Servercode (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();

Wo:

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

4
Tolle Idee, ich habe es als Grundgerüst für diese Antwort zum Herunterladen mehrerer Dateien mit jQuery / C # verwendet
Greg

7
Ein Hinweis für andere: Wenn document.cookies das downloadToken nicht enthält, überprüfen Sie den Cookie-Pfad. In meinem Fall musste ich den Pfad auf der Serverseite auf "/" setzen (z. B. cookie.setPath ("/") in Java), obwohl der Pfad standardmäßig leer war. Für einige Zeit dachte ich, das Problem sei die spezielle Behandlung von 'localhost'-Domain-Cookies ( stackoverflow.com/questions/1134290/… ), aber das war am Ende nicht das Problem. Vielleicht ist das für andere so lesenswert.
jlpp

2
@bulltorious Bevor ich näher auf Ihre Lösung eingehe, frage ich mich, ob sie mit einer domänenübergreifenden Download-Anfrage für Dateien funktioniert. Denken Sie, dass dies der Fall sein wird oder dass Cookies-Einschränkungen dies gefährden werden?
Kiks73

5
Genial - mir wäre in 100 Jahren nicht in den Sinn gekommen, dass Sie Cookies als Teil eines Dateidownloads einbinden könnten. Vielen Dank!!
Freifaller

8
Wie andere bereits betont haben, löst diese Lösung nur einen Teil des Problems, nämlich das Warten, bis der Server die Dateizeit vorbereitet hat. Der andere Teil des Problems, der je nach Dateigröße und Verbindungsgeschwindigkeit erheblich sein kann, besteht darin, wie lange es dauert, bis die gesamte Datei tatsächlich auf dem Client gespeichert ist. Und das ist mit dieser Lösung nicht gelöst.
AsGoodAsItGets

27

Eine sehr einfache (und lahme) einzeilige Lösung besteht darin, das window.onblur()Ereignis zum Schließen des Ladedialogs zu verwenden. Wenn es zu lange dauert und der Benutzer sich entscheidet, etwas anderes zu tun (z. B. E-Mails zu lesen), wird der Ladedialog natürlich geschlossen.


Dies ist ein einfacher Ansatz, der ideal ist, um ein Lade-Overlay für einen Dateidownload zu entfernen, der mit onbeforeunloadThank you ausgelöst wurde .
wf4

5
Dies funktioniert nicht in allen Browsern (einige verlassen / verwischen das aktuelle Fenster nicht als Teil des Download-Workflows, z. B. Safari, einige IE-Versionen usw.).
Hiattp

4
Chrome und andere solche Browser laden automatisch die Dateien herunter, bei denen diese Bedingung fehlschlägt.
Glücklicher

@Lucky das ist nur standardmäßig. Es ist durchaus möglich, dass ein Nutzer von Chrome angibt, wo Downloads gespeichert werden sollen, und daher das Dialogfeld
ESR

2
schlechte Idee, weil Sie die Unschärfe beim Tab-Wechsel oder eine Aktion außerhalb des Fensters aktivieren
Michael

14

alter Thread, ich weiß ...

Aber diejenigen, die hier von Google geführt werden, könnten an meiner Lösung interessiert sein. es ist sehr einfach und dennoch zuverlässig. und es ermöglicht die Anzeige von echten Fortschrittsmeldungen (und kann einfach in vorhandene Prozesse eingebunden werden):

Das Skript, das verarbeitet (mein Problem war: Dateien über http abrufen und als Zip-Datei bereitstellen), schreibt den Status in die Sitzung.

Der Status wird abgefragt und jede Sekunde angezeigt. das ist alles (ok, es ist nicht so. Sie müssen sich um viele Details kümmern [z. B. gleichzeitige Downloads], aber es ist ein guter Anfang ;-)).

die Downloadseite:

    <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']);
?>

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

3
aber wenn der Benutzer mehrere Fenster oder Downloads geöffnet hat? Außerdem erhalten Sie hier einen redundanten Anruf an den Server
Yuki

3
Wenn Sie mehrere Verbindungen von einem Benutzer haben, warten alle darauf, dass andere Verbindungen beendet werden, da session_start () die Sitzung für den Benutzer sperrt und verhindert, dass alle anderen Prozesse darauf zugreifen.
Honza Kuchař

2
Sie müssen nicht .each()für Event-Registrierungen verwenden. Sagen $('a.download').click()
Sie

Bewerten Sie den Code nicht im Inneren setTimeout('getstatus()', 1000);. Verwenden Sie die fn direkt:setTimeout(getstatus, 1000);
Roko C. Buljan

11

Ich benutze die folgenden, um Blobs herunterzuladen und die Objekt-URL nach dem Download zu widerrufen. es funktioniert in Chrom und 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;
}

Nachdem der Dialog zum Herunterladen von Dateien geschlossen wurde, erhält das Fenster ihren Fokus zurück, sodass das Fokusereignis ausgelöst wird.


Es besteht immer noch das Problem, das Fenster zu wechseln und zurückzukehren, wodurch sich das Modal versteckt.
dudeNumber4

9
Browser wie Chrome, die in das untere Fach heruntergeladen werden, verwischen / fokussieren das Fenster nie neu.
Coleman

10

Anhand von Elmers Beispiel habe ich meine eigene Lösung vorbereitet. Nachdem Elemente mit definierter Download- Klasse geklickt haben, können benutzerdefinierte Nachrichten auf dem Bildschirm angezeigt werden. Ich habe verwendet Fokus Trigger , um die Nachricht zu verbergen.

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>

Jetzt sollten Sie jedes Element zum Herunterladen implementieren:

<a class="download" href="file://www.ocelot.com.pl/prepare-report">Download report</a>

oder

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

Nach jedem Download- Klick sehen Sie eine Nachricht, die Ihr Bericht erstellt. Bitte warten Sie ...


2
Was ist, wenn der Benutzer auf das Fenster klickt?
Tom Roggero

das ist genau das, wonach ich gesucht habe, danke !!
Sergio

Das hide () wird in meinem Fall nicht aufgerufen
Prashant Pimpale

8

Ich habe eine einfache JavaScript-Klasse geschrieben, die eine ähnliche Technik implementiert wie die in bulltorious answer beschriebene . Ich hoffe, es kann jemandem hier nützlich sein. Das GitHub-Projekt heißt response-monitor.js

Standardmäßig wird spin.js als Warteindikator verwendet, es werden jedoch auch Rückrufe für die Implementierung eines benutzerdefinierten Indikators bereitgestellt .

JQuery wird unterstützt, ist aber nicht erforderlich.

Bemerkenswerte Eigenschaften

  • Einfache Integration
  • Keine Abhängigkeiten
  • JQuery-Plug-In (optional)
  • Spin.js Integration (optional)
  • Konfigurierbare Rückrufe zur Überwachung von Ereignissen
  • Verarbeitet mehrere gleichzeitige Anforderungen
  • Serverseitige Fehlererkennung
  • Timeout-Erkennung
  • Browserübergreifend

Anwendungsbeispiel

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>

Client (einfaches 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

Client (JQuery)

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

Client mit Rückrufen (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);

Server (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

Weitere Beispiele finden Sie im Beispielordner im Repository.


5

Ich bin sehr spät zur Party, aber ich werde das hier aufstellen, wenn jemand anderes meine Lösung wissen möchte:

Ich hatte einen echten Kampf mit genau diesem Problem, aber ich fand eine praktikable Lösung mit iframes (ich weiß, ich weiß. Es ist schrecklich, aber es funktioniert für ein einfaches Problem, das ich hatte)

Ich hatte eine HTML-Seite, die ein separates PHP-Skript startete, das die Datei generierte und dann herunterlud. Auf der HTML-Seite habe ich die folgende JQuery im HTML-Header verwendet (Sie müssen auch eine JQuery-Bibliothek einfügen):

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

Geben Sie in Ihrer_download_script.php Folgendes an:

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*
}

Um dies zu beheben, startet jquery zuerst Ihr PHP-Skript in einem Iframe. Der Iframe wird geladen, sobald die Datei generiert wurde. Anschließend startet jquery das Skript erneut mit einer Anforderungsvariablen, die das Skript anweist, die Datei herunterzuladen.

Der Grund, warum Sie den Download und die Dateierzeugung nicht auf einmal durchführen können, liegt in der Funktion php header (). Wenn Sie header () verwenden, ändern Sie das Skript in eine andere als eine Webseite, und jquery erkennt das Download-Skript nie als "geladen". Ich weiß, dass dies möglicherweise nicht unbedingt erkennt, wenn ein Browser eine Datei empfängt, aber Ihr Problem klang ähnlich wie meins.


5

Wenn Sie eine Datei streamen, die Sie dynamisch generieren, und eine Echtzeit-Server-zu-Client-Messaging-Bibliothek implementiert ist, können Sie Ihren Client ganz einfach benachrichtigen.

Die Server-zu-Client-Messaging-Bibliothek, die ich mag und empfehle, ist Socket.io (über Node.js). Nachdem Ihr Serverskript fertig ist, kann die letzte Zeile in diesem Skript, die zum Herunterladen gestreamt wird, eine Nachricht an Socket.io senden, die eine Benachrichtigung an den Client sendet. Auf dem Client wartet Socket.io auf eingehende Nachrichten, die vom Server gesendet werden, und ermöglicht es Ihnen, darauf zu reagieren. Der Vorteil dieser Methode gegenüber anderen besteht darin, dass Sie nach Abschluss des Streamings ein "echtes" Endereignis erkennen können.

Sie können beispielsweise Ihre Besetztanzeige anzeigen, nachdem auf einen Download-Link geklickt wurde, Ihre Datei streamen, eine Nachricht vom Server in der letzten Zeile Ihres Streaming-Skripts an Socket.io senden, auf dem Client auf eine Benachrichtigung warten und die Benachrichtigung erhalten und aktualisieren Sie Ihre Benutzeroberfläche, indem Sie die Besetztanzeige ausblenden.

Mir ist klar, dass die meisten Leute, die Antworten auf diese Frage lesen, möglicherweise nicht über diese Art von Setup verfügen, aber ich habe diese exakte Lösung in meinen eigenen Projekten mit großer Wirkung verwendet und sie funktioniert wunderbar.

Socket.io ist unglaublich einfach zu installieren und zu verwenden. Weitere Informationen : http://socket.io/


5

"Wie erkennt man, wann der Browser den Dateidownload erhält?"
Ich hatte das gleiche Problem mit dieser Konfiguration:
struts
1.2.9 jquery-1.3.2.
jquery-ui-1.7.1.custom
IE 11
java 5


Meine Lösung mit einem Cookie:
- Client-Seite:
Rufen Sie beim Absenden Ihres Formulars Ihre Javascript-Funktion auf, um Ihre Seite auszublenden und Ihren wartenden Spinner zu laden

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

Rufen Sie dann eine Funktion auf, die alle 500 ms überprüft, ob ein Cookie vom Server kommt.

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

Wenn das Cookie gefunden wird, beenden Sie die Überprüfung alle 500 ms, lassen Sie das Cookie ablaufen und rufen Sie Ihre Funktion auf, um zu Ihrer Seite zurückzukehren und den wartenden Spinner zu entfernen ( removeWaitingSpinner () ). Es ist wichtig, das Cookie abzulaufen, wenn Sie eine andere Datei erneut herunterladen möchten!

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 ...
}

- Serverseite:
Fügen Sie am Ende Ihres Serverprozesses der Antwort ein Cookie hinzu. Dieses Cookie wird an den Client gesendet, wenn Ihre Datei zum Download bereitsteht.

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

Ich hoffe jemandem zu helfen!


Es funktioniert perfekt. Vielen Dank für diese schöne Probe.
Sedat Kumcu

4

Wenn der Benutzer die Generierung der Datei auslöst, können Sie diesem "Download" einfach eine eindeutige ID zuweisen und den Benutzer an eine Seite senden, die alle paar Sekunden aktualisiert (oder mit AJAX überprüft) wird. Wenn die Datei fertig ist, speichern Sie sie unter derselben eindeutigen ID und ...

  • Wenn die Datei fertig ist, führen Sie den Download durch.
  • Wenn die Datei noch nicht fertig ist, zeigen Sie den Fortschritt an.

Dann können Sie das ganze Iframe / Warten / Browserfenster-Durcheinander überspringen und haben dennoch eine wirklich elegante Lösung.


Das klingt nach dem oben erwähnten Ansatz für temporäre Dateien. Ich könnte so etwas tun, wenn sich herausstellt, dass meine Idee unmöglich ist, aber ich hatte gehofft, es zu vermeiden.
JW.

3

Wenn Sie die Datei nicht generieren und auf dem Server speichern möchten, sind Sie bereit, den Status zu speichern, z. B. Datei in Bearbeitung, Datei vollständig? Ihre "wartende" Seite könnte den Server abfragen, um zu erfahren, wann die Dateierzeugung abgeschlossen ist. Sie würden nicht sicher wissen, ob der Browser den Download gestartet hat, aber Sie hätten ein gewisses Vertrauen.


2

Ich hatte gerade genau das gleiche Problem. Meine Lösung bestand darin, temporäre Dateien zu verwenden, da ich bereits eine Reihe temporärer Dateien generiert habe. Das Formular wird eingereicht mit:

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...');

Am Ende des Skripts, das die Datei generiert, habe ich:

header('Refresh: 0;url=fetch.php?token=' . $token);
echo '<html></html>';

Dadurch wird das Ladeereignis auf dem Iframe ausgelöst. Dann wird die Wartemeldung geschlossen und der Dateidownload gestartet. Getestet auf IE7 und Firefox.


2

Nach meiner Erfahrung gibt es zwei Möglichkeiten, damit umzugehen:

  1. Setzen Sie beim Herunterladen ein kurzlebiges Cookie und lassen Sie JavaScript kontinuierlich auf seine Existenz überprüfen. Das einzige wirkliche Problem ist, dass die Lebensdauer der Cookies stimmt - zu kurz und der JS kann sie übersehen, zu lang und die Download-Bildschirme für andere Downloads werden möglicherweise abgebrochen. Die Verwendung von JS zum Entfernen des Cookies bei der Erkennung behebt dies normalerweise.
  2. Laden Sie die Datei mit fetch / XHR herunter. Sie wissen nicht nur genau, wann der Dateidownload abgeschlossen ist. Wenn Sie XHR verwenden, können Sie Fortschrittsereignisse verwenden, um einen Fortschrittsbalken anzuzeigen! Speichern Sie den resultierenden Blob mit msSaveBlob in IE / Edge und einem Download-Link ( wie diesem ) in Firefox / Chrome. Das Problem bei dieser Methode ist, dass iOS Safari das Herunterladen von Blobs nicht richtig zu handhaben scheint. Sie können den Blob mit einem FileReader in eine Daten-URL konvertieren und diese in einem neuen Fenster öffnen. Dadurch wird die Datei geöffnet und nicht gespeichert.

2

Grüße, ich weiß, dass das Thema alt ist, aber ich hinterlasse eine Lösung, die ich woanders gesehen habe und die funktioniert hat:

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

Du kannst es benutzen:

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

1

Wenn Sie eine Datei heruntergeladen haben, die gespeichert wird, anstatt sich im Dokument zu befinden, können Sie nicht feststellen, wann der Download abgeschlossen ist, da dies nicht im Umfang des aktuellen Dokuments liegt, sondern ein separater Vorgang im Browser.


8
Ich sollte klarstellen - ich bin nicht allzu besorgt darüber, wann der Download abgeschlossen ist . Wenn ich nur feststellen kann, wann der Download beginnt, würde das ausreichen.
JW.

0

Die Frage ist, ob beim Generieren einer Datei eine Warteanzeige angezeigt wird und nach dem Herunterladen der Datei wieder normal angezeigt wird. Ich verwende dazu gerne einen versteckten iFrame und verknüpfe das Onload-Ereignis des Frames, um meine Seite über den Start des Downloads zu informieren. ABER Onload wird im IE nicht für Dateidownloads ausgelöst (wie beim Header-Token für Anhänge). Das Abrufen des Servers funktioniert, aber ich mag die zusätzliche Komplexität nicht. Also hier ist was ich tue:

  • Zielen Sie wie gewohnt auf den versteckten iFrame.
  • Generieren Sie den Inhalt. Cache es mit einem absoluten Timeout in 2 Minuten.
  • Senden Sie eine Javascript-Umleitung zurück an den aufrufenden Client, und rufen Sie die Generatorseite im Wesentlichen ein zweites Mal auf. HINWEIS: Dadurch wird das Onload-Ereignis im IE ausgelöst, da es sich wie eine normale Seite verhält.
  • Entfernen Sie den Inhalt aus dem Cache und senden Sie ihn an den Client.

Haftungsausschluss, tun Sie dies nicht auf einer belebten Site, da sich das Caching summieren könnte. Aber wirklich, wenn Ihre Websites, die den lang laufenden Prozess beschäftigt sind, Sie sowieso von Threads hungern lassen.

So sieht der Code aus, das ist alles, was Sie wirklich brauchen.

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

0

Eine schnelle Lösung, wenn Sie nur eine Nachricht oder ein Loader-GIF anzeigen möchten, bis der Download-Dialog angezeigt wird, besteht darin, die Nachricht in einem versteckten Container abzulegen. Wenn Sie auf die Schaltfläche klicken, mit der die herunterzuladende Datei generiert wird, wird der Container sichtbar. Verwenden Sie dann jquery oder javascript, um das Focusout-Ereignis der Schaltfläche abzufangen und den Container auszublenden, der die Nachricht enthält


0

Wenn Xmlhttprequest mit Blob keine Option ist, können Sie Ihre Datei in einem neuen Fenster öffnen und prüfen, ob in diesem Fensterkörper eny Elemente mit Intervallen ausgefüllt werden.

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


0

In diesem Java / Spring-Beispiel wird das Ende eines Downloads erkannt. An diesem Punkt wird die Anzeige "Laden ..." ausgeblendet.

Vorgehensweise: Setzen Sie auf der JS-Seite ein Cookie mit einem maximalen Ablaufalter von 2 Minuten und fragen Sie jede Sekunde nach dem Ablaufdatum des Cookies . Dann überschreibt die Serverseite dieses Cookie mit einem früheren Ablaufalter - dem Abschluss des Serverprozesses. Sobald der Cookie-Ablauf in der JS-Abfrage erkannt wird, wird "Laden ..." ausgeblendet.

JS Seite

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 Server Seite

    @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;
}

Das Cookie wird vom JS-Skript erstellt, aber nicht vom Controller aktualisiert. Es behält den ursprünglichen Wert (0) bei. Wie kann ich den Cookie-Wert aktualisieren, ohne die Seite zu aktualisieren?
Shessuky

Das ist seltsam - können Sie sicherstellen, dass der Name genau korrekt ist? Der Cookie wird überschrieben, wenn der Name übereinstimmt. Lass es mich wissen
Gen b.

Der ursprüngliche Wert ist nicht 0. Der in JS festgelegte ursprüngliche Wert beträgt 2 Minuten. Der NEUE Wert, mit dem der Server ändern soll, ist 0.
Gen b.

Tun Sie dies auch: myCookie.setPath("/"); response.addCookie(myCookie);
Gen b.

Ich habe (aus irgendeinem Grund) herausgefunden, dass ich Cookies hinzufügen sollte, bevor ich response.getOutputStream () mache ; (Antwort-Ausgabestream zum Anhängen von Download-Dateien), wurde nicht berücksichtigt, als ich es nach diesem Schritt
tat

0

Primefaces verwendet auch Cookie-Polling

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

-2

Erstellen Sie einen Iframe, wenn Sie auf die Schaltfläche / den Link klicken, und hängen Sie diesen an den Text an.

                  $('<iframe />')
                 .attr('src', url)
                 .attr('id','iframe_download_report')
                 .hide()
                 .appendTo('body'); 

Erstellen Sie einen Iframe mit Verzögerung und löschen Sie ihn nach dem Download.

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

Hier fehlen Informationen und das Problem "Wann wird das Laden ausgeblendet?" Wird nicht gelöst.
Tom Roggero
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.