Определить, когда браузер получает файл загрузки - PullRequest
447 голосов
/ 10 июля 2009

У меня есть страница, которая позволяет пользователю загружать динамически генерируемый файл. Генерирование занимает много времени, поэтому я хотел бы показать индикатор «ожидания». Проблема в том, что я не могу понять, как определить, когда браузер получил файл, поэтому я могу скрыть индикатор.

Я делаю запрос в скрытой форме, которая отправляет на сервер и нацеливает скрытый iframe для его результатов. Поэтому я не заменяю все окно браузера результатом. Я слушаю событие загрузки в iframe в надежде, что оно сработает после завершения загрузки.

Я возвращаю заголовок «Content-Disposition: attachment» с файлом, в результате чего браузер отображает диалоговое окно «Save». Но браузер не запускает событие загрузки в iframe.

Один из подходов, которые я попробовал, - это использование ответа из нескольких частей. Таким образом, он отправит пустой 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; IE запускает событие «load», но не загружает файл, а Safari загружает файл (с неправильным именем и типом содержимого) и не запускает событие «load».

Другой подход может заключаться в том, чтобы позвонить, чтобы начать создание файла, затем опросить сервер, пока он не будет готов, затем загрузить уже созданный файл. Но я бы предпочел не создавать временные файлы на сервере.

У кого-нибудь есть идея получше?

Ответы [ 17 ]

417 голосов
/ 12 ноября 2010

Один возможное решение использует JavaScript на клиенте.

Клиентский алгоритм:

  1. Создать случайный уникальный токен.
  2. Отправьте запрос на загрузку и включите токен в поле GET / POST.
  3. Показать индикатор ожидания.
  4. Запустите таймер, и каждую секунду или около того ищите файл cookie с именем «fileDownloadToken» (или что-то другое).
  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
    );
}
28 голосов
/ 04 июня 2011

Очень простое (и неудачное) однострочное решение - использовать событие window.onblur(), чтобы закрыть диалог загрузки. Конечно, если это займет слишком много времени и пользователь решит заняться чем-то другим (например, чтением писем), диалоговое окно загрузки закроется.

12 голосов
/ 11 сентября 2010

старая тема, я знаю ...

но те, которые приводятся здесь Google, могут быть заинтересованы в моем решении. это очень просто, но надежно. и это позволяет отображать сообщения о реальном прогрессе (и может быть легко подключен к существующим процессам):

скрипт, который обрабатывает (моя проблема была: получение файлов по 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
    ?>
10 голосов
/ 26 марта 2013

Я использую следующее для загрузки BLOB-объектов и отзыва 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;
}

после того, как диалоговое окно загрузки файла закрыто, окно возвращает ее фокус, так что происходит событие фокусировки.

8 голосов
/ 29 декабря 2014

Я написал простой класс JavaScript, который реализует технику, аналогичную описанной в бредовом ответе . Я надеюсь, что это может быть полезно для кого-то здесь. Проект GitHub называется response-monitor.js

По умолчанию он использует spin.js в качестве индикатора ожидания, но также предоставляет набор обратных вызовов для реализации пользовательского индикатора.

JQuery поддерживается, но не требуется.

Известные функции

  • Простая интеграция
  • Нет зависимостей
  • Плагин JQuery (опционально)
  • Интеграция Spin.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});

Клиент с обратными вызовами (JQuery)

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

//this value is passed to the client through the ResponseMonitor.onResponse callback
$cookieValue = 1; //for ex, "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 в хранилище.

5 голосов
/ 06 ноября 2016

Если вы передаете файл, который вы генерируете динамически, а также внедрили библиотеку обмена сообщениями между серверами в реальном времени, вы можете довольно легко предупредить вашего клиента.

Библиотека обмена сообщениями между серверами, которую я люблю и рекомендую, - Socket.io (через Node.js). После того, как ваш серверный скрипт завершит генерацию файла, который передается для загрузки, ваша последняя строка в этом скрипте может отправить сообщение в Socket.io, которое отправит уведомление клиенту. На клиенте Socket.io прослушивает входящие сообщения, отправленные с сервера, и позволяет вам действовать с ними. Преимущество использования этого метода перед другими заключается в том, что вы можете обнаружить «истинное» событие завершения после завершения потоковой передачи.

Например, вы можете отобразить индикатор занятости после нажатия на ссылку для скачивания, отправить файл в потоковом режиме, отправить сообщение в Socket.io с сервера в последней строке скрипта потоковой передачи, прослушать уведомление клиента на клиенте, получите уведомление и обновите свой интерфейс, скрыв индикатор занятости.

Я понимаю, что большинство людей, читающих ответы на этот вопрос, могут не иметь такого типа установки, но я использовал это точное решение с большим эффектом в своих собственных проектах, и оно прекрасно работает.

Socket.io невероятно прост в установке и использовании. Подробнее: http://socket.io/

5 голосов
/ 15 октября 2014

На примере Элмера я подготовил собственное решение. После щелчка элементов с определенным классом 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>

или

<input class="download" type="submit" value="Download" name="actionType">

После каждой загрузки щелчка вы увидите сообщение ваш отчет создается, пожалуйста, подождите ...

3 голосов
/ 09 декабря 2014

Я очень опоздал на вечеринку, но я поставлю это здесь, если кто-то еще захочет узнать мое решение:

У меня была настоящая борьба с этой точной проблемой, но я нашел жизнеспособное решение, используя iframes (я знаю, я знаю. Это ужасно, но это работает для простой проблемы, которая у меня была)

У меня была html-страница, которая запустила отдельный скрипт php, который сгенерировал файл, а затем загрузил его. На html-странице я использовал следующий jquery в html-заголовке (также необходимо включить библиотеку 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 сначала запускает ваш php-скрипт в iframe. Iframe загружается после создания файла. Затем jquery снова запускает скрипт с переменной запроса, сообщающей скрипту о загрузке файла.

Причина, по которой вы не можете выполнять загрузку и генерацию файлов сразу, связана с функцией php header (). Если вы используете header (), вы изменяете скрипт на что-то отличное от веб-страницы, и jquery никогда не распознает загружаемый скрипт. Я знаю, что это не обязательно обнаруживает, когда браузер получает файл, но ваша проблема звучит похоже на мою.

3 голосов
/ 10 июля 2009

Когда пользователь запускает генерацию файла, вы можете просто назначить уникальный идентификатор для этой «загрузки» и отправить пользователя на страницу, которая обновляется (или проверяется с помощью AJAX) каждые несколько секунд. Как только файл будет завершен, сохраните его под тем же уникальным идентификатором и ...

  • Если файл готов, загрузите его.
  • Если файл не готов, показать прогресс.

Тогда вы можете пропустить весь беспорядок iframe / wait / browserwindow, но при этом получить действительно элегантное решение.

3 голосов
/ 10 июля 2009

Если вы не хотите создавать и хранить файл на сервере, готовы ли вы сохранить статус, например, файл в процессе, файл завершен? Ваша страница ожидания может опросить сервер, чтобы узнать, когда генерация файла завершена. Вы не могли бы знать наверняка, что браузер начал загрузку, но у вас будет некоторая уверенность.

...