You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在React前端正确展示Laravel返回的StreamedResponse图片?

解决方案

一、React中正确展示私密文件的方法

你的核心问题是没有正确获取后端返回的文件Blob数据,导致后续生成的URL指向文件名而非实际文件内容。以下是修正步骤:

1. 修正useAttachment钩子,正确获取Blob响应

axios默认会把响应解析为JSON,需指定responseType: 'blob'来获取原始文件流:

import { useEffect, useState } from 'react'
import { api } from '@utils/useAxios'

const useAttachment = (files: any[] | undefined) => {
  const [attachments, setAttachments] = useState<Blob[]>([])

  const handleRequest = async (fileName: string) => {
    try {
      // 指定responseType为blob,确保拿到文件二进制数据
      const resp = await api().post(
        'api/attachment', 
        { file_name: fileName }, 
        { responseType: 'blob' }
      )
      // 存储Blob对象而非响应对象
      setAttachments(prev => [...prev, resp.data])
    } catch (err) {
      console.error('获取附件失败:', err)
    }
  }

  useEffect(() => {
    if (!files) return
    // 清空旧的附件数据
    setAttachments([])
    files.forEach(async att => {
      await handleRequest(att.file_name)
    })
  }, [files])

  return { attachments }
}

export default useAttachment

2. 正确生成图片预览URL

使用URL.createObjectURL生成可访问的Blob URL,同时注意组件卸载时清理URL避免内存泄漏:

import { useEffect, useState } from 'react'
import useAttachment from './useAttachment'

const FilePreview = ({ files }: { files: any[] | undefined }) => {
  const [previews, setPreviews] = useState<string[]>([])
  const { attachments } = useAttachment(files)

  useEffect(() => {
    if (!attachments.length) return
    
    // 生成Blob URL并存储
    const newPreviews = attachments.map(blob => URL.createObjectURL(blob))
    setPreviews(newPreviews)

    // 组件卸载时清理所有Blob URL
    return () => {
      newPreviews.forEach(url => URL.revokeObjectURL(url))
    }
  }, [attachments])

  return (
    <div>
      {previews.map((src, index) => (
        <img 
          key={index} 
          src={src} 
          alt={`附件${index+1}`} 
          style={{ width: '200px', margin: '10px' }}
        />
      ))}
    </div>
  )
}

export default FilePreview

如果是文档类文件(如PDF、Word),可以用<a>标签提供下载链接:

{previews.map((src, index) => (
  <a 
    key={index} 
    href={src} 
    download={files[index].file_name}
    style={{ display: 'block', margin: '5px' }}
  >
    下载附件 {index+1}: {files[index].file_name}
  </a>
))}

二、当前方案的性能问题优化

当前逐个请求文件的方案在文件数量较多时,会引发请求风暴导致服务器压力过大,以下是替代优化方案:

1. 后端提供批量获取接口

最有效的方式是新增批量获取接口,前端一次传入多个文件名,后端返回打包后的ZIP文件:

  • 后端示例(Laravel):
public function batchDownload(Request $request) {
    $fileNames = $request->input('file_names');
    $zip = new ZipArchive();
    $zipName = 'attachments.zip';
    
    if ($zip->open(storage_path($zipName), ZipArchive::CREATE) === TRUE) {
        foreach ($fileNames as $fileName) {
            $filePath = Storage::path($fileName);
            if (file_exists($filePath)) {
                $zip->addFile($filePath, basename($fileName));
            }
        }
        $zip->close();
        
        return response()->download(storage_path($zipName))->deleteFileAfterSend(true);
    }
    
    return response()->json(['error' => '打包失败'], 500);
}
  • 前端调用:
const handleBatchDownload = async () => {
  const fileNames = files?.map(f => f.file_name) || []
  const resp = await api().post(
    'api/attachment/batch', 
    { file_names: fileNames }, 
    { responseType: 'blob' }
  )
  const url = URL.createObjectURL(resp.data)
  const a = document.createElement('a')
  a.href = url
  a.download = 'attachments.zip'
  a.click()
  URL.revokeObjectURL(url)
}

2. 实现图片懒加载

如果是列表展示大量图片,只加载当前可视区域内的文件,减少初始请求量:

const LazyImage = ({ blob, alt }) => {
  const [isLoaded, setIsLoaded] = useState(false)
  const imgRef = useRef<HTMLImageElement>(null)

  useEffect(() => {
    const observer = new IntersectionObserver(entries => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          setIsLoaded(true)
          observer.unobserve(entry.target)
        }
      })
    })
    if (imgRef.current) observer.observe(imgRef.current)
    return () => observer.disconnect()
  }, [])

  return (
    <div ref={imgRef} style={{ width: '200px', height: '200px', margin: '10px' }}>
      {isLoaded ? (
        <img src={URL.createObjectURL(blob)} alt={alt} />
      ) : (
        <div>加载中...</div>
      )}
    </div>
  )
}

3. 增加前端缓存

对已加载的文件进行缓存,避免重复请求:

// 用useRef存储全局缓存
const fileCache = useRef<Record<string, Blob>>({})

const handleRequest = async (fileName: string) => {
  // 先检查缓存
  if (fileCache.current[fileName]) {
    setAttachments(prev => [...prev, fileCache.current[fileName]])
    return
  }
  
  try {
    const resp = await api().post(
      'api/attachment', 
      { file_name: fileName }, 
      { responseType: 'blob' }
    )
    fileCache.current[fileName] = resp.data
    setAttachments(prev => [...prev, resp.data])
  } catch (err) {
    console.error('获取附件失败:', err)
  }
}

4. 后端优化

  • 设置合理的缓存头:返回文件时添加Cache-Control头,让浏览器缓存文件(需结合身份验证避免非法访问)
  • 使用签名URL的CDN:将私密文件存储在支持临时签名的CDN,后端生成签名URL给前端,前端直接从CDN获取,减轻服务器压力

三、当前方案是否合适?

当前逐个请求的方案仅适用于文件数量较少(10个以内)的场景,如果文件数量超过20个,请求风暴会显著增加服务器负载,同时前端会出现请求排队、加载缓慢的问题,建议优先采用批量接口+懒加载+缓存的组合方案。

内容的提问来源于stack exchange,提问作者askepott

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.24 21:48:28