在Ajax调用上显示进度的最佳方法是什么?

时间:2010-10-10 18:06:04

标签: jquery ajax progress-bar

我有一个Ajax调用,可以更新数据库中的5,000条记录,因此这需要花费很多时间。我有一个Ajax“正在加载图片”,显示正在发生的事情,但我正在寻找一种更好的方式来显示“正在更新50的5000 ...”,“更新200的5000”,或类似的东西。

在没有5000个不同帖子的情况下,在Ajax / jQuery中做这样的事情的最佳方法是什么?

12 个答案:

答案 0 :(得分:23)

我认为最好的是使用Comet

在Comet风格的应用程序中,服务器实际上可以将数据推送到客户端(而不是一次又一次地从服务器请求客户端请求数据)。客户端只需连接到服务器一次。然后服务器将继续将数据推送回客户端。

来自维基百科:

  

Comet是一种编程技术,它使Web服务器能够将数据发送到客户端,而无需客户端请求它。它允许创建托管在浏览器中的事件驱动的Web应用程序。

现在让我们来看看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's 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(); };

Reference

答案 1 :(得分:4)

我会让在SESSION变量中执行大更新记录的函数在每次单个(或那么多)更新后进行当前进度,并使用单独的AJAX脚本从SESSION中检索此进度值并让JavaScript使用这样可以更新进度条/文本。

答案 2 :(得分:2)

我假设您当前正在为批量更新中的所有记录使用一个POST,并在加入和返回之间放置加载图像。

不要让服务器在完成更新之前等待返回,而是让它立即返回,并为该批量更新提供特殊ID。然后实现一个服务器调用,该调用返回批量更新的状态,进度对话框可以调用该状态来报告进度。

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

答案 3 :(得分:2)

每隔 n 毫秒,我会触发一次Ajax回调,可以查询完成了多少(例如更新的记录数)并使用它来显示进度条。类似于this的工作方式。

答案 4 :(得分:2)

您可以使用进度更新响应缓冲区,定期从服务器刷新响应缓冲区。

但您可能无法通过xhttpr在完成请求之前阅读该请求。您可以通过iframe发出请求,并通过“http streaming”进行加载。

但即使这样也可以粗略。 HTTP并不意味着将零碎的东西转移到碎片上。像其他人指出的那样,最好进行单独的后续调用以获取操作的状态。

答案 5 :(得分:2)

我在阅读回复时得到了一个主意。

JavaScript和PHP共享cookie,

  1. 在进行Ajax调用时使用JavaScript创建cookie。
  2. 在Ajax PHP文件中,每次SQL更新时都会增加该cookie中的值。
  3. 在JavaScript中,递归函数将读取该特定cookie并更新进度条编号。
  4. 优点:

    1. 只有1个Ajax电话。
    2. 减少服务器负载。

答案 6 :(得分:1)

我假设您有理由单独遍历每条记录,而不是简单地运行SQL语句。

如果是这种情况,只需每200次左右进行一次ajax调用。如果对每组200条记录执行此操作,它将只消耗50个Ajax调用。

像(伪代码):

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

答案 7 :(得分:1)

我不确定服务器端在哪里发布,但您应该能够将此方法应用于大多数编程语言。我以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!
?>

在回显之后,刷新输出缓冲区以确保将此数据发送到浏览器。因为它是一个完整的脚本标记,浏览器将在里面执行javascript,将进度条更新为传递给它的任何值。

要使整个工作正常工作,您需要添加一些计算来理解您调用进度条的数字,但我认为这不应该是一个太大的问题。在示例中使用setProgress可能更有意义使用百分比,尽管为了清晰起见我想要清除所需的计算。

答案 8 :(得分:0)

为了在加载过程中显示进度,我会修改后端,以便它可以进行选择性加载。

例如,

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

答案 9 :(得分:0)

有些东西似乎很可疑。

我不熟悉无法在闪存中更新5000条记录的数据库......所以这不仅仅是您应用的单个更新,而是记录更新的记录?

考虑一个允许用户下载5000个条目并且不标记哪些条目已被编辑的系统,然后在更新结束时应用单个更新按钮,这将要求以某种方式传回所有5000条记录。那将是最糟糕的情况。

因此可能有某种方法可以对问题进行分区,使得没有等待时间。考虑一个临时的数据库表(或者只是在应用程序内存中,只需创建一个ORM实体列表就很容易......但是这是一个旁边的)然后当提交这些更新时,它们至少可以在一个批量不需要任何客户端/服务器传输。甚至可以标记已编辑的各个字段,这样除了确切改变之外,DB上没有更新的内容。

有很长的运行过程,我知道有时候你不得不使用别人提供给你的东西......但也许有点想到你可以简单地摆脱等待时间。

答案 10 :(得分:0)

创建一个这样的简单表:

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语句)。也可以使用session来存储进度,但这会带来很多问题,因为会话必须写入关闭(如果它存储在文件中),否则showLog AJAX查询将等待长操作完成(和松散的感觉)。

答案 11 :(得分:0)

我曾经用这种方式做过类似的事情(类似于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之间的比率调整进度条的大小。

此解决方案通常会将客户端复杂性换成网络带宽。

请勿将此服务器活动与最终的文件导入混合,除非您确实知道自己在做什么。 您将交换数据库查询(用于计算更新行的选择)以获得稳定性:如果用户更改页面该怎么办?进度条没有问题,但导入将无法完成。