PHP项目Laravel文件流式响应怎样

wen PHP项目 5

本文目录导读:

PHP项目Laravel文件流式响应怎样

  1. 流式下载(Streamed Download)
  2. 流式响应(Streamed Response)
  3. 大文件下载 - 控制器中使用 Response
  4. 前端 JavaScript 配合使用
  5. 服务器配置注意事项
  6. 最佳实践建议

在 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

最佳实践建议

  1. 内存使用:使用较小的缓冲区(如 1MB)避免内存溢出
  2. 超时处理:适当调整 PHP 脚本执行时间
  3. 断点续传:如果文件很大,建议实现 Range 请求支持
  4. 进度跟踪:对于耗时操作,使用 SSE 或 WebSocket 提供进度
  5. 安全考虑:验证用户权限,防止任意文件下载

选择方案的最佳依据:

  • 小文件 (<10MB):使用 Storage::download()
  • 大文件下载:使用 streamDownload()
  • 实时数据推送:使用 stream() 配合 SSE
  • 需要断点续传:实现 Range 请求支持

抱歉,评论功能暂时关闭!