如何显示加载百分比并且不使用JavaScript实现?

7

我想制作类似于PHP中的加载器,因此我使用了以下代码:

<?php 
$x=1;
while($x<=100) {
   echo "Loading: $x %<br>";
   $x++;
}   
?>

我希望它能够从“加载1%”显示到“加载100%”。但这将导致所有内容一次性出现,新行出现后不会消失。因此,我想知道如何使新行出现并且旧行消失,并且在页面加载后开始,这样用户就可以观看加载器实际从1%到100%加载的过程。

更新:我知道我应该使用JS和/或Ajax来实现它,我只是想知道是否也有一种方法可以使用PHP来做到这一点:)


8
在其中添加一些 JavaScript 代码来移除或替换它,或者使用 AJAX 轮询服务器以获取该行。 - Lawrence Cherone
2
是的,用JavaScript可能会更容易。如果你只是在做一个随机循环,它实际上与页面加载的进度没有任何关系。此外,一个只是回显100次的循环基本上是瞬间完成的……计算机非常快。 - Andrew Whatever
1
是的,我不明白为什么您认为行为会与您得到的不同。一旦输出发送到浏览器,就无法取消回显输出。 - Mike Brant
2
尽管未缓冲的服务器端脚本可以像交互式控制台程序一样运行,但您不能假设在您和浏览器之间没有节点作为缓冲区,并且阻塞页面处理流程也是一个不好的想法。 - Dai
2
我知道应该使用JS和/或Ajax来实现它,只是想知道是否也可以用PHP来实现 :) - Marwan Ossama
显示剩余5条评论
21个回答

38

PHP是一种服务器端语言,它根据请求给出响应。

你无法像想象中的那样操作DOM

为此,你应该使用JavaScript和AJAX根据PHP的结果更改DOM元素。


1
我知道我应该使用JS和/或Ajax来实现它,我只是想知道是否也可以用PHP来做到这一点 :) - Marwan Ossama
7
PHP是一种服务器端语言,你无法像想要的那样在DOM上进行操作。 - showdev
2
@MarwanOssama 不行。一旦 PHP 向浏览器发送了内容,那就是它的全部;PHP 无法更改或修改已经发送的内容。 - Sverri M. Olsen
1
这并不总是正确的,PHP的flush/ob_flush在某些Web服务器上实际上可以附加到DOM中。请查看我刚刚发布的答案。 - Jimmie Tyrrell
7
我实际上是在给你的答案点赞,不仅因为它是正确的,而且为了让你获得罕见的反转徽章,显然这是你的第一个金色徽章!恭喜 :-) - Valmiky Arquissandas
1
我现在正在查看这个答案,因为我很好奇谁获得了逆转徽章!其中一半已经被删除了! - Almo

30

跟进长时间运行的任务是常见的,但第一次实现并不容易。以下是一个完整的示例。

一个长时间运行任务的示例 (在 Sellermania 的产品中的一个长时间运行任务示例)

背景

任务

假设您当前有以下任务,并希望向访问者显示进度条。

PHP task.php

  <?php

  $total_stuffs = 200;
  $current_stuff = 0;
  while ($current_stuff < $total_stuffs) {
    $progress = round($current_stuff * 100 / $total_stuffs, 2);

    // ... some stuff
    sleep(1);

    $current_stuff++;
  }

用户界面

您的美丽界面如下所示:

一个漂亮的UI

HTML ui.html

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta http-equiv="content-type" content="text/html; charset=utf-8"/>
        <title>My Task!</title>
    </head>
    <body>

        <a id="run_task" href="#">Run task</a>

        <div id="task_progress">Progression: <span id="task_progress_pct">XX</span>%

        <script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>

        <script type="text/javascript">

            $('#run_task').click(function(e) {
                e.preventDefault();
                $.get('task-launch.php');
            });

        </script>

    </body>
</html>

启动器

为了使演示有效,我们需要将任务在后台启动。因此,当单击“运行”时,将异步调用此PHP脚本,并在后台执行上述任务。

PHP task-launch.php

<?php

  // launches the task in background
  // see https://dev59.com/jmjWa4cB1Zd3GeqPoj7-#12341511 for details about the arguments
  exec("/usr/bin/php task.php > /dev/null 2>&1 &");

问题

problems

这里有三个问题:

  1. 通过多次点击运行按钮,您可以运行任务多次,如何避免同时在后台运行多个任务?

  2. 由于任务在服务器端运行,因此我们需要做一些事情来访问服务器并请求进度信息。

  3. 当我们连接到服务器端时,$progress变量无法读取,因为它存储在task.php运行实例的上下文中。


解决方案

solutions

将进度信息存储在可从外部读取的位置

通常,进度信息存储在数据库、文件或其他能够被程序(实际上是您的任务)写入和另一个程序(应该显示进度的 UI)读取的地方。

我开发了一个用于在多个 PHP 应用程序之间共享数据的类 (在这里可用于 Github),它的工作方式与 stdClass 大致相同,但始终会将其内容安全地同步到文件中。

只需下载 src/Sync.php 并通过以下方式更改上面的 task.php:

PHP task.php

<?php

require("Sync.php");

$total_stuffs = 200;
$current_stuff = 0;

$shared = new Sync("some_file.txt");
$shared->progress = 0;

while ($current_stuff < $total_stuffs) {
  $shared->progress = round($current_stuff * 100 / $total_stuffs, 2);

  // ... some stuff
  sleep(1);

  $current_stuff++;
}

// to tell the follower that the task ended
$shared->progress = null;

重要提示:在这里,some_file.txt 是存储您任务共享数据的地方,如果每个用户都有自己的任务,可以使用“task_[user_id].txt”等命名方式。请查看 GitHub 上的自述文件 以优化文件访问。

使用同步变量保护任务启动器

  • 任务开始时,进度被设置为0,因此在运行任务之前,首先要检查是否将此进度设置为0。

PHP task-launch.php

<?php

require("Sync.php");
$shared = new Sync("some_file.txt");

if (is_null($shared->progress)) {
  exec("/usr/bin/php task.php > /dev/null 2>&1 &");
}
  • 如果快速点击运行按钮两次,我们仍然可能有两个任务实例。为了处理这种情况,我们需要模拟互斥锁,换句话说,使变量只对当前应用程序可用来执行某些操作--其他应用程序将保持睡眠状态,直到共享变量被解锁。

PHP task.php

<?php

require("Sync.php");

$total_stuffs = 200;
$current_stuff = 0;

$shared = new Sync("some_file.txt");

// here is the magic: impossible to set the progression to 0 if an instance is running
// ------------------
$shared->lock();
if (!is_null($shared->progress))
{
    $shared->unlock();
    exit ;  
}
$shared->progress = 0;
$shared->unlock();
// ------------------

while ($current_stuff < $total_stuffs) {
  $shared->progress = round($current_stuff * 100 / $total_stuffs, 2);

  // ... some stuff
  sleep(1);

  $current_stuff++;
}

// the task ended, no more progression
$shared->progress = null;
警告: 如果您的任务崩溃并且无法到达终点,您将永远无法再次启动它。为避免这种情况,您还可以将子进程的getmypid()和一些time()东西存储在共享变量中,并在任务中添加超时逻辑。

使用轮询来查询服务器的进度信息

轮询意味着每隔一段时间(例如1秒、5秒或其他时间)向服务器请求进度信息。简而言之,客户端会每 N 秒向服务器询问进度信息。

  • 在服务器端,我们需要编写处理程序来回答进度信息。

PHP task-follow.php

<?php

require("Sync.php");

$shared = new Sync("some_file.txt");

if ($shared->progress !== null) {
    echo $shared->progress;
} else {
    echo "--"; // invalid value that will break polling
}
  • 在客户端,我们需要编码“向服务器请求进度信息”的业务

HTML ui-polling.html

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta http-equiv="content-type" content="text/html; charset=utf-8"/>
        <title>My Task!</title>
    </head>
    <body>

        <a id="run_task" href="#">Run task</a>

        <div id="task_progress">Progression: <span id="task_progress_pct">XX</span>%

        <script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>

        <script type="text/javascript">

            $('#run_task').click(function(e) {
                e.preventDefault();

                <!-- not a good practice to run a long-running task this way but that's a sample -->
                $.get('task-launch.php');

                <!-- launches the polling business -->
                setTimeout(function() {
                    getProgressionInformation();
                }, 1000);

            });

            function getProgressionInformation() {
                $.get('task-follow.php', function(progress) {
                    $('#task_progress_pct').html(progress);
                    if (progress !== '--') {
                        <!-- if the task has not finished, we restart the request after a 1000ms delay -->
                        setTimeout(function() {
                            getProgressionInformation();
                        }, 1000);
                    }
                });
            }

            /* the task might be already running when the page loads */
            $(document).ready(function() {
                getProgressionInformation();
            });

        </script>

    </body>
</html>

it works!


只需最少的JavaScript?

我还开发了一个jQuery插件domajax,旨在实现“无需JavaScript”的ajax(实际上,插件本身是基于jQuery编写的,但使用它不需要JavaScript代码),通过组合选项,您可以进行轮询。

在我们的演示中:

  • 关注者变为:

PHP task-follow.php

<?php

require("Sync.php");

$shared = new Sync("some_file.txt");

if ($shared->progress !== null) {
    echo $shared->progress;
}
  • UI源代码如下:

HTML ui-domajax.html

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta http-equiv="content-type" content="text/html; charset=utf-8"/>
        <title>My Task!</title>
    </head>
    <body>

        <a
            href="#"
            id="run-task"
            class="domajax click"
            data-endpoint="task-launch.php"
            data-domajax-complete="#polling"
        >Run task</a>

        <div
            id="polling"
            data-endpoint="task-follow.php"
            data-delay="1000"
            data-output-not-empty="#task-progress-pct"
            data-domajax-not-empty=""
        ></div>

        <div id="task-progress">Progression: <span id="task-progress-pct">--</span>%

        <script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
        <script src="//domajax.com/js/domajax/jquery.domajax.js"></script>

    </body>
</html>

正如您所看到的,在这段代码中没有任何可见的JavaScript代码。很干净,不是吗?

domajax网站上还有其他示例,请查看演示窗格中的“轻松管理进度条”选项卡。 所有选项都有详细记录。


所有的Javascript代码都需要额外下载120kb,加载到内存中并进行JIT编译。避免使用Javascript(正如OP的问题所要求的)的一个原因不仅仅是为了代码的简洁性,还包括效率方面的考虑。 - Jimmie Tyrrell
你说得对,这个问题需要在没有JavaScript的情况下显示进度;但它也包含了关于输出缓冲和全局轮询架构的混淆。我的答案更加通用并涵盖了该主题;它提供了一种(更好的)轮询方式。在我看来,在实际生活中,当需要轮询时很少避免使用JavaScript。 - Alain Tiemblo

27

实际上你并非需要 Javascript 就能完成这个任务。如果你等待的任务是持久性的,你可以简单地创建一个 PHP 脚本来:

  1. 查找任务(例如在数据库中)
  2. 检查任务的完成百分比
  3. 向用户输出百分比
  4. 如果百分比 < 100%
    • 刷新页面
    • 否则,重定向到完成页面。

你可以使用 HTML meta 刷新实现非 JavaScript 刷新。它可能会像这样:

<?php
    $taskId = $_GET("taskId");
    $task = lookupTask($taskId);
    $refresh = $task->percentComplete < 100;
?>
<html>
    <head>
        <? if($refresh): ?>
        <meta http-equiv="refresh" content="5">
        <? else: ?>
        <meta http-equiv="refresh" content="0; url=taskComplete?taskId=<?=$taskId ?>" />
        <? endif; ?>
    </head>
    <body>
        Loading: <?=$task->percentComplete ?>
    </body>
</html>

另一种方法是使用 PHP 的 flush() 方法,这是一种非常丑陋的 hack 方法。Flush 方法将强制输出缓冲区中的内容到正在运行 PHP 的地方(例如 CGI)。有时此输出还会被主机应用程序进一步缓冲,但在某些 Web 服务器上,它会直接发送到用户。在此处阅读更多信息:http://php.net/manual/zh/function.flush.php

您可能需要将其与 ob_flush 结合使用,但我不确定:http://php.net/manual/zh/function.ob-flush.php

使用 CSS,您可以默认隐藏所有“加载”文本,但覆盖最后一个子级的“加载”文本并显示它。例如:

<style type="text/css">
.loadText {
    display: none;
}
.loadText:last-child {
    display: block;
}
</style>
<?php 
for ($x = 0; $x <= 100; $x++) {
   echo '<div class="loadText">Loading: ', $x, '%</div>';
   flush();
}
?>

这两个解决方案都很糟糕,我只是为了完整性而分享它们 :)


11

稍微解释一下这里的答案:

PHP是一种服务器端语言,但这不是它通常无法工作的原因。

这与我们通过HTTP协议进行通信的方式有关。
当客户端向服务器发送一个HTTP请求时,首先要建立一个连接。然后服务器处理请求并生成一个响应,然后将其发送回客户端。
一旦接收到响应,连接就断开了。
所以,通常情况下,您会收到一个响应。

一个常见的误解是使用默认启用的 keep-alive 标头用于持久连接可以保持套接字保持打开状态并持续推送内容。 HTTP 实际上不能这样工作,需要在流数据之前进行一些更改。
但是有关此问题的更多信息,请参见这里这里

你能做什么?

我可以想到几个选择。

  1. 使用客户端语言(通常为JavaScript),我们可以根据每个响应发送XMLHttpRequest对象来查询服务器,从而有效地创建媒体流。
  2. 截龙头是一个允许Web服务器通过HTTP请求向客户端推送数据的广义术语。APE 是该实现之一。
  3. 查看使用Ratchet的WebSockets。
  4. 理论上,如果您知道客户端的地址,您可以在双方都编写接受cURL和XHR的API的情况下进行通信。在这种情况下,两侧都将充当客户端和服务器,并且可以在同一级别上通信。PHP可以实现这一点,但不适用于浏览器。

最后,您可能会想阅读这个流行答案,介绍了一些相关技术。


在HTTP 1.1(也包括SPDY),除非另有声明,否则所有连接均被视为持久连接。您可以使用单个连接进行多个请求。而SPDY行为相同。这不是一个误解!只需阅读规范即可:http://tools.ietf.org/html/rfc7230#section-6.3 - Jens A. Koch
@Jens-AndréKoch,您所说的是正确的,但我的回答是针对您可能没有仔细阅读的问题。您确实有多个请求使用同一个套接字。这也是默认设置,我已经说明了。但是持久连接只是意味着客户端可以使用相同的底层套接字发送多个请求并接收相应的响应。服务器不能简单地将数据推送回客户端,因为它不受控制,只是对多个请求进行多次响应。它在这里不是发起 - LifeQuery
不。客户端发出请求。服务器响应并保持连接打开——以便将数据分块简单地推回到客户端。然后关闭连接。它不会对多个请求进行多次响应,而是对一个请求进行多次响应。例如:https://dev59.com/FH_aa4cB1Zd3GeqPyhnF#25203014 - 我仔细阅读了问题:它没有提到需要多个请求的要求。无论如何,我们都知道我们将使用JS或轮询技术来处理这个问题。 - Jens A. Koch
@Jens-AndréKoch 我看了你的回答,但它并没有与我所说的任何事情相矛盾。我强调的唯一一件事是HTTP是一个请求/响应协议。即使在你的脚本中数据以块的形式发送回来,它仍然只是一个响应。我的回答就是为了阐明这个问题,正如我已经清楚地加粗指出的那样。相关的回答在这里 - LifeQuery

7
更简单地说:
<style> div:not(:last-child) { display:none } </style>
<?php 
$x=1;
while($x<=100) {
   echo "<div>Loading: $x %</div>";
   $x++;
}   
?>

那样一来,旧的 <div> 将会在新的出现时被隐藏。

1
如果您开启了响应缓冲并且在每次迭代后不刷新,这根本行不通。但是,它也测量不到任何东西。无论您将其放置在哪里,它基本上都会立即通过这些行。即使使用响应缓冲。 - Andrew Barber
关于响应缓冲区的观点很好。此外,我假设作者会将“echo”语句放在其他地方,以便测量实际值。 - jasonhansel
我明白你的第二点意思;你可能想重新排列你的代码来模拟它的工作方式,而不是使用循环。另外,请确保使用@-名称“ping”我,这样我才能看到回复;就像这样:@jasonhansel - Andrew Barber
当然可以工作。但是你需要使用GOTO或者延迟等待直到某些事情完成,或者延迟另一轮。哎呀,那将是一个令人讨厌的hack :) - kaiser
@kaiser 如果他正在使用类似于curlopt_progressfunction的东西,那就不一定了。 - jasonhansel

4

这个脚本适用于 PHP 5.4 和 Nginx 服务器端,并经过 Firefox 客户端测试。但不兼容 Chrome。

演示视频

提示

  • 只有一个请求
  • 连接必须保持打开
  • 为了分批发送内容,响应必须是分块的
  • 您需要为您的 Web 服务器设置无缓冲头文件
  • PHP 必须配置允许立即刷新
  • 您需要一个技巧或聪明的解决方案来在浏览器中写入同一行
    • 这里使用了 CSS 技巧

<?php
header('X-Accel-Buffering: no'); // nginx related - turn the output buffer off
header('Content-Encoding: chunked;');

header('Transfer-Encoding', 'chunked');
header('Content-Type', 'text/plain');
header('Connection', 'keep-alive');

function dump_chunk($chunk)
{
  printf("%s\r\n", $chunk);
  flush();
  ob_flush(); // if you comment this out, the chunk updates are slower
}

ob_start();
?>
<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>A Chunked Response</title>
        <style> div:not(:last-child) { display:none; } </style>
    </head>
    <body>
        <?php
        // inital clear all
        ob_end_flush(); flush(); ob_flush();

        // browser padding
        echo str_pad('', 4096) . "\n"; flush(); ob_flush();

        for ($i = 0; $i < 1000; $i++) {
            usleep(15000);
            dump_chunk('<div>Sending data chunk ' . ($i + 1) . ' of 1000 <br /></div>');
        }

        usleep(10000); // this is needed for the last update
        ?>
    </body>
</html>

3

我不确定我是否真正理解了你的问题,但是我做了一些通常人们不知道的事情。为了给人以被覆盖的印象,您可以在输出前插入一个CR(回车符“\r”)。我在下面的示例中写下了它:

<?php
$x=1;
while($x<=100) {
   echo "\rLoading: $x %";
   usleep(10000);
   $x++;
}
?>

在终端上运行以下命令,可以获得一个炫酷的加载状态: $ php code.php 希望这是你一直在寻找的内容。

2

不行,你需要使用javascript或jquery的帮助才能做到这一点。

如果需要,你可以拥有一个php页面,并返回如下进度:

//yourprogresspage.php

    <?php 
    $x="your progress";
    echo "Loading: $x %<br>";

    ?>

那么你需要使用jquery从你的页面调用它。
<script>
   var repeater;
   function updateprogress() {
      $.post( "yourprogresspage.php", function( data ) {
         $( ".progressdiv" ).html( data );
      });
      repeater = setTimeout(updateprogress, 1000);
   }   
  updateprogress();
</script>

可以仅使用HTML和CSS完成。您可以使用固定定位或CSS选择器来定位最后一行的回显。 - aecend
在问题中,他明确提到需要借助PHP来完成此操作,如果进度是从数据库值获取的,那么肯定需要使用PHP或任何其他服务器端脚本。在这种情况下,你如何只使用HTML和CSS来显示进度呢? - JSunny

2
我使用了一些CSS和HTML,这是我得到的结果:
<html>
    <?php 
        $x=1;
        while($x<=100)
        {
    ?>
    <div id="count"style="position:absolute;top:10px;left:10px;">
        <style>
        #count:after 
        {
          content:'<?php echo "Loading: $x % "; ?>';      
        }
        </style>
    </div>
    <?php 
        $x++;
        sleep(1); 
        } 
    ?>
</html>

1

这对我来说很好用

if(!session_id())
    session_start();        

if($_SESSION['loding']<=100){
   echo "Loading:".$_SESSION['loding']++ ."%<br>";
    ?>
     <div style="width:<?echo $_SESSION['loding'];?>px; height:10px; background-color:green;"></div>
   <? //the above div content displays a green loader type effect.
    header( "refresh:.5; url=loaderinphp.php" );
   }
else{
     //redirect to another page or code here
}//unset the session $_SESSION['loding'] while calling again for another page load

截图:

enter image description here

PHP的特点在于它会被完全加载并显示为HTML内容。

(i.e)

echo "1";
usleep(5000000);
echo "2";

这样做会同时显示两个echo语句,但随着usleep函数中时间的增加,页面加载时间会延长。

因此,每次增加加载内容的值时,我们必须发出一次服务器请求。

如您所知,可以使用JavaScript、jQuery、Ajax来实现此操作,我已尝试使用session来完成。

希望这对您有所帮助...


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接