我有一个允许用户下载动态生成文件的页面。生成它需要很长时间,所以我想显示一个“等待”指示器。问题是,我不知道如何检测浏览器何时接收到文件,以便我可以隐藏指示器。
我请求一个隐藏的表单,它post到服务器,并以一个隐藏的iframe作为其结果的目标。这样我就不会用结果替换整个浏览器窗口。我监听iframe上的“load”事件,希望它在下载完成时触发。
我将文件返回一个“Content-Disposition: attachment”头,这将导致浏览器显示“Save”对话框。但是浏览器不会在iframe中触发“load”事件。
我尝试过的一种方法是采用多部分回答。所以它会发送一个空的HTML文件,以及附加的可下载文件。
例如:
Content-type: multipart/x-mixed-replace;boundary="abcde"
--abcde
Content-type: text/html
--abcde
Content-type: application/vnd.fdf
Content-Disposition: attachment; filename=foo.fdf
file-content
--abcde
这在Firefox中是有效的;它接收空HTML文件,触发“load”事件,然后显示可下载文件的“Save”对话框。但它在ie和Safari浏览器上无法运行;Internet Explorer触发“load”事件,但它不下载文件,而Safari下载文件(带有错误的名称和内容类型),但不触发“load”事件。
另一种方法可能是调用启动文件创建,轮询服务器直到准备就绪,然后下载已经创建的文件。但我宁愿避免在服务器上创建临时文件。
我该怎么办?
这个解决方案非常简单,但很可靠。并且它可以显示真实的进度消息(并且可以轻松地插入到现有流程中):
处理的脚本(我的问题是:通过HTTP检索文件并将其作为ZIP传递)将状态写入会话。
该状态每秒轮询一次。这就是全部(好吧,这不是。您必须注意很多细节(例如,并发下载),但这是一个开始的好地方;-))。
下载页面:
<a href="download.php?id=1" class="download">DOWNLOAD 1</a>
<a href="download.php?id=2" class="download">DOWNLOAD 2</a>
...
<div id="wait">
Please wait...
<div id="statusmessage"></div>
</div>
<script>
// This is jQuery
$('a.download').each(function()
{
$(this).click(
function() {
$('#statusmessage').html('prepare loading...');
$('#wait').show();
setTimeout('getstatus()', 1000);
}
);
});
});
function getstatus() {
$.ajax({
url: "/getstatus.php",
type: "POST",
dataType: 'json',
success: function(data) {
$('#statusmessage').html(data.message);
if(data.status == "pending")
setTimeout('getstatus()', 1000);
else
$('#wait').hide();
}
});
}
</script>
文件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
?>
问题是在生成文件时有一个“等待”指示器,然后在文件下载后恢复正常。我喜欢这样做的方式是使用一个隐藏的iFrame和钩子帧的onload事件,让我的页面知道什么时候开始下载。
但是onload不会在Internet Explorer中触发文件下载(就像使用附件头令牌一样)。轮询服务器是可行的,但我不喜欢这种额外的复杂性。这就是我所做的:
目标隐藏iFrame像往常一样。
生成内容。用
2分钟内绝对超时。
发送JavaScript重定向回
调用客户端,本质上调用
第二次生成页面。注意:这将导致在Internet Explorer中触发onload事件,因为它就像一个普通页面一样。
从缓存中删除内容并
发送给客户端。
免责声明:不要在繁忙的站点上这样做,因为缓存会增加。但实际上,如果您的站点非常繁忙,那么长时间运行的进程无论如何都会耗尽您的线程。
下面是隐藏代码的样子,这是您真正需要的。
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'");
}
}
基于Elmer的例子,我准备了自己的解决方案。单击具有定义的“download”类的项目后,浏览器窗口中将显示一条自定义消息。我用焦点触发器隐藏了消息。我用焦点触发器隐藏了消息。
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>
or
<input class="download" type="submit" value="Download" name="actionType">
每次点击下载后,您将看到消息:
您的报告正在创建中。请稍等…
当按钮/链接被单击时创建一个iframe,并将其附加到body。
$('<iframe />')
.attr('src', url)
.attr('id', 'iframe_download_report')
.hide()
.appendTo('body');
创建一个延迟的iframe,下载后删除。
var triggerDelay = 100;
var cleaningDelay = 20000;
var that = this;
setTimeout(function() {
var frame = $('<iframe style="width:1px; height:1px;" class="multi-download-frame"></iframe>');
frame.attr('src', url + "?" + "Content-Disposition: attachment ; filename=" + that.model.get('fileName'));
$(ev.target).after(frame);
setTimeout(function() {
frame.remove();
}, cleaningDelay);
}, triggerDelay);