本文目录导读:

- 流式下载(Streamed Download)
- 流式响应(Streamed Response)
- 大文件下载 - 控制器中使用 Response
- 前端 JavaScript 配合使用
- 服务器配置注意事项
- 最佳实践建议
在 Laravel 中实现文件流式响应有几种主要方式,下面是完整的方案对比和代码示例:
流式下载(Streamed Download)
适用于用户下载大文件,不需要加载到内存:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Response;
use Illuminate\Support\Facades\Storage;
class FileDownloadController extends Controller
{
/**
* 流式下载 - 服务器本地文件
*/
public function downloadLocalFile()
{
$filePath = storage_path('app/files/large-file.zip');
$fileName = 'large-file.zip';
return response()->streamDownload(function () use ($filePath) {
$stream = fopen($filePath, 'r');
// 分块输出文件内容
while (!feof($stream)) {
echo fread($stream, 1024 * 1024); // 每次读取1MB
flush();
ob_flush();
}
fclose($stream);
}, $fileName);
}
/**
* 流式下载 - 云存储文件
*/
public function downloadFromCloud()
{
$disk = Storage::disk('s3');
$filePath = 'path/to/large-file.zip';
$fileName = 'large-file.zip';
return response()->streamDownload(function () use ($disk, $filePath) {
$stream = $disk->readStream($filePath);
if ($stream) {
while (!feof($stream)) {
echo fread($stream, 1024 * 1024);
flush();
ob_flush();
}
fclose($stream);
}
}, $fileName);
}
/**
* 流式下载 - 带进度条的下载
*/
public function downloadWithProgress()
{
$filePath = storage_path('app/files/large-file.zip');
$fileName = 'large-file.zip';
$fileSize = filesize($filePath);
return response()->streamDownload(function () use ($filePath, $fileSize) {
$stream = fopen($filePath, 'r');
$downloaded = 0;
while (!feof($stream)) {
$chunk = fread($stream, 1024 * 1024);
echo $chunk;
$downloaded += strlen($chunk);
// 进度百分比可以记录到 session 或缓存中
Cache::put('download_progress', ($downloaded / $fileSize) * 100, 300);
flush();
ob_flush();
}
fclose($stream);
Cache::forget('download_progress');
}, $fileName, [
'Content-Type' => 'application/octet-stream',
'Content-Length' => $fileSize
]);
}
}
流式响应(Streamed Response)
适用于实时生成数据响应:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class StreamController extends Controller
{
/**
* 实时日志流 - 类似 tail -f
*/
public function streamLogs()
{
$logFile = storage_path('logs/laravel.log');
return response()->stream(function () use ($logFile) {
// 设置无缓冲输出
while (true) {
clearstatcache();
if (file_exists($logFile)) {
$content = file_get_contents($logFile);
echo $content . PHP_EOL;
}
// 刷新输出缓冲区
ob_flush();
flush();
// 等待一秒再检查
sleep(1);
}
}, 200, [
'Content-Type' => 'text/plain',
'Cache-Control' => 'no-cache',
'X-Accel-Buffering' => 'no', // 禁用 Nginx 缓冲
]);
}
/**
* 实时数据推送 - SSE (Server-Sent Events)
*/
public function streamSse()
{
return response()->stream(function () {
$data = [
'status' => 'processing',
'progress' => 0
];
for ($i = 1; $i <= 100; $i++) {
echo "data: " . json_encode([
'status' => 'processing',
'progress' => $i
]) . "\n\n";
ob_flush();
flush();
usleep(100000); // 100ms
}
echo "data: " . json_encode([
'status' => 'completed',
'progress' => 100
]) . "\n\n";
}, 200, [
'Content-Type' => 'text/event-stream',
'Cache-Control' => 'no-cache',
'X-Accel-Buffering' => 'no',
]);
}
/**
* 流式生成 CSV 导出
*/
public function streamCsvExport()
{
return response()->stream(function () {
// 创建输出流
$output = fopen('php://output', 'w');
// 写入表头
fputcsv($output, ['ID', 'Name', 'Email', 'Created At']);
// 分页获取数据并写入
User::chunk(100, function ($users) use ($output) {
foreach ($users as $user) {
fputcsv($output, [
$user->id,
$user->name,
$user->email,
$user->created_at
]);
}
// 刷新输出
ob_flush();
flush();
});
fclose($output);
}, 200, [
'Content-Type' => 'text/csv',
'Content-Disposition' => 'attachment; filename="users.csv"',
]);
}
}
大文件下载 - 控制器中使用 Response
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\StreamedResponse;
class LargeFileController extends Controller
{
/**
* 使用 Symfony StreamedResponse
*/
public function downloadLargeFile()
{
$filePath = storage_path('app/files/video.mp4');
return new StreamedResponse(function () use ($filePath) {
$stream = fopen($filePath, 'rb');
// 设置合适的缓冲区大小
$bufferSize = 1024 * 1024; // 1MB
while (!feof($stream)) {
$data = fread($stream, $bufferSize);
echo $data;
// 立即发送数据
if (ob_get_level() > 0) {
ob_flush();
}
flush();
}
fclose($stream);
}, 200, [
'Content-Type' => 'video/mp4',
'Content-Disposition' => 'attachment; filename="movie.mp4"',
'Content-Length' => filesize($filePath),
'Accept-Ranges' => 'bytes',
'Cache-Control' => 'public, max-age=3600',
]);
}
/**
* 支持断点续传的下载
*/
public function resumeDownload(Request $request)
{
$filePath = storage_path('app/files/video.mp4');
$fileSize = filesize($filePath);
// 获取 Range 请求头
$range = $request->header('Range');
if ($range) {
// 解析 Range: bytes=start-end
if (preg_match('/bytes=(\d+)-(\d*)/', $range, $matches)) {
$start = intval($matches[1]);
$end = !empty($matches[2]) ? intval($matches[2]) : $fileSize - 1;
if ($start >= $fileSize || $start > $end) {
return response('Invalid range', 416);
}
$length = $end - $start + 1;
return response()->stream(function () use ($filePath, $start, $length) {
$stream = fopen($filePath, 'rb');
fseek($stream, $start);
echo fread($stream, $length);
fclose($stream);
}, 206, [
'Content-Type' => 'video/mp4',
'Content-Length' => $length,
'Content-Range' => "bytes {$start}-{$end}/{$fileSize}",
'Accept-Ranges' => 'bytes',
]);
}
}
// 完整下载
return $this->downloadLargeFile();
}
}
前端 JavaScript 配合使用
// 前端处理流式下载
const downloadLargeFile = async (url) => {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error('Download failed');
}
// 创建 blob 对象
const blob = await response.blob();
// 创建下载链接
const downloadUrl = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = downloadUrl;
a.download = 'large-file.zip';
document.body.appendChild(a);
a.click();
// 清理
window.URL.revokeObjectURL(downloadUrl);
document.body.removeChild(a);
} catch (error) {
console.error('Download error:', error);
}
};
// SSE 前端示例
const eventSource = new EventSource('/api/stream/sse');
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log('Progress:', data.progress);
// 更新进度条
updateProgressBar(data.progress);
if (data.status === 'completed') {
eventSource.close();
}
};
服务器配置注意事项
Nginx 配置
# 禁用缓冲
location /api/stream/ {
proxy_buffering off;
proxy_cache off;
chunked_transfer_encoding on;
proxy_set_header Connection '';
proxy_http_version 1.1;
proxy_read_timeout 3600;
}
# 大文件下载
location /downloads/ {
alias /path/to/files/;
internal; # 内部访问
limit_rate 10m; # 限速
}
Apache 配置
SetEnv no-gzip 1 SetEnv dont-vary 1
最佳实践建议
- 内存使用:使用较小的缓冲区(如 1MB)避免内存溢出
- 超时处理:适当调整 PHP 脚本执行时间
- 断点续传:如果文件很大,建议实现 Range 请求支持
- 进度跟踪:对于耗时操作,使用 SSE 或 WebSocket 提供进度
- 安全考虑:验证用户权限,防止任意文件下载
选择方案的最佳依据:
- 小文件 (<10MB):使用
Storage::download() - 大文件下载:使用
streamDownload() - 实时数据推送:使用
stream()配合 SSE - 需要断点续传:实现 Range 请求支持