检测浏览器何时接收到文件下载

我有一个允许用户下载动态生成文件的页面。它需要很长时间来生成,所以我想显示一个“等待”;指标。问题是,我不知道如何检测浏览器何时接收到文件,以便我可以隐藏指示器。

我向服务器请求一个隐藏的表单(的帖子),并以一个隐藏的iframe为目标获取其结果。这样我就不会用结果替换整个浏览器窗口。我听着“load”;事件,希望它将在下载完成时触发。

我返回一个"Content-Disposition: attachment"头文件,这将导致浏览器显示“保存”;对话框。但是浏览器不会触发“加载”;事件。

我尝试的一种方法是使用multi-part响应。所以它会发送一个空的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"对话框。但它失败在Internet ExplorerSafari;Internet Explorer会触发“加载”;事件,但它不下载文件,并Safari浏览器下载文件(有错误的名称和内容类型),并且不触发“load"事件。

另一种方法可能是调用启动文件创建,轮询服务器直到准备就绪,然后下载已经创建的文件。但我宁愿避免在服务器上创建临时文件。

我该怎么办?

409120 次浏览

如果您下载了一个文件,该文件是保存的,而不是在文档中,则无法确定下载何时完成,因为它不在当前文档的范围内,而是浏览器中的一个单独进程。

当用户触发文件的生成时,您可以简单地为“下载”分配一个唯一的ID,并将用户发送到每隔几秒钟刷新(或使用AJAX检查)的页面。一旦文件完成,将其保存在相同的唯一ID和…

  • 如果文件已经准备好,请进行下载。
  • 如果文件尚未准备好,请显示进度。

然后你可以跳过整个iframe/waiting/browserwindow的混乱,而有一个真正优雅的解决方案。

如果你不想在服务器上生成和存储文件,你是否愿意存储状态,例如文件进行中,文件完成?您的“等待”页面可以轮询服务器以了解文件生成何时完成。您不能确定是浏览器启动了下载,但您有一定的信心。

我也遇到过同样的问题。我的解决方案是使用临时文件,因为我已经生成了一堆临时文件。提交表格时:

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上的load事件被触发。然后关闭等待消息,然后开始文件下载。它在Internet Explorer 7和Firefox上进行了测试。

问题是在生成文件时有一个“等待”指示器,然后在文件下载后恢复正常。我喜欢这样做的方式是使用一个隐藏的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'");
}
}

这个解决方案非常简单,但很可靠。并且它可以显示真实的进度消息(并且可以轻松地插入到现有流程中):

处理的脚本(我的问题是:通过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>

文件# EYZ0

<?php
session_start();
echo json_encode($_SESSION['downloadstatus']);
?>

文件# EYZ0

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

一个可能的解决方案在客户端使用JavaScript。

客户端算法:

  1. 生成一个随机的唯一令牌。
  2. 提交下载请求,并在GET/POST字段中包含令牌。
  3. 显示“等待”指示灯。
  4. 启动一个计时器,每隔一秒左右,查找一个名为“fileDownloadToken”的cookie(或任何您决定的cookie)。
  5. 如果cookie存在,并且它的值与令牌匹配,则隐藏“等待”指示器。

服务器算法:

  1. 在请求中查找GET/POST字段。
  2. 如果它有一个非空值,删除一个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
);
}

一个非常简单(并且蹩脚)的一行解决方案是使用window.onblur()事件关闭加载对话框。当然,如果它花了太长时间,用户决定做其他事情(如阅读电子邮件),加载对话框将关闭。

我使用以下代码下载blobs并在下载后撤销对象URL。它在Chrome和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;
}

关闭文件下载对话框后,窗口将获得其焦点,因此将触发焦点事件。

当按钮/链接被单击时创建一个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);

基于埃尔默的例子,我准备了自己的解决方案。在点击一个带有“下载" 下载"类时,自定义消息将显示在浏览器窗口中。我使用焦点触发器隐藏消息。我使用了焦点触发器来隐藏消息。

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

超文本标记语言

<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">
在每一次下载点击后,你将看到消息:
您的报告正在创建中。请等待…< / em >

我在这个问题上有一个真正的斗争,但我发现了一个使用iframes的可行解决方案。这很糟糕,但它适用于我遇到的一个简单问题。)

我有一个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使用请求变量再次启动脚本,告诉脚本下载文件。

不能一次性完成下载和文件生成的原因是php header()函数。如果你使用header(),你正在将脚本更改为网页以外的内容,jQuery将永远不会将下载脚本识别为“已加载”。我知道这可能并不一定是检测浏览器何时接收文件,但您的问题听起来与我的类似。

我编写了一个简单的JavaScript类,实现了类似于bulltorious的回答中描述的技术。我希望它能对在座的人有用。

GitHub项目名为response-monitor.js

默认情况下,它使用spin.js作为等待指示器,但它也提供了一组用于实现自定义指示器的回调。

jQuery是支持的,但不是必需的。

显著的特征

  • 简单的集成
  • 没有依赖关系
  • jQuery插件(可选)
  • js集成(可选)
  • 用于监视事件的可配置回调
  • 同时处理多个请求
  • 服务器端错误检测
  • 超时检测
  • 跨浏览器

示例使用

超文本标记语言

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

带有回调函数的客户端

// 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; // Example: response-monitor_1419642741528


// This value is passed to the client through the ResponseMonitor.onResponse callback
$cookieValue = 1; // For example, "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

有关更多示例,请检查存储库中的示例文件夹

如果您正在流式处理动态生成的文件,并且还实现了实时的服务器到客户端消息库,那么您可以非常容易地提醒客户端。

我喜欢并推荐的服务器到客户端消息传递库是Socket。io(通过Node.js)。服务器脚本生成供下载的流文件后,脚本中的最后一行可以向Socket发出消息。IO,它向客户端发送通知。在客户端,Socket。IO监听从服务器发出的传入消息,并允许您对它们进行操作。与其他方法相比,使用此方法的好处是,您能够在流处理完成后检测“真正的”完成事件。

例如,您可以在单击下载链接后显示繁忙指示器,流式传输文件,向Socket发出消息。在您的流脚本的最后一行中,从服务器中输入io,在客户端侦听通知,接收通知并通过隐藏busy指示器更新您的UI。

我知道大多数读到这个问题答案的人可能没有这种类型的设置,但我已经在我自己的项目中使用了这种确切的解决方案,而且效果非常好。

套接字。IO非常容易安装和使用。更多信息:http://socket.io/

如果您只想在显示下载对话框之前显示消息或加载器GIF图像,那么一个快速的解决方案是将消息放在一个隐藏的容器中,当您单击生成要下载的文件的按钮时,您将使容器可见。

然后使用jQuery或JavaScript捕获按钮的focusout事件来隐藏包含消息的容器。

如果带blob的XMLHttpRequest不是一个选项,那么你可以在一个新窗口中打开你的文件,并检查是否有任何元素在该窗口体中以间隔填充。

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 an error page configured in your application
// for failed requests, check for those DOM elements.
}
}, 1000)
// Better if you specify the maximum number of intervals

我在这个配置中遇到了同样的问题:

我用饼干的解决方案:

客户端:

在提交表单时,调用JavaScript函数隐藏页面并加载等待的旋转器

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

然后,调用一个函数,每500毫秒检查一次cookie是否来自服务器。

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

如果找到cookie,停止每500毫秒检查一次,使cookie过期,并调用函数返回页面并删除等待的旋转器(removeWaitingSpinner ())。如果您希望能够再次下载另一个文件,过期cookie是很重要的!

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将被发送到客户端。

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

根据我的经验,有两种方法可以解决这个问题:

  1. 在下载时设置一个短期cookie,并让JavaScript不断检查它的存在。唯一真正的问题是让cookie的生命周期正确-太短,JavaScript可能会错过它,太长,它可能会取消其他下载的下载屏幕。使用JavaScript在发现cookie时删除cookie通常可以解决这个问题。
  2. 使用fetch/XHR下载文件。您不仅确切地知道文件下载何时完成,如果您使用XHR,您还可以使用进度事件来显示进度条!保存结果blob与msSaveBlobInternet Explorer边缘和下载链接(比如这个)在Firefox和Chrome。这个方法的问题是iOS Safari似乎不能正确地处理下载blob -你可以用FileReader将blob转换为数据URL并在一个新窗口中打开它,但这是打开文件,而不是保存它。

这个Java/春天示例检测下载的结束,在这一点上它隐藏了&;指标。

在JavaScript方面,设置饼干,最大过期时间为2分钟,并每秒轮询cookie 过期。然后服务器端用早些时候过期时间覆盖这个cookie——服务器进程的完成。一旦在JavaScript轮询中检测到cookie过期,“加载…”;是隐藏的。

JavaScript的一面

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 JavaScript 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 JavaScript timer detects the expired cookie earlier than 2 min.
// (coming from the server), or the initial JavaScript-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 the 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 a timer to check for the 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 JavaScript 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服务器端

    @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 JavaScript-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 seconds for any flushing concerns
myCookie.setPath("/");
response.addCookie(myCookie);
//... -- presumably the download is writing to the Output Stream...
return null;
}

PrimeFaces也使用cookie轮询。

# EYZ0:

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

来自其他地方的有效解决方案:

/**
*  download file, show modal
*
* @param uri link
* @param name file name
*/
function downloadURI(uri, name) {
// <------------------------------------------       Do something (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'));
}

你可以使用它:

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


核心问题是web浏览器没有在页面导航被取消时触发事件,但有一个在页面完成加载时触发的事件。任何外部的直接浏览器事件都将是一个优点和缺点的黑客。

有四种已知的方法来处理检测浏览器下载何时开始:

  1. 调用fetch(),检索整个响应,附加带有download属性的a标记,并触发单击事件。现代网络浏览器将为用户提供保存已经检索到的文件的选项。这种方法有几个缺点:
  • 整个数据团存储在RAM中,因此如果文件很大,它将消耗同样多的RAM。对于小文件,这可能不是问题。
  • 用户必须等待整个文件下载完成后才能保存。他们也不能离开页面,直到它完成。
  • 未使用内置的web浏览器文件下载器。
  • 除非设置了CORS报头,否则跨域获取可能会失败。
  1. 使用iframe +服务器端cookie。如果页面在iframe中加载,而不是开始下载,iframe将触发load事件,但如果下载开始,它不会触发任何事件。在web服务器上设置cookie可以被JavaScript循环检测到。这种方法有几个缺点:
  • 服务器和客户机必须协同工作。服务器必须设置cookie。客户端必须检测cookie。
  • 跨域请求将无法设置cookie。
  • 每个域可以设置多少个cookie是有限制的。
  • 不能发送自定义HTTP报头。
  1. 使用带有URL重定向的iframe。iframe启动一个请求,一旦服务器准备好文件,它将转储一个HTML文档,该文档执行元刷新到一个新的URL,这将在1秒后触发下载。iframe上的load事件发生在HTML文档加载时。这种方法有几个缺点:
  • 服务器必须维护所下载内容的存储。需要cron作业或类似作业来定期清理目录。
  • 当文件准备好时,服务器必须转储特殊的HTML内容。
  • 在从DOM中删除iframe之前,客户端必须猜测iframe何时实际向服务器发出了第二个请求,以及下载实际何时开始。这可以通过将iframe留在DOM中来解决。
  • 不能发送自定义HTTP报头。
  1. 使用iframe + XHR。iframe触发下载请求。一旦通过iframe发出请求,就会通过XHR发出相同的请求。如果iframe上的load事件触发,则表示发生了错误,请中止XHR请求,并删除iframe。如果触发xhr# EYZ1事件,则iframe中可能已经开始下载,请中止XHR请求,等待几秒钟,然后删除iframe。这样就可以在不依赖服务器端cookie的情况下下载更大的文件。这种方法有几个缺点:
  • 对于相同的信息有两个不同的请求。服务器可以通过检查传入的报头来区分XHR和iframe。
  • 除非设置了歌珥报头,否则跨域XHR请求可能会失败。但是,直到服务器发送回HTTP报头,浏览器才知道CORS是否被允许。如果服务器等待发送报头,直到文件数据准备就绪,那么即使没有CORS, XHR也可以大致检测到iframe何时开始下载。
  • 客户端必须猜测下载何时开始从DOM中删除iframe。这可以通过将iframe留在DOM中来解决。
  • 不能在iframe上发送自定义报头。

如果没有适当的内置web浏览器事件,这里就没有任何完美的解决方案。然而,根据您的用例,上述四种方法中的一种可能比其他方法更适合。

只要可能,动态地将响应流发送到客户端,而不是先在服务器上生成所有内容,然后再发送响应。各种文件格式可以流,如CSV, JSON, XML, 邮政编码等。这真的取决于找到一个支持流媒体内容的库。当请求一开始就流化响应时,检测下载的开始并不重要,因为它几乎马上就开始了。

另一种选择是预先输出下载标题,而不是等待所有内容先生成。然后生成内容,最后开始发送到客户端。用户内置的下载程序将耐心等待数据到达。缺点是底层网络连接可能会超时等待数据开始流动(无论是在客户端还是服务器端)。

我更新了下面的参考代码添加一个正确的下载URL链接并尝试一下。

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style type="text/css">
body {
padding: 0;
margin: 0;
}


svg:not(:root) {
display: block;
}


.playable-code {
background-color: #F4F7F8;
border: none;
border-left: 6px solid #558ABB;
border-width: medium medium medium 6px;
color: #4D4E53;
height: 100px;
width: 90%;
padding: 10px 10px 0;
}


.playable-canvas {
border: 1px solid #4D4E53;
border-radius: 2px;
}


.playable-buttons {
text-align: right;
width: 90%;
padding: 5px 10px 5px 26px;
}
</style>


<style type="text/css">
.event-log {
width: 25rem;
height: 4rem;
border: 1px solid black;
margin: .5rem;
padding: .2rem;
}


input {
width: 11rem;
margin: .5rem;
}


</style>


<title>XMLHttpRequest: progress event - Live_example - code sample</title>
</head>


<body>
<div class="controls">
<input class="xhr success" type="button" name="xhr" value="Click to start XHR (success)" />
<input class="xhr error" type="button" name="xhr" value="Click to start XHR (error)" />
<input class="xhr abort" type="button" name="xhr" value="Click to start XHR (abort)" />
</div>


<textarea readonly class="event-log"></textarea>


<script>
const xhrButtonSuccess = document.querySelector('.xhr.success');
const xhrButtonError = document.querySelector('.xhr.error');
const xhrButtonAbort = document.querySelector('.xhr.abort');
const log = document.querySelector('.event-log');


function handleEvent(e) {
if (e.type == 'progress')
{
log.textContent = log.textContent + `${e.type}: ${e.loaded} bytes transferred Received ${event.loaded} of ${event.total}\n`;
}
else if (e.type == 'loadstart')
{
log.textContent = log.textContent + `${e.type}: started\n`;
}
else if  (e.type == 'error')
{
log.textContent = log.textContent + `${e.type}: error\n`;
}
else if (e.type == 'loadend')
{
log.textContent = log.textContent + `${e.type}: completed\n`;
}
}


function addListeners(xhr) {
xhr.addEventListener('loadstart', handleEvent);
xhr.addEventListener('load', handleEvent);
xhr.addEventListener('loadend', handleEvent);
xhr.addEventListener('progress', handleEvent);
xhr.addEventListener('error', handleEvent);
xhr.addEventListener('abort', handleEvent);
}


function runXHR(url) {
log.textContent = '';


const xhr = new XMLHttpRequest();


var request = new XMLHttpRequest();
addListeners(request);
request.open('GET', url, true);
request.responseType = 'blob';
request.onload = function (e) {
var data = request.response;
var blobUrl = window.URL.createObjectURL(data);
var downloadLink = document.createElement('a');
downloadLink.href = blobUrl;
downloadLink.download = 'download.zip';
downloadLink.click();
};
request.send();
return request
}


xhrButtonSuccess.addEventListener('click', () => {
runXHR('https://abbbbbc.com/download.zip');
});


xhrButtonError.addEventListener('click', () => {
runXHR('http://i-dont-exist');
});


xhrButtonAbort.addEventListener('click', () => {
runXHR('https://raw.githubusercontent.com/mdn/content/main/files/en-us/_wikihistory.json').abort();
});
</script>


</body>
</html>


Return to post

参考:# EYZ0

您可以依赖浏览器的缓存,并在文件加载到缓存时触发同一文件的第二次下载。

$('#link').click(function(e) {
e.preventDefault();


var url = $(this).attr('href');
var request = new XMLHttpRequest();
request.responseType = "blob";
request.open("GET", url);


var self = this;
request.onreadystatechange = function () {
if (request.readyState === 4) {
var file = $(self).data('file');
var anchor = document.createElement('a');
anchor.download = file;
console.log(file);
console.log(request);
anchor.href = window.URL.createObjectURL(request.response);
anchor.click();
console.log('Completed. Download window popped up.');
}
};
request.send();
});