Каков наилучший способ показать прогресс в вызове Ajax?

У меня есть вызов Ajax, который обновляет 5000 записей в базе данных, поэтому это занимает много времени. У меня есть Ajax "Загрузка изображения", показывающий, что что-то происходит, но я ищу лучший способ показать "Обновление 50 из 5000...", "Обновление 200 из 5000" или что-то в этом роде.

Каков наилучший способ сделать что-то подобное в Ajax/jQuery, не делая 5000 разных сообщений?

Ответ 1

Лучшее, что я думаю, использует Comet.

В приложениях типа Comet сервер может по существу передавать данные клиенту (вместо данных запроса клиента с сервера снова и снова). Клиент должен только подключиться к серверу один раз. а затем сервер будет продолжать возвращать данные клиенту.

Материал из Википедии:

Комета - это метод программирования, который позволяет веб-серверам отправлять данные клиенту, не требуя от клиента запроса. Это позволяет создавать управляемые событиями веб-приложения, размещенные в браузере.

А теперь посмотрим, как работает комета. См. Следующий серверный код. здесь используется цикл while, вместо этого вы можете установить собственное условие. В цикле while страница записывает дату и время, а затем спит в течение 1/2 секунд.

Код страницы ASP.NET: Service.aspx.cs

public static string Delimiter = "|";

protected void Page_Load(object sender, EventArgs e)
{
    Response.Buffer = false;

    while (true)
    {
        Response.Write(Delimiter
            + DateTime.Now.ToString("HH:mm:ss.FFF"));
        Response.Flush();

        // Suspend the thread for 1/2 a second
        System.Threading.Thread.Sleep(500);
    }

    // Yes I know we'll never get here,
    // it just hard not to include it!
    Response.End();
}

Клиентский код - чистый JavaScript

Сделайте запрос только один раз, а затем продолжайте проверять данные в readyState === 3 XMLHttpRequest.

function getData()
{
    loadXMLDoc("Service.aspx");
}

var req = false;
function createRequest() {
    req = new XMLHttpRequest(); // http://msdn.microsoft.com/en-us/library/ms535874%28v=vs.85%29.aspx
}

function loadXMLDoc(url) {
    try {
        if (req) { req.abort(); req = false; }

        createRequest();

        if (req) {
            req.onreadystatechange = processReqChange;
            req.open("GET", url, true);
            req.send("");
        }
        else { alert('unable to create request'); }
    }
    catch (e) { alert(e.message); }
}

function processReqChange() {
    if (req.readyState == 3) {
        try {
            ProcessInput(req.responseText);

            // At some (artibrary) length   recycle the connection
            if (req.responseText.length > 3000) { lastDelimiterPosition = -1; getData(); }
        }
        catch (e) { alert(e.message); }
    }
}

var lastDelimiterPosition = -1;
function ProcessInput(input) {
    // Make a copy of the input
    var text = input;

    // Search for the last instance of the delimiter
    var nextDelimiter = text.indexOf('|', lastDelimiterPosition + 1);
    if (nextDelimiter != -1) {

        // Pull out the latest message
        var timeStamp = text.substring(nextDelimiter + 1);
        if (timeStamp.length > 0) {
            lastDelimiterPosition = nextDelimiter;
            document.getElementById('outputZone').innerHTML = timeStamp;
        }
    }
}

window.onload = function () { getData(); };

Ссылка

Ответ 2

Я бы позволил функции, которая делает большую запись обновления в переменной SESSION, ее текущий прогресс после каждого отдельного (или так много) обновления и использует отдельный AJAX script для извлечения этого значения прогресса из СЕССИИ и пусть JavaScript использует это, чтобы обновить индикатор выполнения/текст.

Ответ 3

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

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

var progressCallback = function(data){
  //update progress dialog with data
  //if data does not indicate completion
    //ajax call status function with "progressCallback" as callback
});
//ajax call status function with "progressCallback" as callback

Ответ 4

Я хотел бы запустить обратный вызов Ajax через каждые миллисекунды, которые могут запросить, сколько сделано (например, количество обновленных записей), и использовать это для отображения индикатора выполнения. Что-то вроде способа this работает.

Ответ 5

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

Но у вас может возникнуть проблема с чтением запроса до его завершения через xhttpr. Возможно, вы сможете сделать свой запрос через iframe и выполнить эту загрузку через "HTTP-поток".

Но даже это может быть отрывочным. HTTP не предназначен для передачи вещей по частям/фрагментарным. Как и другие, лучше всего сделать отдельные последующие вызовы, чтобы получить статус операции.

Ответ 6

У меня появилась идея при чтении ответов.

Файлы cookie для JavaScript и PHP,

  • Создайте cookie с JavaScript при вызове Ajax.
  • В файле Ajax PHP увеличивайте значение в этом файле cookie с каждым обновлением SQL.
  • В JavaScript рекурсивная функция будет считывать этот конкретный файл cookie и будет обновлять номера индикаторов выполнения.

Преимущества:

  • Только 1 вызов Ajax.
  • Меньшая загрузка на сервер.

Ответ 7

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

Если это так, просто сделайте ajax-вызов каждые 200 или около того итераций. Если вы сделаете это для каждой группы из 200 записей, она будет потреблять только 50 вызовов Ajax.

Что-то вроде (псевдокод):

If iterationNumber mod 200 == 0
    // Make Ajax call.

Ответ 8

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

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

В коде на стороне сервера сделайте куски обновлений (скажем, 100 за итерацию) и сгенерируйте вывод. Вывод javascript-запроса для каждой итерации:

<?php
  while () { // Whatever your loop needs to be.
  // Do your chunk of updates here.
?>
   <script type="text/javascript">
     setProgress(100);
   </script>
<?php
    flush(); // Send what we have so far to the browser so the script is executed.
  }
  setProgress(5000); // All done!
?>

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

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

Ответ 9

Чтобы показать прогресс во время загрузки, я бы изменил свой backend так, чтобы он мог выполнять выборочную загрузку.

Например,

var total_rec = 5000;
var load_steps = 20;
var per_load = total_rev / load_steps;
var loaded = 0; 
while (loaded < total_rec) {
    www.foobar.com/api.php?start=loaded&end=(loaded+per_load);
    loaded += per_load;
}

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

Альтернативный способ изменения бэкэнд может быть

www.foobar.com/api.php?start=loaded&count=50

Ответ 10

Что-то кажется подозрительным.

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

Рассмотрим систему, которая позволяет пользователю загружать 5000 записей и не указывать, какие из них были отредактированы, а затем в конце создания обновлений применяется одна кнопка обновления, которая потребовала бы, чтобы все 5000 записей были переданы каким-либо образом. Это будет худший случай.

Таким образом, может быть какой-то способ разделить проблему таким образом, что нет времени ожидания. Рассмотрим, например, временную таблицу db (или просто в памяти приложений, довольно просто, просто создав список объектов ORM... но это в стороне), тогда, когда приходит время для фиксации этих обновлений, их можно, по крайней мере, сделать в без необходимости передачи клиент-сервер. Может быть даже возможно отметить отдельные поля, которые были отредактированы, поэтому ничего не обновляется в БД, кроме того, что изменилось.

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

Ответ 11

Создайте простую таблицу следующим образом:

CREATE TABLE progress_data (
  statusId int(4) NOT NULL AUTO_INCREMENT,
  progress float DEFAULT NULL COMMENT 'percentage',
  PRIMARY KEY (id_progress_data)
);

Код JQuery:

//this uses Jquery Timers http://plugins.jquery.com/project/timers
$('#bUpdate').click(function() {
    //first obtain a unique ID of this operation - this has to by synchronized
    $.ajaxSetup({'async': false});
    $.post('ajax.php', {'operation': 'beginOperation'}, function(data) {
        statusId = parseInt(data.statusId);
    });
    //now run the long-running task with the operation ID and other params as necessary
    $.ajaxSetup({'async': true});
    $.post('ajax.php', {'operation': 'updateSite', 'statusId': statusId, 'param': paramValue}, function(data) {
        $('#progress_bar').stopTime('statusLog'); //long operation is finished - stop the timer
        if (data.result) {
            //operation probably successful
        } else {
            //operation failed
        }
    });
    //query for progress every 4s, 'statusLog' is just the name of the timer
    $('#progress_bar').everyTime('4s', 'statusLog', function() {
        var elm = $(this);
        $.post('ajax.php', {'operation': 'showLog', 'statusId': statusId}, function(data) {
            if (data) {
                //set bar percentage
                $('#progress').css('width', parseInt(data.progress) + '%');
            }
        });
    });
    return false;
}

Внутренний код (в PHP):

if (isset($_POST['operation'])) {
        ini_set("display_errors", false);
        session_write_close();  //otherwise requests would block each other
        switch ($_POST['operation']) {
            /**
            * Initialize progress operation, acquire ID (statusId) of that operation and pass it back to
            *   JS frontend. The frontend then sends the statusId back to get current state of progress of
            * a given operation.
            */
            case 'beginOperation': {
                $statusId = //insert into progress_data
                echo json_encode(array('statusId' => $statusId));
                break;
            }
            /**
            * Return back current progress state.
            */
            case 'showLog': {
                $result->progress = (float) //SELECT progress FROM progress_data WHERE statusId = $_POST['statusId']
                echo json_encode($result);
                break;
            }
            case 'updateSite': {
                //start long running operation, return whatever you want to, during the operation ocassionally do:
                    UPDATE progress_data SET progress=... WHERE statusId = $_POST['statusId']
            }
        }
    }
    /* Terminate script, since this 'view' has no template, there si nothing to display.
    */
    exit;

Я уже использовал этот подход в 3 приложениях, и я должен сказать, что он очень надежный и быстрый enogh (операция showLog - просто простая инструкция SELECT). Также можно использовать сеанс для сохранения прогресса, но это создает массу проблем, поскольку сеанс должен быть закрыт для записи (если он хранится в файлах), в противном случае запросы showLog AJAX будут ждать завершения длинной операции (и свободный смысл).

Ответ 12

Я когда-то делал что-то подобное (похоже на Zain Shaikh, но проще):

На сервере

int toUpdate = 5000;  
int updated = 0;  
int prev = updated;
while(updated < toUpdate)  
{  
    updated = getAlreadyUpdatedRows();
    flushToClient(generateZeroSequenceOfLength(updated - prev));  
    prev = updated;  
    sleep(500);  
}  
closeStream();

На клиенте
Следуйте по пути Zain Shaikh, но на ProcessInput просто измените размер панели выполнения в соответствии с соотношением количества записей для обновления и input.length.

Это решение часто обрабатывает сложность клиентской стороны для пропускной способности сети.

Не смешивайте активность этого сервера с конечным вводом файла, если вы действительно не знаете, что делаете. Вы будете торговать db-запросами (выбор для подсчета обновленных строк) для стабильности: что, если пользователь меняет страницу? Нет проблем для индикатора выполнения, но импорт не будет завершен.