【问题标题】:What is the best way of showing progress on an Ajax call?显示 Ajax 调用进度的最佳方式是什么?
【发布时间】:2025-12-29 17:35:10
【问题描述】:

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

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

【问题讨论】:

标签: jquery ajax progress-bar


【解决方案1】:

我认为最好的方法是使用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

只发出一次请求,然后继续检查XMLHttpRequestreadyState === 3中的数据。

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

【讨论】:

  • 当心...每个浏览器处理此问题的方式有所不同 - 也就是它根本不起作用。猜猜哪个浏览器的行为与其他浏览器不同。有关详细信息,请参阅下面的答案。
【解决方案2】:

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

【讨论】:

  • 这听起来有点乱。 .你有这样做的例子吗??
【解决方案3】:

我假设您当前对批量更新中的所有记录使用一个 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

【讨论】:

【解决方案4】:

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

【讨论】:

    【解决方案5】:

    我只是在阅读回复时有了一个想法。

    JavaScript 和 PHP 共享 cookie,

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

    优点:

    1. 只有 1 个 Ajax 调用。
    2. 减少服务器负载。

    【讨论】:

    • 哇,这可能吗?在没有多个 ajax 调用的情况下,如何在客户端(javascript)和服务器之间传递 cookie 的变化值?
    • javascript 将使用递归读取 cookie 函数检查 cookie 值,它不会为此使用 ajax。 w3schools.com/js/js_cookies.asp查看此链接以使用 javascript 读取 cookie
    • 在客户端和服务器之间必须有一些内部握手才能反映 cookie 更新。由于这种内部通信,我们可能能够在服务器端更改 cookie 值并在客户端读取它。没有它怎么可能?
    • @Sandeepan:你明白了。我认为 javascript 不会从 cookie 中获取价值,因为该 cookie 将设置在服务器端。同意
    【解决方案6】:

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

    但是在通过xhttpr 完成请求之前,您可能无法读取请求。您也许可以通过 iframe 提出请求,并通过“http 流”进行加载。

    但即使这样也可能是粗略的。 HTTP 并不是要零碎/零散地传输东西。就像其他人指出的那样,最好单独进行后续调用以获取操作状态。

    【讨论】:

      【解决方案7】:

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

      如果是这种情况,只需每 200 次左右迭代进行一次 ajax 调用。如果对每组 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!
        ?>
        

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

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

        【讨论】:

          【解决方案9】:

          像这样创建一个简单的表:

          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 个应用程序中使用过这种方法,我必须说它非常可靠和快速(showLog 操作只是一个简单的 SELECT 语句)。也可以使用 session 来存储进度,但这会带来很多问题,因为 session 必须写关闭(如果存储在文件中),否则 showLog AJAX 查询将等待长时间操作完成(和松散的感觉)。

          【讨论】:

          • 您的代码实际上每次都在轮询服务器。 OP 不想进行投票,因为他明确提到 '不做 5000 个不同的帖子'
          • 好吧,如果运行 4 小时,这段代码将向服务器发出 5000 个请求。在 10 分钟的操作中,这将发出 150 个请求,这与 5000 个相去甚远。他没有说他想要一个请求。
          【解决方案10】:

          似乎有些可疑。

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

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

          所以可能有一些方法可以划分问题,这样就没有等待时间了。例如考虑一个临时数据库表(或者只是在应用程序内存中,只需创建一个 ORM 实体列表就很容易......但这是一个问题),然后当需要提交这些更新时,它们至少可以在批处理,无需任何客户端/服务器传输。甚至可以标记已编辑的各个字段,这样数据库上就没有更多更新了,除了更改的确切内容。

          有长时间运行的进程,我知道有时你会被困在不得不使用别人提供给你的东西......但也许稍微思考一下你就可以简单地摆脱等待时间。

          【讨论】:

            【解决方案11】:

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

            此解决方案通常以客户端复杂性换取网络带宽。

            不要将此服务器活动与最终的文件导入混为一谈,除非您真的知道自己在做什么。 你会为了稳定性交易数据库查询(用于计算更新行的选择):如果用户更改页面怎么办?进度条没有问题,但是导入不会完成。

            【讨论】:

              【解决方案12】:

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

              例如,

              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
              

              【讨论】:

                最近更新 更多