跟进长时间运行的任务是常见的,但第一次实现并不容易。以下是一个完整的示例。
(在 Sellermania 的产品中的一个长时间运行任务示例)
背景
任务
假设您当前有以下任务,并希望向访问者显示进度条。
PHP task.php
<?php
$total_stuffs = 200;
$current_stuff = 0;
while ($current_stuff < $total_stuffs) {
$progress = round($current_stuff * 100 / $total_stuffs, 2);
sleep(1);
$current_stuff++;
}
用户界面
您的美丽界面如下所示:
![一个漂亮的UI](https://istack.dev59.com/swqEk.webp)
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
exec("/usr/bin/php task.php > /dev/null 2>&1 &");
问题
![problems](https://istack.dev59.com/QkzQp.webp)
这里有三个问题:
通过多次点击运行按钮,您可以运行任务多次,如何避免同时在后台运行多个任务?
由于任务在服务器端运行,因此我们需要做一些事情来访问服务器并请求进度信息。
当我们连接到服务器端时,$progress
变量无法读取,因为它存储在task.php
运行实例的上下文中。
解决方案
![solutions](https://istack.dev59.com/8KLTB.webp)
将进度信息存储在可从外部读取的位置
通常,进度信息存储在数据库、文件或其他能够被程序(实际上是您的任务)写入和另一个程序(应该显示进度的 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);
sleep(1);
$current_stuff++;
}
$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");
$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);
sleep(1);
$current_stuff++;
}
$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 "--";
}
- 在客户端,我们需要编码“向服务器请求进度信息”的业务
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();
$.get('task-launch.php');
setTimeout(function() {
getProgressionInformation();
}, 1000);
});
function getProgressionInformation() {
$.get('task-follow.php', function(progress) {
$('#task_progress_pct').html(progress);
if (progress !== '--') {
setTimeout(function() {
getProgressionInformation();
}, 1000);
}
});
}
/* the task might be already running when the page loads */
$(document).ready(function() {
getProgressionInformation();
});
</script>
</body>
</html>
![it works!](https://istack.dev59.com/6vX9f.webp)
只需最少的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;
}
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网站上还有其他示例,请查看演示窗格中的“轻松管理进度条”选项卡。 所有选项都有详细记录。