我有一个允许用户下载动态生成文件的页面。生成它需要很长时间,所以我想显示一个“等待”指示器。问题是,我不知道如何检测浏览器何时接收到文件,以便我可以隐藏指示器。
我请求一个隐藏的表单,它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”事件。
另一种方法可能是调用启动文件创建,轮询服务器直到准备就绪,然后下载已经创建的文件。但我宁愿避免在服务器上创建临时文件。
我该怎么办?
我在这个配置中遇到了同样的问题:
Struts 1.2.9
jQuery 1.3.2。
jQuery UI 1.7.1.custom
Internet Explorer 11
Java 5
我用饼干的解决方案:
客户端:
在提交表单时,调用JavaScript函数隐藏页面并加载等待的旋转器
function loadWaitingSpinner() {
... hide your page and show your spinner ...
}
然后,调用一个函数,每500毫秒检查一次cookie是否来自服务器。
function checkCookie() {
var verif = setInterval(isWaitingCookie, 500, verif);
}
如果找到cookie,停止每500毫秒检查一次,使cookie过期并调用函数返回页面并删除等待spinner (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);
我更新了下面的参考代码添加一个正确的下载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:进度事件,实时示例
这个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;
}
当按钮/链接被单击时创建一个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);