最可控的excel导出方式是用phpspreadsheet手动实现,需严格处理响应头、输出缓冲和中文编码,避免文件损坏或乱码,并针对大数据量采用分sheet、批量填充和临时文件等策略。

直接用 PhpOffice\PhpSpreadsheet 手动写 Excel 文件最可控,Yii 本身不内置导出能力,所谓“一键导出”都是封装或扩展的幻觉——底层逃不开设置响应头、避免输出污染、处理中文和大文件这三关。
导出时 Excel 打不开提示“文件损坏”
90% 是响应头没设对,或控制器里有意外输出(比如 var_dump、空格、调试日志、布局模板被渲染)。
-
Content-Type必须是application/vnd.openxmlformats-officedocument.spreadsheetml.sheet(.xlsx)或application/vnd.ms-excel(.xls),不能只写application/octet-stream - 必须手动加
Content-Disposition: attachment; filename="xxx.xlsx",sendFile()不会自动加这个头 - 中文文件名要用
rawurlencode()编码,否则 Safari 和旧 IE 直接拒收:'filename="'.rawurlencode($fileName).'"' - 导出前务必调
ob_end_clean(),清掉 Yii 可能已开启的输出缓冲 - 别在
action里 echo 任何东西,也别让异常堆栈落到响应体里
中文乱码或报 mb_strlen(): Unknown encoding "UTF-8"
不是 header 没设 charset,也不是 mb_convert_encoding() 能救的——根本原因是 PHPExcel 或早期 phpspreadsheet 实例没设默认编码,且项目缺失 mbstring 扩展。
- 确认
php -m | grep mbstring输出存在,否则所有多字节字符串操作都会崩 - 创建
Spreadsheet后立刻设字体和编码:$spreadsheet->getDefaultStyle()->getFont()->setName('Microsoft YaHei');$spreadsheet->getProperties()->setCreator('Yii2') - 不要依赖
setEncoding('UTF-8')这类不存在的方法,phpspreadsheet没这个 API - 字段含 emoji 或中文标点时,确保数据库查询结果本身就是 UTF-8 编码(查
SHOW CREATE TABLE确认表字符集)
大数据量(5 万行以上)内存溢出或超时
phpspreadsheet 是全内存模型,10 万行 × 15 列很容易吃光 2GB 内存,setPreCalculateFormulas(false) 只减缓,不根治。
- 禁用分页:用
'pagination' => false确保ActiveDataProvider拉全量数据,但别指望它解决内存问题 - 分 Sheet 导出:每 4 万行新建一个
$spreadsheet->createSheet(),并用$sheet->setTitle('Data_1')命名 - 避免
setCellValue()循环写入:改用fromArray()批量填充二维数组,快 3–5 倍 - 导出前调
ini_set('memory_limit', '2048M')和set_time_limit(0),仅临时兜底,不能替代分片 - 真正海量数据(如百万级)建议走队列 + 临时文件 + 前端轮询下载链接,别卡在 HTTP 请求里
用 sendFile 还是直接输出到 php://output
优先选 sendFile(),php://output 在 Yii 控制器里极易被布局、Gzip、调试工具污染,导致 Excel 二进制损坏。
- 先写临时文件:
$tmpFile = sys_get_temp_dir() . '/export_' . uniqid() . '.xlsx' - 用
Xlsxwriter 写入后立即sendFile($tmpFile, ['mimeType' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet']) - sendFile 后立刻
unlink($tmpFile),别等afterAction—— 用户关浏览器就漏删 - 小文件(≤5MB)用
sendFile完全够用;大文件若配了 Nginx 的X-Accel-Redirect,可上streamFile,否则不如老实写临时文件 - 绝对不要在
sendFile后写exit或die,它内部已终止脚本
最容易被忽略的是:导出逻辑一旦混进 GridView 的 toolbar 或其他组件回调里,就极难控制输出时机和缓冲状态。真要稳定,就老老实实写独立 actionExport,清缓冲、设头、写文件、发响应,四步闭环,少绕弯。











