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
相关产品推荐
相关产品推荐

