Detect when browser receives file download
我有一个页面,允许用户下载动态生成的文件。生成时间很长,所以我想显示一个"等待"指示器。问题是,我不知道如何检测浏览器何时收到文件,所以我可以隐藏指示器。
我以一个隐藏的形式提出请求,该表单将发布到服务器,并针对其结果生成一个隐藏的iframe。所以我不会用结果替换整个浏览器窗口。我在iframe上监听一个"加载"事件,希望下载完成后它会启动。
我将返回一个带有文件的"content-disposition:attachment"头文件,这将导致浏览器显示"save"对话框。但是浏览器不会在iFrAME中引发一个"加载"事件。
我尝试的一种方法是使用多部分响应。所以它会发送一个空的HTML文件,以及附加的可下载文件。例如:
1 2 3 4 5 6 7 8 9 10 11 | 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文件,触发"加载"事件,然后显示可下载文件的"保存"对话框。但它在IE和Safari上失败;IE触发"加载"事件但不下载文件,Safari下载文件(名称和内容类型错误),并且不触发"加载"事件。
另一种方法可能是调用以开始创建文件,然后轮询服务器直到它准备就绪,然后下载已经创建的文件。但我宁愿避免在服务器上创建临时文件。
有人有更好的主意吗?
一种可能的解决方案是在客户机上使用JavaScript。
客户端算法:
服务器算法:
客户端源代码(JavaScript):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | 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):
1 2 3 4 5 6 7 8 | $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(); |
在哪里?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | 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 ); } |
一个非常简单(也是跛足)的单行解决方案是使用
旧线,我知道…
但那些由谷歌领导的人可能对我的解决方案感兴趣。它很简单,但也很可靠。它使显示真正的进度消息成为可能(并且可以很容易地插入到现有进程中):
处理文件的脚本(我的问题是:通过HTTP检索文件并将其作为zip传递)将状态写入会话。
每秒轮询和显示状态。仅此而已(好吧,不是。您必须处理很多细节(例如并发下载),但这是一个很好的开始;-)。
下载页面:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | DOWNLOAD 1 DOWNLOAD 2 ... Please wait... //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(); } }); } |
GETStasuS.PHP
1 2 3 4 | <?php session_start(); echo json_encode($_SESSION['downloadstatus']); ?> |
下载程序
1 2 3 4 5 6 7 8 9 10 11 12 | <?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 ?> |
我使用下面的内容下载blobs并在下载后撤销对象URL。它在Chrome和Firefox中工作!
1 2 3 4 5 6 7 8 9 10 11 12 | 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类,它实现了一种类似于欺负回答中描述的技术。希望对这里的人有用。Github项目称为response-monitor.js
默认情况下,它使用spin.js作为等待指示器,但它还为实现自定义指示器提供一组回调。
支持jquery,但不需要。
显著特征
- 简单集成
- 无依赖关系
- jquery插件(可选)
- spin.js集成(可选)
- 监控事件的可配置回调
- 处理多个同时请求
- 服务器端错误检测
- 超时检测
- 跨浏览器
示例用法
HTML
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | <!-- the response monitor implementation --> <script src="response-monitor.js"> <!-- optional JQuery plug-in --> <script src="response-monitor.jquery.js"> Link 1 (Timeout: 30s) Link 2 (Timeout: 10s) <form id="my_form" method="POST"> <input type="text" name="criteria1"> <input type="text" name="criteria2"> <input type="submit" value="Download Report"> </form> |
客户端(纯javascript)
1 2 3 4 5 6 7 | //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)
1 2 | $('.my_anchors').ResponseMonitor(); $('#my_form').ResponseMonitor({timeout: 20}); |
带回调的客户端(jquery)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | //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)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | $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 |
有关更多示例,请检查存储库上的Examples文件夹。
基于埃尔默的例子,我已经准备了自己的解决方案。在元素单击定义的下载类之后,它允许在屏幕上显示自定义消息。我用焦点触发器来隐藏信息。
JavaScript
1 2 3 4 5 6 7 8 9 10 11 12 13 | $(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
1 | please wait... |
现在,您应该实现下载的任何元素:
1 | Download report |
或
1 | <input class="download" type="submit" value="Download" name="actionType"> |
每次下载后,您将看到您的报告正在创建的消息,请稍候…
如果您正在动态地生成一个文件,并且还实现了一个实时服务器到客户端消息库,那么您可以很容易地提醒您的客户端。
我喜欢和推荐的服务器到客户端消息库是SoCKE.IO(通过节点.js)。在服务器脚本完成后,生成正在下载的文件,该脚本中的最后一行可以向SoCKET.IO发送消息,它向客户端发送通知。在客户端上,SoCKE.IO监听从服务器发出的传入消息,并允许您对它们进行操作。使用这种方法比其他方法的好处是,在流媒体完成之后,你能够检测到一个"真实"的完成事件。
例如,您可以在单击下载链接后显示忙指示器,传输文件,在流脚本的最后一行从服务器向socket.io发送消息,在客户端上侦听通知,接收通知并通过隐藏忙指示器来更新您的UI。
我意识到大多数阅读这个问题的答案的人可能没有这种类型的设置,但我已经在我自己的项目中使用了这种精确的解决方案,效果非常好。
socket.io非常容易安装和使用。更多信息:http://socket.io/
我参加晚会很晚,但如果有人想知道我的解决方案,我就把它挂在这里:
我确实遇到了这个问题,但我找到了一个可行的解决方案。这很可怕,但它对我的一个简单问题起作用。
我有一个HTML页面,它启动了一个单独的PHP脚本,生成了这个文件,然后下载了它。在HTML页面上,我在HTML标题中使用了以下jquery(您还需要包括jquery库):
1 2 3 4 5 6 7 8 9 10 | $(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) --> }); }); |
在您的_download_script.php上,有以下内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | 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混乱,但有一个真正优雅的解决方案。
我也有同样的问题。我的解决方案是使用临时文件,因为我已经生成了许多临时文件。该表格提交时附带:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | var microBox = { show : function(content) { $(document.body).append('' + content + ''); 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...'); |
在生成文件的脚本末尾,我有:
1 2 | header('Refresh: 0;url=fetch.php?token=' . $token); echo '<html></html>'; |
这将导致触发iframe上的加载事件。然后,等待消息关闭,然后开始文件下载。在IE7和火狐上测试过。
"如何检测浏览器何时收到文件下载?"
我在配置时遇到了同样的问题:
支柱1.2.9
jquery-1.3.2.
jquery-ui-1.7.1.自定义I.BR/> IE 11Java 5
我的cookie解决方案:
-客户端:
提交表单时,调用javascript函数隐藏页面并加载等待微调器
1 2 3 | function loadWaitingSpinner(){ ... hide your page and show your spinner ... } |
然后,调用一个函数,该函数每隔500毫秒检查一次cookie是否来自服务器。
1 2 3 | function checkCookie(){ var verif = setInterval(isWaitingCookie,500,verif); } |
如果找到cookie,请停止每隔500毫秒检查一次,使cookie过期,并调用函数返回页面并删除等待微调器(removeWaitingsPinner())。如果您想再次下载另一个文件,则必须使cookie过期!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | 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将被发送到客户机。
1 2 | Cookie waitCookie = new Cookie("waitingCookie","done"); response.addCookie(waitCookie); |
我希望能帮助别人!
如果您下载了一个已保存的文件,而不是文档中的文件,则无法确定下载完成的时间,因为它不在当前文档的范围内,而是浏览器中的一个单独进程。
如果您只想在显示下载对话框之前显示消息或加载程序gif,那么一个快速的解决方案是将消息放入隐藏的容器中,当您单击生成要下载的文件的按钮时,可以使容器可见。然后使用jquery或javascript捕获按钮的focusout事件以隐藏包含消息的容器
如果xmlhttprequest with blob不是一个选项,那么您可以在新窗口中打开您的文件,并检查eny元素是否以间隔填充到该窗口体中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | 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 |
问题是在生成文件时要有一个"等待"指示器,然后在文件下载后恢复正常。我喜欢这样做的方式是使用隐藏的iframe并钩住框架的onload事件,以便在下载开始时让我的页面知道。但是,在IE中,对于文件下载,onload不会触发(就像使用附件头标记一样)。轮询服务器可以工作,但我不喜欢这种额外的复杂性。所以我要做的是:
- 像往常一样瞄准隐藏的iframe。
- 生成内容。缓存它绝对超时2分钟。
- 发送一个javascript重定向回调用客户机,本质上调用第二次生成页面。注意:这将导致在IE中触发OnLoad事件,因为它的行为类似于常规页面。
- 从缓存中删除内容并发送给客户。
免责声明,不要在繁忙的网站上这样做,因为缓存可能会累积。但实际上,如果您的站点忙于长时间运行的进程,无论如何都会使线程匮乏。
下面是代码隐藏的样子,这是您真正需要的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | 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'"); } } |
单击按钮/链接时创建iframe并将其附加到正文。
1 2 3 4 5 | $('<iframe />') .attr('src', url) .attr('id','iframe_download_report') .hide() .appendTo('body'); |
延迟创建iframe并在下载后将其删除。
1 2 3 4 5 6 7 8 9 10 11 | 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); |