我有一个允许用户下载动态生成文件的页面。生成它需要很长时间,所以我想显示一个“等待”指示器。问题是,我不知道如何检测浏览器何时接收到文件,以便我可以隐藏指示器。
我请求一个隐藏的表单,它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”事件。
另一种方法可能是调用启动文件创建,轮询服务器直到准备就绪,然后下载已经创建的文件。但我宁愿避免在服务器上创建临时文件。
我该怎么办?
这个Java/Spring示例检测下载的结束,在这一点上它隐藏了“Loading…”指示符。
方法:在JavaScript方面,设置一个最大过期时间为2分钟的cookie,并每秒钟轮询一次cookie过期时间。然后服务器端用更早的过期时间覆盖这个cookie——服务器进程的完成。一旦在JavaScript轮询中检测到cookie过期,“Loading…”就会被隐藏。
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;
}
我写了一个简单的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文件夹。
我使用以下代码下载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;
}
关闭文件下载对话框后,窗口将获得其焦点,因此将触发焦点事件。