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

Laravel使用Maatwebsite导入Excel时如何在Blade页面显示进度条

Laravel Excel 导入实时进度条实现方案

前置说明

10万条数据同步导入必然触发请求超时,所以必须采用「异步队列处理+缓存存储进度+前端轮询拉取」的方案实现,以下是完整实现步骤:


第一步:改造导入逻辑支持进度上报

1.1 新建导入类

执行命令生成导入类:php artisan make:import ClientsImport
导入类代码如下,通过Maatwebsite的事件钩子监听导入进度,写入缓存:

<?php
namespace App\Imports;

use App\Models\Client;
use Maatwebsite\Excel\Concerns\ToModel;
use Maatwebsite\Excel\Concerns\WithChunkReading;
use Maatwebsite\Excel\Concerns\WithEvents;
use Maatwebsite\Excel\Events\AfterChunk;
use Maatwebsite\Excel\Events\BeforeImport;
use PhpOffice\PhpSpreadsheet\Shared\Date;

class ClientsImport implements ToModel, WithChunkReading, WithEvents
{
    protected $importId;

    public function __construct(string $importId)
    {
        $this->importId = $importId;
        // 初始化进度缓存,有效期1小时
        cache()->set("import_progress:{$this->importId}", [
            'total' => 0,
            'processed' => 0,
            'status' => 'processing'
        ], 3600);
    }

    public function model(array $row)
    {
        $row['entrydate'] = Date::excelToDateTimeObject($row['entrydate'])->format('Y-m-d');
        return new Client($row);
    }

    public function registerEvents(): array
    {
        return [
            // 导入开始前获取总行数
            BeforeImport::class => function(BeforeImport $event) {
                $totalRows = $event->getReader()->getTotalRows();
                $total = reset($totalRows); // 取第一个工作表的总行数
                cache()->set("import_progress:{$this->importId}", [
                    'total' => $total,
                    'processed' => 0,
                    'status' => 'processing'
                ], 3600);
            },
            // 每处理完一个分块更新进度
            AfterChunk::class => function(AfterChunk $event) {
                $progress = cache()->get("import_progress:{$this->importId}");
                $progress['processed'] += $this->chunkSize();
                $progress['processed'] = min($progress['processed'], $progress['total']); // 避免计数溢出
                cache()->set("import_progress:{$this->importId}", $progress, 3600);
            }
        ];
    }

    // 分块大小可根据服务器性能调整,建议500-2000区间
    public function chunkSize(): int
    {
        return 1000;
    }
}

1.2 新建队列处理任务

执行命令生成队列任务:php artisan make:job ProcessExcelImport
任务代码如下,异步执行导入逻辑,处理完成/失败时更新缓存状态:

<?php
namespace App\Jobs;

use App\Imports\ClientsImport;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Maatwebsite\Excel\Facades\Excel;

class ProcessExcelImport implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    protected $filePath;
    protected $importId;
    public $timeout = 300; // 设置超时时间为5分钟,可根据实际情况调整

    public function __construct(string $filePath, string $importId)
    {
        $this->filePath = $filePath;
        $this->importId = $importId;
    }

    public function handle()
    {
        Excel::import(new ClientsImport($this->importId), $this->filePath);
        // 导入完成更新状态
        $progress = cache()->get("import_progress:{$this->importId}");
        $progress['status'] = 'completed';
        $progress['processed'] = $progress['total'];
        cache()->set("import_progress:{$this->importId}", $progress, 3600);
        // 可选:导入完成后删除临时文件
        unlink(storage_path('app/' . $this->filePath));
    }

    // 导入失败更新状态
    public function failed(\Throwable $e)
    {
        cache()->set("import_progress:{$this->importId}", [
            'status' => 'failed',
            'message' => $e->getMessage()
        ], 3600);
    }
}

第二步:新增业务路由

在routes/web.php中添加2个路由,分别用于提交导入任务、查询导入进度:

use Illuminate\Http\Request;
use Illuminate\Support\Str;
use App\Jobs\ProcessExcelImport;

// 提交导入任务
Route::post('/import/excel', function (Request $request) {
    $request->validate([
        'excel_file' => 'required|file|mimes:xlsx,xls'
    ]);
    // 生成唯一导入ID
    $importId = Str::uuid()->toString();
    // 存储上传的文件到临时目录
    $filePath = $request->file('excel_file')->store('temp/imports');
    // 调度队列任务
    ProcessExcelImport::dispatch($filePath, $importId);
    return response()->json(['import_id' => $importId]);
});

// 查询导入进度
Route::get('/import/progress/{importId}', function (string $importId) {
    $progress = cache()->get("import_progress:{$importId}");
    if (!$progress) return response()->json(['status' => 'not_found'], 404);
    return response()->json($progress);
});

第三步:Blade视图实现进度条

前端逻辑:提交文件获取导入ID后,定时轮询进度接口更新进度条样式:

<!DOCTYPE html>
<html>
<head>
    <title>Excel导入</title>
    <meta name="csrf-token" content="{{ csrf_token() }}">
    <style>
        .progress-container {
            display: none;
            width: 100%;
            height: 20px;
            background: #e5e7eb;
            border-radius: 10px;
            overflow: hidden;
            margin: 20px 0;
        }
        .progress-bar {
            height: 100%;
            width: 0%;
            background: #10b981;
            transition: width 0.3s ease;
        }
        .progress-text {
            text-align: center;
            margin-top: 5px;
        }
    </style>
</head>
<body>
    <div class="container">
        <input type="file" id="excel_file" accept=".xlsx,.xls">
        <button id="import_btn">开始导入</button>

        <div class="progress-container" id="progress_container">
            <div class="progress-bar" id="progress_bar"></div>
        </div>
        <div class="progress-text" id="progress_text"></div>
        <div id="result_msg"></div>
    </div>

    <script>
        document.getElementById('import_btn').addEventListener('click', function () {
            const fileInput = document.getElementById('excel_file');
            if (!fileInput.files[0]) {
                alert('请先选择Excel文件');
                return;
            }

            const formData = new FormData();
            formData.append('excel_file', fileInput.files[0]);
            formData.append('_token', document.querySelector('meta[name="csrf-token"]').content);

            // 提交导入任务
            fetch('/import/excel', {
                method: 'POST',
                body: formData
            }).then(res => res.json())
            .then(data => {
                const importId = data.import_id;
                document.getElementById('progress_container').style.display = 'block';
                const progressBar = document.getElementById('progress_bar');
                const progressText = document.getElementById('progress_text');
                const resultMsg = document.getElementById('result_msg');

                // 每1秒轮询一次进度
                const timer = setInterval(() => {
                    fetch(`/import/progress/${importId}`)
                    .then(res => res.json())
                    .then(progress => {
                        if (progress.status === 'completed') {
                            clearInterval(timer);
                            progressBar.style.width = '100%';
                            progressText.innerText = '导入完成,共处理' + progress.total + '条数据';
                            resultMsg.innerText = '导入成功';
                            return;
                        }
                        if (progress.status === 'failed') {
                            clearInterval(timer);
                            progressBar.style.background = '#ef4444';
                            progressText.innerText = '导入失败:' + progress.message;
                            return;
                        }
                        // 计算进度百分比
                        const percent = progress.total > 0 ? Math.round((progress.processed / progress.total) * 100) : 0;
                        progressBar.style.width = percent + '%';
                        progressText.innerText = `已处理${progress.processed}/${progress.total}条,进度${percent}%`;
                    })
                }, 1000);
            });
        });
    </script>
</body>
</html>

性能优化建议

  • 原代码单条create插入10万条数据性能极低,建议将导入类改为ToCollection接口,攒够1000条记录后批量插入,导入速度可提升10倍以上
  • 队列驱动建议改用Redis,比database驱动性能高很多,适合处理大流量任务
  • 对体验要求高的场景可将前端轮询改为WebSocket推送,避免频繁请求接口
  • 可根据服务器配置调整分块大小和队列超时时间,避免任务异常中断

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.03 13:57:05