Détecter lorsque le navigateur reçoit le téléchargement du fichier
J'ai une page qui permet à l'utilisateur de télécharger un générées dynamiquement fichier. Cela prend beaucoup de temps à générer, donc j'aimerais montrer un indicateur "en attente". Le problème est, Je ne peux pas comprendre comment détecter quand le navigateur a reçu le fichier, donc je peux cacher l'indicateur.
Je fais la requête sous une forme cachée, qui publie sur le serveur, et cible un iframe caché pour ses résultats. C'est ainsi que je ne remplace pas toute la fenêtre du navigateur par le résultat. J'écoute pour une "charge" événement sur l'iframe, dans l'espoir qu'il se déclenchera lorsque le téléchargement sera terminé.
Je retourne un en-tête" Content-Disposition: attachment " avec le fichier, ce qui amène le navigateur à afficher la boîte de dialogue "Enregistrer". Mais le navigateur ne déclenche pas un événement "load" dans l'iframe.
Une approche que j'ai essayé est d'utiliser une réponse en plusieurs parties. Afin d'envoyer un fichier HTML vide, ainsi que le fichier téléchargeable. Par exemple:
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
Cela fonctionne dans Firefox; il reçoit le HTML vide fichier, déclenche l'événement" load", puis affiche la boîte de dialogue" Save " pour le fichier téléchargeable. Mais il échoue sur IE et Safari; IE déclenche l'événement "load" mais ne télécharge pas le fichier, et Safari télécharge le fichier (avec le mauvais nom et le mauvais type de contenu), et ne déclenche pas l'événement "load".
Une approche différente pourrait consister à lancer un appel pour lancer la création du fichier, puis interroger le serveur jusqu'à ce qu'il soit prêt, puis télécharger le fichier déjà créé. Mais je préfère éviter de créer des fichiers temporaires sur le serveur.
Quelqu'un a une meilleure idée?
16 réponses
Une solution possible utilise JavaScript sur le client.
L'algorithme client:
- générer un jeton unique aléatoire.
- soumettez la demande de téléchargement et incluez le jeton dans un champ GET / POST.
- affiche l'indicateur "attente".
- démarrez une minuterie, et chaque seconde, recherchez un cookie nommé "fileDownloadToken" (ou tout ce que vous décidez).
- si le cookie existe et que sa valeur correspond au jeton, " indicateur.
L'algorithme du serveur:
- recherchez le champ GET / POST dans la requête.
- s'il a une valeur non vide, supprimez un cookie (par exemple "fileDownloadToken") et définissez sa valeur sur la valeur du jeton.
Code source du Client (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;
}
Exemple de code serveur (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();
Où:
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
);
}
Une solution d'une ligne très simple (et boiteuse) consiste à utiliser l'événement window.onblur()
pour fermer la boîte de dialogue de chargement. Bien sûr, si cela prend trop de temps et que l'utilisateur décide de faire autre chose (comme lire des e-mails), la boîte de dialogue de chargement se fermera.
Vieux fil, je sais...
Mais ceux qui sont menés ici par google pourraient être intéressés par ma solution. c'est très simple, mais fiable. et il permet d'Afficher des messages de progression réels (et peut être facilement branché à des processus existants):
Le script qui traite (mon problème était: récupérer des fichiers via http et les livrer en zip) écrit l'état dans la session.
L'état est interrogé et affiché toutes les secondes. c'est tout (ok, ce n'est pas. vous devez prendre soin de beaucoup de détails [par exemple les téléchargements simultanés], mais c'est un bon endroit pour commencer ;-)).
La page de téléchargement:
<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']);
?>
Télécharger.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
?>
J'utilise ce qui suit pour télécharger des blobs et révoquer l'url d'objet après le téléchargement. cela fonctionne dans chrome et 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;
}
Une fois la boîte de dialogue de téléchargement de fichier fermée, la fenêtre récupère son focus afin que l'événement focus soit déclenché.
, j'ai écrit un simple JavaScript classe qui implémente une technique similaire à celle décrite dans bulltorious réponse. J'espère que cela peut être utile à quelqu'un ici. Le projet GitHub s'appelle response-monitor.js
Par défaut, il utilise tourner.js {[7] } comme indicateur d'attente mais il fournit également un ensemble de rappels pour l'implémentation d'un indicateur personnalisé.
JQuery est pris en charge mais pas nécessaire.
Notable caractéristiques
- intégration Simple
- aucune dépendance
- plug-in JQuery (facultatif)
- tourner.intégration js (facultatif)
- rappels configurables pour surveiller les événements
- gère plusieurs requêtes simultanées
- détection D'erreur Côté Serveur
- Détection du délai d'attente
- navigateur croisé
Exemple d'utilisation de
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 (JavaScript simple)
//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 avec callbacks (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);
Serveur (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
Pour plus d'exemples, consultez le dossierexamples du référentiel.
Sur la base de L'exemple D'Elmer, j'ai préparé ma propre solution. Après que les éléments cliquent avec la classe download définie, il permet d'afficher un message personnalisé à l'écran. J'ai utilisé focus déclencheur pour cacher le message.
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>
Maintenant, vous devez implémenter n'importe quel élément à télécharger:
<a class="download" href="file://www.ocelot.com.pl/prepare-report">Download report</a>
Ou
<input class="download" type="submit" value="Download" name="actionType">
Après chaque Télécharger cliquez sur vous verrez le message votre rapport est en train de créer, s'il vous plait attendre...
Lorsque l'utilisateur déclenche la génération du fichier, vous pouvez simplement attribuer un ID unique à ce "téléchargement", et envoyer l'utilisateur à une page qui s'actualise (ou vérifie avec AJAX) toutes les quelques secondes. Une fois le fichier terminé, enregistrez-le sous ce même identifiant unique et...
- si le fichier est prêt, effectuez le téléchargement.
- si le fichier n'est pas prêt, afficher la progression.
Ensuite, vous pouvez ignorer tout le désordre iframe/waiting/browserwindow, mais avoir un très élégant solution.
Je suis très en retard à la fête mais je vais mettre ceci ici si quelqu'un d'autre voudrait connaître ma solution:
J'ai eu une vraie lutte avec ce problème exact mais j'ai trouvé une solution viable en utilisant des iframes (je sais, je sais. C'est terrible mais cela fonctionne pour un problème simple que j'ai eu)
J'ai eu une page html qui a lancé un script PHP distinct qui a généré le fichier, puis l'a téléchargé. Sur la page html, j'ai utilisé le jquery suivant dans l'en-tête html (vous devrez inclure un jquery bibliothèque aussi):
<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>
Sur your_download_script.php, ont les éléments suivants:
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*
}
Pour décomposer cela, jquery lance d'abord votre script php dans un iframe. L'iframe est chargé une fois le fichier généré. Ensuite, jquery lance à nouveau le script avec une variable de requête indiquant au script de télécharger le fichier.
La raison pour laquelle vous ne pouvez pas faire le téléchargement et la génération de fichiers en une seule fois est due à la fonction PHP header (). Si vous utilisez header (), vous modifiez le script à autre chose qu'une page web et jquery ne reconnaîtra jamais le script de téléchargement comme étant "chargé". Je sais que cela ne détecte pas nécessairement quand un navigateur reçoit un fichier, mais votre problème semblait similaire au mien.
J'ai juste eu exactement le même problème. Ma solution était d'utiliser des fichiers temporaires puisque je générais déjà un tas de fichiers temporaires. Le formulaire est soumis avec:
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...');
À la fin du script qui génère le fichier que j'ai:
header('Refresh: 0;url=fetch.php?token=' . $token);
echo '<html></html>';
Cela provoquera le déclenchement de l'événement load sur l'iframe. Ensuite, le message d'attente est fermé et le téléchargement du fichier démarre. Testé sur IE7 et Firefox.
Si vous ne voulez pas générer et stocker le fichier sur le serveur, êtes-vous prêt à stocker l'état, par exemple file-in-progress, file-complete? Votre page "en attente" pourrait interroger le serveur pour savoir quand la génération de fichiers est terminée. Vous ne sauriez pas avec certitude que le navigateur a commencé le téléchargement, mais vous auriez une certaine confiance.
Si vous diffusez un fichier que vous générez dynamiquement et que vous disposez également d'une bibliothèque de messagerie Serveur-client en temps réel, vous pouvez alerter votre client assez facilement.
La bibliothèque de messagerie Serveur-client que j'aime et que je recommande est Socket.io (via le nœud.js). Une fois votre script serveur terminé générant le fichier en cours de diffusion pour téléchargement votre dernière ligne de ce script peut émettre un message à Socket.io qui envoie une notification au client. Sur le client, Socket.io écoute les messages entrants émis par le serveur et vous permet d'agir sur eux. L'avantage d'utiliser cette méthode par rapport aux autres est que vous êtes capable de détecter un événement de finition "vrai" après la diffusion en continu.
Par exemple, vous pouvez afficher votre indicateur occupé après avoir cliqué sur un lien de téléchargement, diffuser votre fichier, Socket.io à partir du serveur dans la dernière ligne de votre script de streaming, Écoutez sur le client une notification, recevez la notification et mettez à jour votre interface utilisateur en cachant l'indicateur occupé.
Je me rends compte que la plupart des gens qui lisent des réponses à cette question n'ont peut-être pas ce type de configuration, mais j'ai utilisé cette solution exacte dans mes propres projets et cela fonctionne à merveille.
Socket.io est incroyablement facile à installer et à utiliser. Voir plus: http://socket.io/
La question Est d'avoir un indicateur "en attente" pendant qu'un fichier est généré, puis de revenir à la normale une fois le fichier téléchargé. La façon dont j'aime faire cela consiste à utiliser un iFrame caché et à accrocher l'événement onload du Cadre pour que ma page sache quand le téléchargement commence. mais onload ne se déclenche pas dans IE pour les téléchargements de fichiers (comme avec le jeton d'en-tête de pièce jointe). Interroger le serveur fonctionne, mais je n'aime pas la complexité supplémentaire. Voici donc ce que je fais:
- cible l'iFrame caché comme habituel.
- Générer le contenu. Cache avec un délai d'attente absolu en 2 minutes.
- renvoyer une redirection javascript vers le client appelant, appelant essentiellement l' générateur page une deuxième fois. Remarque: cela provoquera le déclenchement de l'événement onload dans IE car il agit comme une page régulière.
- supprime le contenu du cache et envoyez-le au client.
Avertissement, Ne le faites pas sur un site occupé, à cause de la mise en cache pourrait s'additionner. Mais vraiment, si vos sites occupé le long processus en cours d'exécution vous affamera de threads de toute façon.
Voici à quoi ressemble le codebehind, ce qui est tout ce dont vous avez vraiment besoin.
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'");
}
}
Une solution rapide si vous voulez seulement afficher un message ou un chargeur gif jusqu'à ce que la boîte de dialogue de téléchargement est affiché est de mettre le message dans un conteneur caché et lorsque vous cliquez sur le bouton qui génère le fichier à télécharger, vous rendre le conteneur visible. Ensuite, utilisez jquery ou javascript pour attraper l'événement focusout du bouton pour masquer le conteneur qui contient le message
Si Xmlhttprequest avec blob n'est pas une option, vous pouvez ouvrir votre fichier dans une nouvelle fenêtre et vérifier si les éléments eny sont remplis dans ce corps de fenêtre avec interval.
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
Si vous avez télécharger un fichier qui est enregistré, plutôt que d'être dans le document, il n'y a aucune façon de déterminer quand le téléchargement est terminé, car il n'est pas dans la portée du présent document, mais d'un processus distinct dans le navigateur.
Créez un iframe lorsque le bouton / lien est cliqué et ajoutez-le au corps.
$('<iframe />')
.attr('src', url)
.attr('id','iframe_download_report')
.hide()
.appendTo('body');
Créez un iframe avec delay et supprimez-le après le téléchargement.
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);