You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Laravel执行Ghostscript进程致浏览器卡顿的解决方案咨询

解决Laravel 10中PDF压缩页面卡顿及进程终止问题

一、核心问题分析

你当前使用Process->run()属于同步阻塞执行,会导致HTTP请求一直等待Ghostscript进程完成,因此页面会卡顿直到压缩结束。同时,由于进程依附于当前请求进程,页面刷新时原请求进程已脱离前端控制,无法直接终止。

二、优化方案:异步处理+进程管理

1. 改用异步执行避免页面卡顿

将同步的run()替换为异步的start(),同时存储进程ID(PID)以便后续控制:

use Illuminate\Support\Facades\Cache;
use Symfony\Component\Process\Process;

$cmd = "gs -sDEVICE=pdfwrite -dCompatibilityLevel=1.4 -dPDFSETTINGS=/ebook -dNOPAUSE -dQUIET -dBATCH -sOutputFile={$output} {$input}";
// 用fromShellCommandline避免文件名含空格时的命令分割错误
$process = Process::fromShellCommandline($cmd);
$process->setTimeout(3600);
$process->start();

// 将PID存入缓存,关联用户会话标识
$processKey = 'pdf_compress_' . session()->getId();
Cache::put($processKey, $process->getPid(), 3600);

2. 前端轮询+进程终止功能

  • 状态查询接口:创建接口供前端轮询压缩状态:
public function checkStatus()
{
    $processKey = 'pdf_compress_' . session()->getId();
    $pid = Cache::get($processKey);
    
    if (!$pid) {
        return response()->json(['status' => 'finished']);
    }
    
    // 检查进程是否存活
    $isRunning = Process::isRunning($pid);
    if (!$isRunning) {
        Cache::forget($processKey);
        return response()->json(['status' => 'finished']);
    }
    
    return response()->json(['status' => 'running']);
}
  • 终止进程接口:用户刷新或主动终止时,调用接口结束进程:
public function terminateProcess()
{
    $processKey = 'pdf_compress_' . session()->getId();
    $pid = Cache::get($processKey);
    
    if ($pid && Process::isRunning($pid)) {
        // 根据系统类型执行终止命令
        if (PHP_OS_FAMILY === 'Windows') {
            Process::fromShellCommandline("taskkill /F /PID {$pid}")->run();
        } else {
            Process::fromShellCommandline("kill {$pid}")->run();
        }
        Cache::forget($processKey);
        return response()->json(['success' => true]);
    }
    
    return response()->json(['success' => false]);
}
  • 前端逻辑:页面加载后轮询状态,刷新前触发终止接口:
// 轮询压缩状态
let pollInterval = setInterval(async () => {
    const res = await fetch('/pdf-compress/check-status');
    const data = await res.json();
    if (data.status === 'finished') {
        clearInterval(pollInterval);
        // 处理压缩完成后的逻辑,比如提示下载
    }
}, 1000);

// 页面刷新/关闭时终止进程
window.addEventListener('beforeunload', async () => {
    await fetch('/pdf-compress/terminate', { method: 'POST' });
    clearInterval(pollInterval);
});

3. 更稳健的长耗时处理:队列异步执行

若PDF压缩耗时较长,建议用Laravel队列彻底解耦HTTP请求与压缩进程:

  • 创建压缩任务:
namespace App\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Symfony\Component\Process\Process;

class CompressPdf implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
    
    protected $inputPath;
    protected $outputPath;
    
    public function __construct(string $inputPath, string $outputPath)
    {
        $this->inputPath = $inputPath;
        $this->outputPath = $outputPath;
    }
    
    public function handle()
    {
        $cmd = "gs -sDEVICE=pdfwrite -dCompatibilityLevel=1.4 -dPDFSETTINGS=/ebook -dNOPAUSE -dQUIET -dBATCH -sOutputFile={$this->outputPath} {$this->inputPath}";
        $process = Process::fromShellCommandline($cmd);
        $process->setTimeout(3600);
        $process->run();
    }
}
  • 触发队列任务:
use App\Jobs\CompressPdf;

// 触发任务后立即返回响应,压缩在后台执行
CompressPdf::dispatch($input, $output);
return response()->json(['status' => 'started']);

队列方案下,用户刷新页面不影响后台进程;若需终止,可在任务中记录PID到数据库/缓存,通过接口匹配任务ID终止对应进程。

三、注意事项

  • 确保服务器具备Ghostscript执行权限,且$input、$output路径有读写权限。
  • 缓存存储PID时,用用户会话或唯一请求ID关联,避免不同用户进程冲突。
  • 队列处理需配置好Laravel队列驱动(如Redis、Database),并启动队列worker。

内容的提问来源于stack exchange,提问作者Toni Joe

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.06 16:32:36