浏览器收到文件下载时进行检测
我有一个页面,允许用户下载一个dynamic生成的文件。 生成需要很长时间,所以我想显示一个“等待”指标。 问题是,我不知道如何检测浏览器何时收到文件,所以我可以隐藏指标。
我以隐藏的forms发出请求,发送到服务器,并针对其隐藏的iframe的结果。 这是所以我不会取代整个浏览器窗口的结果。 我在iframe上侦听一个“加载”事件,希望在下载完成时触发。
我用文件返回一个“Content-Disposition:attachment”标题,导致浏览器显示“Save”对话框。 但浏览器不会触发iframe中的“加载”事件。
我尝试的一种方法是使用多部分响应。 所以它会发送一个空的HTML文件,以及附加的可下载文件。 例如:
Content-type: multipart/x-mixed-replace;boundary="abcde" --abcde Content-type: text/html --abcde Content-type: application/vnd.fdf Content-Disposition: attachment; filename=foo.fdf file-content --abcde
这适用于Firefox; 它接收到空的HTML文件,触发“加载”事件,然后显示可下载文件的“保存”对话框。 但是在IE和Safari上却失败了; IE浏览器会触发“加载”事件,但不会下载文件,Safari下载文件(名称和内容types错误),并且不会触发“加载”事件。
一个不同的方法可能是打电话来启动文件创build,然后轮询服务器,直到它准备好,然后下载已经创build的文件。 但我宁愿避免在服务器上创build临时文件。
有没有人有更好的主意?
一种可能的解决scheme在客户端上使用JavaScript。
客户端algorithm:
- 生成一个随机的唯一标记。
- 提交下载请求,并将令牌包含在GET / POST字段中。
- 显示“等待”指示符。
- 启动一个计时器,每隔一秒左右,寻找名为“fileDownloadToken”(或任何你决定的)的cookie。
- 如果cookie存在,并且其值与令牌匹配,则隐藏“等待”指示符。
服务器algorithm:
- 查找请求中的GET / POST字段。
- 如果它具有非空值,则删除一个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 ); }
一个非常简单(而且是蹩脚的)单行解决scheme是使用window.onblur()
事件closures加载对话框。 当然,如果时间太长,用户决定做其他事情(如阅读电子邮件),加载对话框将closures。
老线索,我知道…
但那些由谷歌领导这些可能会对我的解决scheme感兴趣。 这是非常简单,但可靠。 它可以显示真实的进度信息(并可以很容易地插入到现有的过程中):
处理的脚本(我的问题是:通过http检索文件并以zipforms提供)将状态写入会话。
状态每秒钟轮询和显示。 多数民众赞成在所有(好吧,不,你必须照顾很多细节[如并发下载],但它是一个好地方开始;-))。
下载页面:
<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 ?>
我使用下面的下载blob和撤销对象url。 它在铬和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; }
在文件下载对话框closures后,窗口重新获得焦点,从而触发焦点事件。
我写了一个简单的JavaScript类,它实现了一个类似于在一个悲惨的答案中描述的技术。 我希望这对某个人有用。 GitHub项目被称为response-monitor.js
默认情况下,它使用spin.js作为等待指示器,但它也提供了一组实现自定义指示器的callback。
JQuery支持,但不是必需的。
显着的function
- 简单的集成
- 没有依赖关系
- JQuery插件(可选)
- Spin.js集成(可选)
- 用于监视事件的可configurationcallback
- 处理多个同时请求
- 服务器端错误检测
- 超时检测
- 跨浏览器
用法示例
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});
带callback的客户端(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
有关更多示例,请检查存储库上的示例文件夹。
当用户触发文件生成时,您可以简单地为该“下载”分配一个唯一的ID,并将用户发送到每隔几秒刷新一次(或使用AJAX进行检查)的页面。 一旦文件完成,将其保存在同一个唯一的ID和…
- 如果文件准备就绪,请执行下载。
- 如果文件没有准备好,则显示进度。
那么你可以跳过整个iframe / waiting / browserwindow的混乱,但有一个非常优雅的解决scheme。
基于埃尔默的例子,我准备了自己的解决scheme。 元素点击定义的下载类后,可以在屏幕上显示自定义消息。 我用焦点触发来隐藏消息。
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">
每次下载后点击您将看到您的报告正在创build的消息,请稍候…
我只是有这个完全相同的问题。 我的解决scheme是使用临时文件,因为我已经生成了一堆临时文件。 表格提交:
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上的加载事件被触发。 然后等待消息closures,文件下载将开始。 testingIE7和Firefox。
如果你不想在服务器上生成和存储文件,你是否愿意存储状态,例如文件正在进行,文件完成? 您的“等待”页面可以轮询服务器,以了解文件生成何时完成。 你肯定不知道浏览器开始下载,但你会有一些自信。
我很晚参加派对,但如果其他人想知道我的解决scheme,我会把它放在这里:
我对这个确切的问题有了一个真正的斗争,但是我发现了一个使用iframe的可行的解决scheme(我知道,我知道,这太糟糕了,但是对于我有一个简单的问题,
我有一个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再次通过一个请求variables启动脚本,告诉脚本下载文件。
您无法一次完成下载和文件生成的原因是由于php header()函数。 如果您使用header(),您将脚本更改为除网页以外的其他内容,并且jquery将永远不会将下载脚本识别为“已加载”。 我知道这可能不一定是检测何时浏览器收到一个文件,但你的问题听起来很像我的。
如果您正在stream式传输您dynamic生成的文件,并且还实现了实时的服务器到客户端消息传递库,则可以轻松地提醒您的客户端。
我喜欢和推荐的服务器到客户端消息库是Socket.io(通过Node.js)。 服务器脚本完成后,生成正在stream式传输下载的文件,该脚本的最后一行可以向Socket.io发送一条消息,通知客户端。 在客户端,Socket.io监听从服务器发出的传入消息,并允许你对它们进行操作。 使用这种方法相对于其他方法的好处是,您可以在完成stream式传输后检测到“真实”结束事件。
例如,您可以在单击下载链接后显示繁忙指示符,在您的stream式处理脚本的最后一行发送消息到服务器的Socket.io,在客户端侦听通知,接收通知并通过隐藏忙指标来更新您的用户界面。
我意识到大多数人阅读这个问题的答案可能没有这种types的设置,但我已经在我自己的项目中使用这个确切的解决scheme,效果很好,它奇妙地工作。
Socket.io非常易于安装和使用。 查看更多: http : //socket.io/
问题是生成一个文件时有一个“等待”指示符,然后一旦文件正在下载就恢复正常。 我喜欢这种方式是使用隐藏的iFrame,并挂钩框架的onload事件让我的网页知道何时下载开始。 但是 onload不会在IE中触发文件下载(就像使用附件头令牌一样)。 轮询服务器的作品,但我不喜欢额外的复杂性。 所以这就是我所做的:
- 像往常一样定位隐藏的iFrame。
- 生成内容。 在2分钟内用绝对超时进行caching。
- 发送一个JavaScriptredirect回到调用客户端,本质上再次调用生成器页面。 注意:这将导致onload事件在IE中触发,因为它的行为像一个普通的页面。
- 从caching中删除内容并将其发送给客户端。
免责声明,不要在繁忙的网站上这样做,因为caching可能会加起来。 但是,真的,如果你的网站忙于长时间运行的过程将会让你无论如何都是线程的。
这里是代码隐藏的样子,这是你真正需要的。
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'"); } }
一个快速的解决scheme,如果你只想显示一个消息或一个加载器的GIF,直到显示下载对话框是把消息放在一个隐藏的容器中,当你点击生成要下载的文件的button时,可以看到该容器。 然后使用jquery或javascript来捕获button的focusout事件来隐藏包含消息的容器
如果您已经下载了文件,而不是在文档中,则无法确定下载何时完成,因为它不在当前文档的范围内,而是在浏览器中单独执行。
单击button/链接时创build一个iframe并将其附加到正文。
$('<iframe />') .attr('src', url) .attr('id','iframe_download_report') .hide() .appendTo('body');
用延迟创build一个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);