本文目录导读:

- 方案一:基于文件或数据库 + 前端轮询(最常用、易实现)
- 方案二:Server-Sent Events (SSE) - 单向推送
- 方案三:WebSocket(最佳实时方案)
- 方案四:异步任务队列(生产环境推荐)
- 总结与推荐
在PHP项目中实现进度追踪,核心思路是让执行耗时任务的服务端与请求进度的客户端(通常是浏览器)能够进行异步通信。
由于PHP默认是同步阻塞的,执行一个长时间任务时,请求会被挂起,无法同时返回进度,需要采用特定的架构和机制。
以下是几种主流且实用的实现方案,从简单到复杂排列:
基于文件或数据库 + 前端轮询(最常用、易实现)
这是最经典、兼容性最好的方案,将进度信息存储在共享存储中,前端定时请求查询。
工作流程:
- 发起任务:前端发送一个请求,后端接收后,创建一个唯一的任务ID(如
UUID),并将任务状态(如pending、进度0%)存入文件或数据库。 - 异步执行:后端立即返回任务ID,然后使用
fastcgi_finish_request()或pcntl_fork/exec等方式在后台启动实际的耗时任务。 - 更新进度:后台任务执行过程中,不断更新文件或数据库中该任务ID对应的进度(如
progress: 45%)。 - 前端轮询:前端拿到任务ID后,通过
setInterval或setTimeout定时(如每1-2秒)调用一个专门的“查询进度”API。 - 返回结果:“查询进度”API 从存储中读取该任务ID的进度并返回,当进度达到100%后,轮询结束,前端展示最终结果。
代码示例(核心逻辑):
<?php
// 1. 启动任务接口 (start_job.php)
$taskId = uniqid('task_', true);
file_put_contents("/tmp/progress_{$taskId}.txt", json_encode(['progress' => 0, 'status' => 'processing']));
// 立即返回任务ID给前端
echo json_encode(['task_id' => $taskId]);
flush(); // 确保输出立即发送
// 2. 在后台执行实际任务(使用 pcntl_fork 或 shell_exec 后台运行)
$command = "php background_task.php {$taskId} > /dev/null 2>&1 &";
exec($command);
exit; // 当前请求结束
<?php
// background_task.php
$taskId = $argv[1];
$totalSteps = 100;
for ($i = 1; $i <= $totalSteps; $i++) {
// 模拟耗时操作
sleep(1);
// 更新进度文件
$progressData = json_encode([
'progress' => $i,
'status' => $i === $totalSteps ? 'completed' : 'processing'
]);
file_put_contents("/tmp/progress_{$taskId}.txt", $progressData);
}
echo "Job {$taskId} completed.\n";
<?php
// 3. 查询进度接口 (get_progress.php?task_id=xxx)
$taskId = $_GET['task_id'];
$progressFile = "/tmp/progress_{$taskId}.txt";
if (!file_exists($progressFile)) {
echo json_encode(['progress' => 0, 'status' => 'not_found']);
exit;
}
$data = json_decode(file_get_contents($progressFile), true);
header('Content-Type: application/json');
echo json_encode($data);
优点: 实现简单,不依赖外部服务。 缺点: 轮询产生额外请求,实时性一般;处理大量任务时文件I/O可能成为瓶颈。
Server-Sent Events (SSE) - 单向推送
SSE 允许服务端向客户端主动推送数据,适合进度追踪这种服务端主动更新的场景。
工作流程:
- 前端创建一个
EventSource对象,连接到后端的SSE端点。 - 后端收到请求后,不立即关闭连接,而是设置响应头为
text/event-stream。 - 在
while循环中,执行耗时任务,每次循环后echo "data: " . json_encode($progress) . "\n\n";并flush()。 - 前端通过
EventSource.onmessage监听并更新进度。
代码示例:
<?php
// sse_progress.php
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
header('Connection: keep-alive');
$total = 100;
for ($i = 1; $i <= $total; $i++) {
// 模拟耗时操作
sleep(1);
$data = json_encode(['progress' => $i, 'total' => $total, 'message' => "Processing step $i"]);
echo "data: {$data}\n\n";
ob_flush();
flush();
if (connection_aborted()) {
break; // 客户端断开连接时停止
}
}
echo "event: complete\ndata: {}\n\n";
flush();
前端JavaScript:
const eventSource = new EventSource('/sse_progress.php');
eventSource.onmessage = function(event) {
const data = JSON.parse(event.data);
console.log('Progress:', data.progress);
document.getElementById('progress-bar').style.width = data.progress + '%';
};
eventSource.addEventListener('complete', function() {
eventSource.close();
alert('Job completed!');
});
eventSource.onerror = function() {
eventSource.close();
};
优点: 实时性最好,无需轮询,连接开销小。 缺点: 需要保持长连接,消耗服务器资源;不兼容老旧浏览器;无法通过CDN。
WebSocket(最佳实时方案)
适用于需要双向实时通信(如进度追踪 + 用户取消任务)的场景,可以使用 Ratchet、Swoole、Workerman 等库。
工作流程:
- 前端通过 WebSocket 连接到后端服务(通常独立运行,如端口8080)。
- 前端发送任务启动消息。
- 后端 WebSocket 服务端接收消息,创建任务并存储任务ID与连接ID的映射。
- 后台任务执行时,通过连接ID找到对应的 WebSocket 连接,推送进度消息。
- 前端接收消息并更新UI。
示例(使用 Workerman):
<?php
use Workerman\Worker;
use Workerman\Lib\Timer;
$worker = new Worker('websocket://0.0.0.0:8080');
$worker->count = 4;
// 存储任务ID到连接ID的映射
$taskMap = [];
$worker->onMessage = function ($connection, $data) {
$data = json_decode($data, true);
if ($data['action'] === 'start') {
$taskId = uniqid();
$taskMap[$taskId] = $connection->id;
// 向客户端发送任务ID
$connection->send(json_encode(['type' => 'task_id', 'task_id' => $taskId]));
// 模拟后台任务(真实场景应使用异步任务队列)
for ($i = 1; $i <= 100; $i++) {
Timer::add($i, function () use ($connection, $taskId, $i) {
$connection->send(json_encode(['type' => 'progress', 'task_id' => $taskId, 'progress' => $i]));
});
}
}
};
Worker::runAll();
优点: 全双工,实时性最高,功能最强。 缺点: 实现复杂,需要额外服务和端口;运维成本较高。
异步任务队列(生产环境推荐)
将耗时任务交给专业的队列系统处理,这是最健壮、可扩展的方案,可以结合数据库或 Redis 存储进度。
工具选择:
- 队列: Redis + php-resque / laravel-queue / beanstalkd
- 进度存储: Redis(推荐,支持原子操作)、MySQL
工作流程:
- 任务入队:前端请求将任务推入队列,并立即获得任务ID和初始状态(如
pending)。 - Worker 异步处理:后台 Worker 进程(独立于Web服务器)从队列中取出任务并执行。
- 更新进度:Worker 在每处理一部分后,更新 Redis 中该任务ID的进度值(
SET task:{id}:progress 45)。 - 前端轮询:前端通过另一个接口(如
/api/progress?task_id=xx)查询 Redis 中的进度。
示例(使用 Laravel 队列 + Redis):
// 1. 分发任务
$task = new ProcessDataJob($data);
$taskId = $task->job->getJobId(); // Laravel 自动生成Job ID
dispatch($task);
// 2. 在 Job 的 handle 方法中更新进度
public function handle()
{
$redis = Redis::connection();
$total = 1000;
foreach ($items as $index => $item) {
// 处理数据...
$progress = ($index + 1) / $total * 100;
$redis->set('task:' . $this->job->getJobId() . ':progress', $progress);
$redis->expire('task:' . $this->job->getJobId() . ':progress', 3600);
}
$redis->set('task:' . $this->job->getJobId() . ':progress', 100);
$redis->set('task:' . $this->job->getJobId() . ':result', json_encode($result));
}
// 3. 查询进度 API
public function getProgress($taskId)
{
$progress = Redis::get('task:' . $taskId . ':progress');
$result = Redis::get('task:' . $taskId . ':result');
return response()->json([
'progress' => $progress ?? 0,
'result' => $result ? json_decode($result, true) : null,
'status' => $progress == 100 ? 'completed' : 'processing'
]);
}
优点: 生产环境最稳定,可扩展性强,任务失败可重试。 缺点: 需要额外搭建队列服务(Redis、RabbitMQ等),架构相对复杂。
总结与推荐
| 方案 | 实时性 | 实现难度 | 服务器资源消耗 | 生产环境可靠度 | 推荐场景 |
|---|---|---|---|---|---|
| 文件/数据库 + 前端轮询 | 低(秒级) | ★☆☆☆☆ | 低 | 低 | 简单内部工具,演示项目 |
| SSE | 高(毫秒级) | ★★☆☆☆ | 中(长连接) | 中 | 简单的实时进度展示,单项目 |
| WebSocket | 最高 | ★★★★☆ | 中(长连接) | 高 | 需要双向通信,如聊天+进度 |
| 异步任务队列(推荐) | 中(秒级) | ★★★☆☆ | 中(独立Worker) | 最高 | 生产环境首选,企业级应用 |
对于大多数中型以上的PHP项目,最推荐的组合是:
- Laravel 队列 / Symfony Messenger + Redis
- 配合前端轮询(最简单)或 SSE(如果你追求更好的实时体验)。
最终建议:
- 如果是小项目或学习: 使用方案一(文件 + 轮询)。
- 如果是正式生产项目: 使用方案四(异步队列 + Redis),这是最符合工程化、最容易维护和扩展的方式。
- 如果需要非常高的实时性(如实时监控大盘): 考虑方案二(SSE),实现成本低于WebSocket。