技术分享

Laravel Excel导入导出神器让效率翻倍

作者头像 人称外号大脸猫
15 阅读
Laravel Excel导入导出神器让效率翻倍

spatie/simple-excel 使您能够轻松地读写简单的 Excel 和 CSV 文件。在后台,系统会使用专门的生成工具来确保即使在处理大型文件时也能保持较低的内存占用率

为什么我选择spatie/simple-excel?

在尝试过多个Excel处理包后,我最终锁定了spatie/simple-excel,原因很实在:

轻量高效不臃肿 - 相比其他功能繁杂的包,它只做一件事:高效处理Excel数据 内存控制很出色 - 处理10万行数据也不会让服务器崩溃 API设计超简洁 - 学习成本低,半小时就能上手

实际测试对比:

  • 传统方式导出1万行数据:内存占用约256MB
  • simple-excel导出1万行数据:内存占用仅50MB左右

实战演练:从0到1实现数据导出

基础导出功能

安装只需要一行命令:

composer require spatie/simple-excel

然后实现导出逻辑:

public function exportCustomers()
{
    $customers = Customer::where('status', 'active')->get();
    
    $fileName = '活跃客户_' . date('Ymd_His') . '.xlsx';
    
    return SimpleExcelWriter::streamDownload($fileName)
        ->addHeader(['ID', '姓名', '邮箱', '手机号', '注册时间'])
        ->addRows($customers->map(function ($customer) {
            return [
                $customer->id,
                $customer->name, 
                $customer->email,
                $customer->phone,
                $customer->created_at->format('Y-m-d')
            ];
        }))
        ->toBrowser();
}

高级功能:智能筛选导出

实际业务中,我们经常需要按条件导出:

public function exportByCondition(Request $request)
{
    // 构建动态查询
    $query = Customer::query();
    
    if ($request->company) {
        $query->where('company', 'like', "%{$request->company}%");
    }
    
    if ($request->start_date) {
        $query->where('created_at', '>=', $request->start_date);
    }
    
    $fileName = '筛选客户_' . date('Ymd_His') . '.csv';
    
    return response()->streamDownload(function () use ($query) {
        $writer = SimpleExcelWriter::streamDownload('customers.csv')
            ->addHeader(['姓名', '邮箱', '公司', '注册时间']);
            
        // 分块处理大数据,避免内存溢出
        $query->chunk(2000, function ($customers) use ($writer) {
            foreach ($customers as $customer) {
                $writer->addRow([
                    $customer->name,
                    $customer->email,
                    $customer->company,
                    $customer->created_at->format('Y-m-d H:i:s')
                ]);
            }
        });
        
        $writer->close();
    }, $fileName);
}

数据导入:从Excel到数据库的无缝衔接

完整的导入解决方案

导入功能需要考虑数据验证、错误处理等复杂情况:

class CustomerImport 
{
    public function import($filePath)
    {
        $errors = [];
        $successCount = 0;
        
        SimpleExcelReader::create($filePath)
            ->getRows()
            ->each(function (array $row, int $rowNumber) use (&$errors, &$successCount) {
                // 跳过表头行
                if ($rowNumber === 1) return;
                
                // 数据验证
                $validator = Validator::make($row, [
                    '邮箱' => 'required|email|unique:customers,email',
                    '姓名' => 'required|max:50',
                    '手机号' => 'nullable|max:20'
                ], [
                    '邮箱.required' => '邮箱地址不能为空',
                    '邮箱.unique' => '邮箱地址已存在'
                ]);
                
                if ($validator->fails()) {
                    $errors[] = "第{$rowNumber}行错误: " . implode(',', $validator->errors()->all());
                    return;
                }
                
                // 保存到数据库
                try {
                    Customer::create([
                        'name' => $row['姓名'],
                        'email' => $row['邮箱'],
                        'phone' => $row['手机号'] ?? null,
                        'company' => $row['公司'] ?? null
                    ]);
                    $successCount++;
                } catch (\Exception $e) {
                    $errors[] = "第{$rowNumber}行保存失败: " . $e->getMessage();
                }
            });
            
        return [
            'success' => $successCount, 
            'errors' => $errors,
            'total' => $successCount + count($errors)
        ];
    }
}

提升用户体验的实用技巧

1. 提供标准模板下载

减少用户因格式问题导致的导入失败:

public function downloadTemplate()
{
    $fileName = '客户数据导入模板.xlsx';
    $filePath = storage_path('templates/' . $fileName);
    
    SimpleExcelWriter::create($filePath)
        ->addHeader(['姓名*', '邮箱*', '手机号', '公司', '地址'])
        ->addRow(['张三', 'zhangsan@example.com', '13800138000', '某某科技', '北京市朝阳区'])
        ->addRow(['李四', 'lisi@example.com', '13900139000', '某某贸易', '上海市浦东新区'])
        ->addRow(['*为必填字段', '邮箱不能重复', '', '', '']);
        
    return response()->download($filePath)->deleteFileAfterSend(true);
}

2. 前端进度显示和结果反馈

// 上传显示进度条
const importCustomers = async (file) => {
    const formData = new FormData();
    formData.append('excel_file', file);
    
    try {
        const response = await axios.post('/api/customers/import', formData, {
            headers: { 'Content-Type': 'multipart/form-data' },
            onUploadProgress: (progressEvent) => {
                const percent = Math.round(
                    (progressEvent.loaded * 100) / progressEvent.total
                );
                // 更新进度条显示
                updateProgressBar(percent);
            }
        });
        
        if (response.data.success > 0) {
            showSuccess(`成功导入 ${response.data.success} 条客户数据`);
        }
        
        if (response.data.errors && response.data.errors.length > 0) {
            showErrorDetails(response.data.errors);
        }
    } catch (error) {
        alert('导入失败,请检查文件格式或联系管理员');
    }
};

性能优化:处理海量数据的秘诀

内存优化实战

当需要导出几十万条数据时,内存管理至关重要:

public function exportLargeData()
{
    $fileName = '全量客户数据_' . date('Ymd_His') . '.csv';
    
    return response()->streamDownload(function () {
        $writer = SimpleExcelWriter::streamDownload('large_export.csv')
            ->addHeader(['ID', '姓名', '邮箱', '注册时间', '状态']);
        
        // 使用chunk分批处理,每次处理5000条
        Customer::chunk(5000, function ($customers) use ($writer) {
            foreach ($customers as $customer) {
                $writer->addRow([
                    $customer->id,
                    $customer->name,
                    $customer->email,
                    $customer->created_at->format('Y-m-d'),
                    $customer->status
                ]);
            }
            
            // 每处理完一批就刷新输出缓冲区
            if (ob_get_level() > 0) {
                ob_flush();
            }
            flush();
        });
        
        $writer->close();
    }, $fileName);
}

队列处理超大数据导入

对于特别大的文件,使用队列异步处理:

class ProcessLargeImport implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
    
    public $filePath;
    public $userId;
    
    public function __construct($filePath, $userId)
    {
        $this->filePath = $filePath;
        $this->userId = $userId;
    }
    
    public function handle()
    {
        $importer = new CustomerImport();
        $result = $importer->import(storage_path('app/' . $this->filePath));
        
        // 保存导入结果
        ImportResult::create([
            'user_id' => $this->userId,
            'file_path' => $this->filePath,
            'success_count' => $result['success'],
            'error_count' => count($result['errors']),
            'error_details' => $result['errors']
        ]);
        
        // 发送通知
        Notification::send(
            User::find($this->userId), 
            new ImportFinished($result)
        );
        
        // 清理临时文件
        Storage::delete($this->filePath);
    }
}

避坑指南:常见问题解决方案

中文乱码问题

// 设置正确的编码
$writer = SimpleExcelWriter::create($filePath)
    ->useEncoding('UTF-8');

日期格式混乱

// 统一格式化日期输出
->addRow([
    $order->order_number,
    $order->created_at->format('Y年m月d日 H时i分'), // 中文格式
    $order->amount
])

超时问题处理

// 增加执行时间限制
set_time_limit(600); // 10分钟
ini_set('memory_limit', '512M');

真实案例:效率提升明显

在我们最近的项目中,使用spatie/simple-excel后:

✅ 客户数据导出时间从15分钟减少到30秒 ✅ 5000条数据导入从手动2小时变为自动2分钟
✅ 内存占用减少80%,服务器更稳定 ✅ 错误率从5%降到0.1%以下

总结

spatie/simple-excel之所以成为我的首选,是因为它在性能、易用性、稳定性三个方面都表现出色。无论是简单的数据导出,还是复杂的数据导入验证,它都能提供优雅的解决方案。

关键收获:

  1. 流式处理是大数据导出的关键
  2. 队列异步处理提升用户体验
  3. 完善的错误处理让系统更健壮
  4. 提供模板下载能显著减少用户错误

如果你也在为Excel数据处理烦恼,不妨试试spatie/simple-excel,相信它会成为你的得力助手!