【问题标题】:Show AJAX upload status on progress element在进度元素上显示 AJAX 上传状态
【发布时间】:2012-04-16 13:33:46
【问题描述】:

我有一个 AJAX 脚本可以将文件上传到 PHP 脚本,这可能需要至少 10 秒才能运行。我想为用户显示它的进度。

在执行类中,我有一个属性$progress,它随着进度(1-100)更新,还有一个方法get_progress()(目的应该很明显)。

问题是,如何更新前端的<progress>元素让用户看到?

我认为 AJAX 是解决方案,但我无法理解它。我无法访问同一个对象实例。

【问题讨论】:

标签: php ajax progress-bar


【解决方案1】:

我会把它放在这里作为任何搜索的参考 - 这根本不依赖于 javascript..

<?php

/**
 * Quick and easy progress script
 * The script will slow iterate through an array and display progress as it goes.
 */

#First progress
$array1  = array(2, 4, 56, 3, 3);
$current = 0;

foreach ($array1 as $element) {
    $current++;
    outputProgress($current, count($array1));
}
echo "<br>";

#Second progress
$array2  = array(2, 4, 66, 54);
$current = 0;

foreach ($array2 as $element) {
    $current++;
    outputProgress($current, count($array2));
}

/**
 * Output span with progress.
 *
 * @param $current integer Current progress out of total
 * @param $total   integer Total steps required to complete
 */
function outputProgress($current, $total) {
    echo "<span style='position: absolute;z-index:$current;background:#FFF;'>" . round($current / $total * 100) . "% </span>";
    myFlush();
    sleep(1);
}

/**
 * Flush output buffer
 */
function myFlush() {
    echo(str_repeat(' ', 256));
    if (@ob_get_contents()) {
        @ob_end_flush();
    }
    flush();
}

?>

【讨论】:

  • 为什么我们需要sleep(1)?完成脚本需要更多时间。
  • 我可以知道这部分是什么echo(str_repeat(' ', 256));
  • 几年后这几乎是 SSE
  • @slier 如果您仍然想知道,我相信这是强制某些服务器刷新输出缓冲区,因为它仅在响应中至少有一定数量的数据时才刷新缓冲区.
  • 感谢您的想法。为了使工作正常进行,我回显更长的字符串 echo str_repeat(' ', 4*1024), "\n"; 以避免 http 服务器和/或浏览器在其缓冲区中等待足够的数据。
【解决方案2】:

如果您的任务是上传庞大的数据集或在服务器上处理它,同时将进度更新到服务器,您应该考虑使用某种作业架构,您可以在其中启动作业并使用其他脚本来完成在服务器上运行(例如缩放/处理图像等)。在这种情况下,您一次只做一件事,从而形成一个任务管道,其中有一个输入和一个最终处理的输出。

在管道的每一步,任务的状态都会在数据库中更新,然后可以通过目前存在的任何服务器推送机制将其发送给用户。运行一个处理上传和更新的脚本会给您的服务器带来负载并限制您(如果浏览器关闭怎么办,如果发生其他错误怎么办)。将流程划分为多个步骤后,您可以从上次成功的位置恢复失败的任务。

有很多方法可以做到这一点。但是整体流程是这样的

以下方法是我为个人项目所做的,该脚本非常适合将数千张高分辨率图像上传和处理到我的服务器,然后缩小为多个版本并上传到亚马逊 s3,同时识别其中的对象。 (我的原始代码是在 python 中的)

第 1 步:

启动传输或任务

首先上传您的内容,然后通过简单的 POST 请求立即返回此交易的交易 ID 或 uuid。如果您在任务中执行多个文件或多个事情,您可能还希望在此步骤中处理该逻辑

第 2 步:

完成工作并返回进度。

一旦您弄清楚事务是如何发生的,您就可以使用任何服务器端推送技术来发送更新数据包。我会选择 WebSocket 或服务器发送事件,无论哪个适用,都可以在不受支持的浏览器上使用长轮询。一个简单的 SSE 方法如下所示。

function TrackProgress(upload_id){

    var progress = document.getElementById(upload_id);
    var source = new EventSource('/status/task/' + upload_id );

    source.onmessage = function (event) {
        var data = getData(event); // your custom method to get data, i am just using json here
        progress.setAttribute('value', data.filesDone );
        progress.setAttribute('max', data.filesTotal );
        progress.setAttribute('min', 0);
    };
}

request.post("/me/photos",{
    files: files
}).then(function(data){
     return data.upload_id;
}).then(TrackProgress);

在服务器端,您需要创建一些东西来跟踪任务,一个简单的 Jobs 架构,带有 job_id 和发送到数据库的进度就足够了。我会将作业调度和路由留给您,但之后的概念代码(对于最简单的 SSE,足以满足上述代码)如下。

<?php
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
/* Other code to take care of how do you find how many files are left 
   this is really not required */
function sendStatusViaSSE($task_id){
    $status = getStatus($task_id);
    $json_payload = array('filesDone' => $status.files_done,
                          'filesTotal' => $status.files_total);
    echo 'data: ' . json_encode( $json_payload ) . '\n\n';
    ob_flush();
    flush();

    // End of the game
    if( $status.done ){
        die();
    }

}

while( True ){
     sendStatusViaSSE( $request.$task_id );
     sleep(4);
}

?>

关于 SSE 的好教程可以在这里找到http://html5doctor.com/server-sent-events/

你可以阅读更多关于从服务器推送更新的信息Pushing updates from server

以上是一个概念性的解释,还有其他方法可以实现这一点,但在我的案例中,这是一个解决相当大任务的解决方案。

【讨论】:

  • ob_flush();冲洗();在服务器上启用 gzip 时有效吗?我不这么认为
  • 因回答上传、烧毁问题并强制(因为 OP 接受了对他们自己问题的错误答案)进行编辑而投了反对票。
【解决方案3】:

这有点困难,(仅供参考)PHP 进程和您的 AJAX 请求由单独的线程处理,因此您无法获得 $progress 值。

一个快速的解决方案:你可以在每次更新时将进度值写入$_SESSION['some_progress'],然后你的AJAX请求可以通过访问$_SESSION['some_progress']来获取进度值。

您将需要 JavaScript 的 setInterval()setTimeout() 来继续调用 AJAX 处理程序,直到您获得 100 的返回值。

这不是完美的解决方案,但它快速简单。


因为您不能同时使用同一个会话两次,所以请改用数据库。将状态写入数据库并使用间隔的 AJAX 调用从中读取。

【讨论】:

  • 有些开发者使用一些对象缓存模式,对象写入一些缓存,所以ajax请求可以获得相同的对象属性和值。
  • 这听起来是个不错的解决方案。不过,我想听听其他答案,如果可能的话,我想避免不必要的$_SESSIONs。
  • 将 APC 缓存与 ajax 一起使用。获取进度与使用 apc 显示进度上传相同。您也可以使用 Session 。很简单,可以解决你的问题。
  • 我认为会话方法不会很好。 ajax 脚本中的 session_start 将阻止执行,因为该会话在另一个文件中打开。一个例外情况是,如果您在原始文件中使用了 session_write_close,但您无法再更新进度。
  • APC 在执行之间是否持续存在?此外,会话已退出,因为在执行大型脚本时会话已经在运行,因此在执行时无法再次使用 session_start()。 (@MichaelP 指出并经过测试。)
【解决方案4】:

这是一个老问题,但我也有类似的需求。我想用 php system() 命令运行一个脚本并显示输出。

我没有投票就完成了。

对于第二个 Rikudoit 案例应该是这样的:

JavaScript

document.getElementById("formatRaid").onclick=function(){
    var xhr = new XMLHttpRequest();     
    xhr.addEventListener("progress", function(evt) {
        var lines = evt.currentTarget.response.split("\n");
        if(lines.length)
           var progress = lines[lines.length-1];
        else
            var progress = 0;
        document.getElementById("progress").innerHTML = progress;
    }, false);
    xhr.open('POST', "getProgress.php", true);
    xhr.send();
}

PHP

<?php 
header('Content-Type: application/octet-stream');
header('Cache-Control: no-cache'); // recommended to prevent caching of event data.

// Turn off output buffering
ini_set('output_buffering', 'off');
// Turn off PHP output compression
ini_set('zlib.output_compression', false);
// Implicitly flush the buffer(s)
ini_set('implicit_flush', true);
ob_implicit_flush(true);
// Clear, and turn off output buffering
while (ob_get_level() > 0) {
    // Get the curent level
    $level = ob_get_level();
    // End the buffering
    ob_end_clean();
    // If the current level has not changed, abort
    if (ob_get_level() == $level) break;
}   

while($progress < 100) {
    // STUFF TO DO...
    echo '\n' . $progress;
}
?>

【讨论】:

  • 虽然这个答案是正确的,但我感到不安,因为它使用了 jQuery。您已经在自己进行 XHR 操作,因此它不会添加任何内容 - 如果您从中删除 jQuery,我会很乐意支持它。
  • 也许还能赢得赏金 ;)
【解决方案5】:

解决方案是:

  1. Ajax 轮询 - 在服务器端将进度存储在某处,然后使用 ajax 调用定期获取进度。

  2. 服务器发送事件 - 一种 html5 功能,允许从服务器发送的输出生成 dom 事件。这是这种情况的最佳解决方案,但 IE 10 不支持。

  3. 脚本/Iframe 流式传输 - 使用 iframe 流式传输长时间运行脚本的输出,该脚本会将脚本标记作为间隔输出,从而在浏览器中产生一些反应。

【讨论】:

    【解决方案6】:

    在这里,我只想在@Jarod Law 上面写的https://stackoverflow.com/a/7049321/2012407 之上添加两个问题@

    确实非常简单高效。我调整并使用了:) 所以我的两个担忧是:

    1. 而不是使用setInterval()setTimeout() 在回调中使用递归调用,例如:

      function trackProgress()
      {
          $.getJSON(window.location, 'ajaxTrackProgress=1', function(response)
          {
              var progress = response.progress;
              $('#progress').text(progress);
      
              if (progress < 100) trackProgress();// You can add a delay here if you want it is not risky then.
          });
      }
      

      由于调用是异步的,否则可能会以不需要的顺序返回。

    2. 保存到$_SESSION['some_progress'] 是聪明的,你可以,不需要数据库存储。 您真正需要的是允许同时调用两个脚本而不是被 PHP 排队。所以你最需要的是session_write_close()! 我在这里发布了一个非常简单的演示示例:https://stackoverflow.com/a/38334673/2012407

    【讨论】:

      【解决方案7】:

      您是否考虑过输出 javascript 并使用流刷新?它看起来像这样

      echo '<script type="text/javascript> update_progress('.($progress->get_progress()).');</script>';
      flush();
      

      由于刷新,此输出立即发送到浏览器。从长时间运行的脚本中定期执行它,它应该可以很好地工作。

      【讨论】:

      • 是的,但如果我没记错的话,浏览器不会在发送少于 x 字节的情况下呈现。所以这可能被证明是一个问题。尽管如此,注意到。也会试试的。
      • 你是对的,如果 IE 小于 256 字节,它可能会缓冲。只需添加一个 echo str_repeat(" ",256);
      • 不仅如此。 Chrome 也是如此。而且我不确定需要多少。尽管如此,当我在电脑旁边时,我会对其进行测试。
      【解决方案8】:

      这是通过$_SESSION['progress']session_start()session_write_close() 提供的最有效、最简单且经过测试的解决方案。

      1. 想法是我们将进度保存在$_SESSION['progress'],然后锁定会话以通过session_write_close()不断向用户更新进度。

      2. 在迭代开始时使用session_start();,在迭代结束时使用session_write_close()

      3. 然后通过 ajax 从不同的脚本获取这个会话变量值$_SESSION['progress']

      4. 然后通过 progress_bar 等中的 ajax 响应显示结果。

      现在让我们做一些代码:

      创建表单/请求页面触发AJAX请求(request.php):

      <div id="demo">
      <button type="button" class="button">Do Something</button>
      </div>
      
       $("button").click(function(){
        $.ajax({url: "request-handler.php", success: function(result){
          alert('done');
        }});
      });
      

      request-handler.php

      $total = 100;
      $count = 1;
      while(true)
      {
          /*write your code
           ....
          here*/
      
      
          session_start();
          $percent = ($count *100)/$total;
          $_SESSION["progress"] = number_format((float)$percent, 2, '.', '');
          $_SESSION["progress"] = (int) ceil($percent);
          $count++;
          session_write_close();
      }
      

      Ajax 获取进度(front-view.php):

       <div class="progress" style="display: none;">
            <div class="progress-bar progress-bar-striped active" role="progressbar"
            aria-valuenow="40" aria-valuemin="0" aria-valuemax="100" style="width:0%">
              0%
            </div>
          </div>
      
          <div class="alert alert-success alert-dismissible" style="display: none;">
            <a href="#" class="close" data-dismiss="alert" aria-label="close">&times;</a>
            <strong>Done!</strong> Successfully Done!.
          </div>
      
      <script type="text/javascript">
      
        var pBar = $(".progress-bar");
        var progress = 0;
        var intervalId = window.setInterval(function(){
          $.get("get-progress.php", function(data, status){
            progress = data;
          });
          if(progress > 0)
          {
            pBar.parent().show();
          } 
          var percent = progress+'%';
          pBar.width(percent);
          pBar.html(percent);
          if(progress >= 100)
          {
            pBar.parent().hide();
           $ (".alert").show();
            clearInterval(intervalId);
          } 
           
        }, 2000);
          
       
      
             </script> 
      

      get-progress.php:

      <?php
      
      session_start();
      
      $progress = $_SESSION['progress'];
      if ($progress >= 100) {
          session_start();
          unset($_SESSION['progress']);
      }
      //output the response
      echo json_encode($progress);
      ?>
      

      【讨论】:

        【解决方案9】:

        在Web应用程序中,我们经常会向后端系统发出请求,这可能会触发长时间运行的进程,例如搜索大量数据或长时间运行的数据库进程。然后前端网页可能会挂起并等待该过程完成。在这个过程中,如果我们可以向用户提供一些后端进程的进度信息,可能会提高用户体验。不幸的是,在 Web 应用程序中,这似乎不是一件容易的事,因为 Web 脚本语言不支持多线程并且 HTTP 是无状态的。我们现在可以有 AJAX 来模拟实时过程。

        基本上我们需要三个文件来处理请求。第一个是运行实际长时间运行的作业的脚本,它需要一个会话变量来存储进度。第二个脚本是状态脚本,它将在长时间运行的作业脚本中回显会话变量。最后一个是客户端AJAX脚本,可以频繁轮询状态脚本。

        具体实现可以参考PHP to get long running process progress dynamically

        【讨论】:

          猜你喜欢
          • 2014-03-03
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2014-06-06
          • 2021-11-13
          相关资源
          最近更新 更多