我有一个允许用户下载动态生成文件的页面。生成它需要很长时间,所以我想显示一个“等待”指示器。问题是,我不知道如何检测浏览器何时接收到文件,以便我可以隐藏指示器。
我请求一个隐藏的表单,它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”事件。
另一种方法可能是调用启动文件创建,轮询服务器直到准备就绪,然后下载已经创建的文件。但我宁愿避免在服务器上创建临时文件。
我该怎么办?
一种可能的解决方案是在客户机上使用JavaScript。
客户端算法:
生成一个随机的唯一令牌。
提交下载请求,并在GET/POST字段中包含令牌。
显示“等待”指示灯。
启动一个计时器,每隔一秒左右,查找一个名为“fileDownloadToken”的cookie(或任何您决定的cookie)。
如果cookie存在,并且它的值与令牌匹配,则隐藏“等待”指示器。
服务器算法:
在请求中查找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
);
}
我写了一个简单的JavaScript类,实现了一种类似于bulltorious回答中描述的技术。我希望它能对在座的人有用。
GitHub项目被称为response-monitor.js。
默认情况下,它使用spin.js作为等待指示器,但它也为自定义指示器的实现提供了一组回调。
jQuery是支持的,但不是必需的。
显著的特征
简单的集成
没有依赖关系
jQuery插件(可选)
js集成(可选)
用于监视事件的可配置回调
同时处理多个请求
服务器端错误检测
超时检测
跨浏览器
示例使用
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});
带有回调函数的客户端
// 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
有关更多示例,请检查存储库中的examples文件夹。
我更新了下面的参考代码添加一个正确的下载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
参考:XMLHttpRequest:进度事件,实时示例
这个解决方案非常简单,但很可靠。并且它可以显示真实的进度消息(并且可以轻松地插入到现有流程中):
处理的脚本(我的问题是:通过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
?>