本文目录导读:

- 流式下载(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 请求支持
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播