¿Cuál es la mejor manera de mostrar el progreso en una llamada Ajax?

Tengo una llamada Ajax que actualiza 5.000 registros en una base de datos, por lo que esto lleva mucho tiempo. Tengo una “Imagen de carga” de Ajax que muestra que algo está sucediendo, pero estoy buscando una forma mejor de mostrar “Actualizando 50 de 5000 …”, “Actualizando 200 de 5000”, o algo así.

¿Cuál es la mejor manera de hacer algo como esto en Ajax / jQuery sin hacer 5000 publicaciones diferentes?

Lo mejor que creo es usar Comet .

En las aplicaciones estilo Comet, el servidor esencialmente puede enviar datos al cliente (en lugar de los datos de solicitud del cliente desde el servidor una y otra vez). El cliente solo necesita conectarse al servidor una vez . y luego el servidor seguirá enviando datos al cliente.

De la Wikipedia:

Comet es una técnica de progtwigción que permite a los servidores web enviar datos al cliente sin necesidad de que el cliente lo solicite. Permite la creación de aplicaciones web impulsadas por eventos alojadas en el navegador.

Y ahora veamos cómo funciona Comet. Vea el siguiente código del lado del servidor. aquí se usa un ciclo while, en su lugar puede establecer su propia condición. En el ciclo while, la página escribe un datetime y se vacía y luego duerme durante 1/2 segundo.

Código de página ASP.NET detrás: 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's just hard not to include it! Response.End(); } 

Código del lado del cliente – JavaScript puro

readyState === 3 la solicitud solo una vez y luego siga verificando los datos en el readyState === 3 de 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(); }; 

Referencia

Dejaría que la función que está haciendo el gran registro de actualización en una variable SESSION sea su progreso actual después de cada actualización (y tantas), y utilice un script AJAX separado para recuperar este valor de progreso de SESSION y permita que JavaScript lo use para actualizar su barra de progreso / texto.

Supongo que actualmente está utilizando un POST para todos los registros en la actualización del lote y colocando la imagen de carga entre llamada y devolución.

En lugar de esperar a que el servidor regrese hasta completar la actualización, hágalo regresar inmediatamente con una identificación especial para esa actualización de lote. Luego, implemente una llamada al servidor que devuelva el estado de la actualización del lote, que su diálogo de progreso puede llamar para informar el progreso.

 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 

Lanzaría una callback Ajax una vez cada milisegundos que pueda consultar cuánto se hace (por ejemplo, el número de registros actualizados) y lo usaré para mostrar una barra de progreso. Algo así como la forma en que esto funciona

Me acabo de dar una idea al leer las respuestas.

JavaScript y PHP comparten cookies,

  1. Crea una cookie con JavaScript cuando se realiza una llamada Ajax.
  2. En un archivo PHP de Ajax, incremente el valor en esa cookie con cada actualización de SQL.
  3. En JavaScript, una función recursiva leerá esa cookie en particular y actualizará los números de la barra de progreso.

Ventajas:

  1. Solo 1 llamada Ajax.
  2. Menos carga en el servidor.

Puede actualizar el búfer de respuesta con un progreso, vaciando periódicamente su búfer de respuesta del servidor.

Pero puede tener problemas para leer una solicitud antes de que se complete a través de xhttpr . Es posible que pueda realizar su solicitud a través de un iframe y tener esa carga en progreso a través de ‘http streaming’.

Pero incluso eso puede ser incompleto. HTTP no tiene la intención de transferir cosas por partes / fragmentadas. Como otros señalan, sería mejor hacer llamadas subsecuentes por separado para obtener el estado de la operación.

Supongo que tiene una razón para iterar a través de cada registro individualmente, en lugar de simplemente ejecutar una statement SQL.

Si ese es el caso, simplemente haga una llamada ajax cada 200 o más iteraciones. Si lo haces para cada grupo de 200 registros, solo consumirá 50 llamadas Ajax.

Algo así como (pseudocódigo):

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

No estoy seguro de cuál es el lado del servidor donde está publicando, pero debería poder aplicar este método a la mayoría de los lenguajes de progtwigción. Estoy usando PHP como un ejemplo.

En el lado HTML, tiene alguna función que actualiza la barra de progreso a un ancho determinado. Llamo a esta función ‘setProgress’ para este ejemplo, que toma un número para actualizar la barra de progreso.

En el código del lado del servidor, haga trozos de actualizaciones (digamos 100 por iteración) y genere resultados. Al generar una llamada de JavaScript para cada iteración:

    

Después de repetir esto, limpie el buffer de salida para asegurarse de que estos datos se envíen al navegador. Debido a que es una etiqueta de secuencia de comandos completa, el navegador ejecutará el javascript dentro, actualizando la barra de progreso al valor que le haya pasado.

Para que todo funcione, deberá agregar algunos cálculos para dar sentido a los números que está llamando a la barra de progreso, pero asumo que eso no debería ser un gran problema. Probablemente tenga más sentido establecer SetProgress en los porcentajes de uso del ejemplo, aunque quería mantenerme alejado de los cálculos necesarios para mayor claridad.

Algo parece sospechoso

No estoy familiarizado con las bases de datos que no pueden actualizar 5000 registros en un instante … ¿Así que esto no es solo una actualización que está aplicando, sino un registro por actualización de registro?

Considere un sistema que permite a un usuario descargar 5000 entradas y no marca cuáles han sido editadas y luego al hacer actualizaciones aplica un único botón de actualización que requeriría que los 5000 registros se transmitieran de alguna manera. Ese sería el peor caso.

Entonces, puede haber alguna forma de dividir el problema de forma tal que no haya tiempo de espera. Considere, por ejemplo, una tabla db temporal (o simplemente en la memoria de las aplicaciones, bastante fácil simplemente creando una lista de entidades ORM … pero eso es un lado), cuando llega el momento de comprometer esas actualizaciones, al menos pueden hacerse en una lote sin requerir ninguna transferencia cliente / servidor. Incluso puede ser posible marcar los campos individuales que fueron editados para que no haya nada más actualizado en el DB, excepto exactamente qué cambió.

Hay procesos que se ejecutan a largo plazo y sé que a veces estás atrapado al tener que usar lo que alguien te ha proporcionado … pero quizás con un poco de pensamiento simplemente puedes deshacerte del tiempo de espera.

Crea una tabla simple como esta:

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

Código 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; } 

Código de backend (en 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; 

Ya he usado este enfoque en 3 aplicaciones y debo decir que es muy confiable y rápido enogh (la operación showLog es solo una simple instrucción SELECT). También es posible usar la sesión para almacenar el progreso, pero eso trae muchos problemas, ya que la sesión debe escribirse cerrada (si está almacenada en archivos), de lo contrario, las consultas showLog AJAX esperarán a que finalice la operación larga. (y sentido flojo).

Una vez hice algo similar de esta manera (similar a Zain Shaikh , pero más simple):

En el servidor

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

En el cliente
Siga la ruta Zain Shaikh , pero en ProcessInput simplemente ProcessInput tamaño de su barra de progreso de acuerdo con la relación entre el número de registros para actualizar y la input.length .

Esta solución a menudo intercambia la complejidad del lado del cliente por el ancho de banda de la red.

No mezcle esta actividad de servidor con una importación de archivo eventual, a menos que realmente sepa lo que está haciendo. Usted estaría intercambiando consultas de DB (seleccione para contar las filas actualizadas) para la estabilidad: ¿qué ocurre si el usuario cambia de página? No hay problema para la barra de progreso, pero la importación no se completará.

Para mostrar el progreso durante la carga, modificaría mi backend para que pueda hacer una carga selectiva.

Por ejemplo,

 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; } 

Cada vez que se realiza la carga, actualice la barra de progreso.

Una forma alternativa de modificar el backend puede ser

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