React/Node.js项目实现谷歌云存储单文件及批量下载问题
问题分析与解决方案
前端代码修复(单条下载)
你的前端代码核心问题是获取到Blob后没有触发浏览器下载动作,仅停留在获取Blob对象的步骤;同时变量名重复覆盖也容易引发混淆。修复后的代码如下:
const downloadSingleFile = async () => { try { // 获取GCS文件的有效下载URL const downloadUrl = await getDownloadURL(ref(storage, cellInfo.fileName)); // 用fetch替代XHR,代码更简洁易维护 const response = await fetch(downloadUrl); const blob = await response.blob(); // 创建临时下载链接并触发浏览器下载 const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; // 从文件路径中提取文件名,避免下载后显示完整路径 a.download = cellInfo.fileName.split('/').pop() || '未知文件'; document.body.appendChild(a); a.click(); // 清理临时资源,避免内存泄漏 window.URL.revokeObjectURL(url); document.body.removeChild(a); } catch (error) { console.error('单文件下载失败:', error); } };
前端批量下载实现
如果需要批量下载选中文件,可使用jszip库打包后下载(需先执行npm install jszip安装依赖):
import JSZip from 'jszip'; const downloadBatchFiles = async (selectedRows) => { const zip = new JSZip(); try { // 并行下载所有选中文件并添加到zip包 await Promise.all(selectedRows.map(async (row) => { const downloadUrl = await getDownloadURL(ref(storage, row.fileName)); const response = await fetch(downloadUrl); const blob = await response.blob(); // 用文件名作为zip内的文件路径 zip.file(row.fileName.split('/').pop(), blob); })); // 生成zip包并触发下载 const zipBlob = await zip.generateAsync({ type: 'blob' }); const url = window.URL.createObjectURL(zipBlob); const a = document.createElement('a'); a.href = url; a.download = '批量文件.zip'; document.body.appendChild(a); a.click(); // 清理资源 window.URL.revokeObjectURL(url); document.body.removeChild(a); } catch (error) { console.error('批量下载失败:', error); } };
Node.js后端接口方案
单文件下载接口
使用@google-cloud/storage库直接从GCS获取文件流并返回给前端(需先执行npm install @google-cloud/storage安装依赖):
const { Storage } = require('@google-cloud/storage'); const storage = new Storage({ keyFilename: '你的GCS服务账号密钥.json' }); const YourModel = require('../models/你的数据模型'); // 替换为你的MongoDB模型 // 单文件下载接口 app.get('/api/:location/download/:id', async (req, res) => { try { // 从MongoDB查询对应文件路径 const data = await YourModel.findById(req.params.id); if (!data || !data.fileName) { return res.status(404).send('文件不存在'); } const bucket = storage.bucket('你的GCS存储桶名称'); const file = bucket.file(data.fileName); // 设置响应头,强制浏览器触发下载 res.setHeader('Content-Disposition', `attachment; filename="${data.fileName.split('/').pop()}"`); res.setHeader('Content-Type', 'application/octet-stream'); // 将GCS文件流管道到响应 file.createReadStream().pipe(res); } catch (error) { console.error('后端下载错误:', error); res.status(500).send('下载失败'); } });
后端批量下载接口
使用archiver库打包多个文件后返回(需先执行npm install archiver安装依赖):
const archiver = require('archiver'); // 批量下载接口 app.post('/api/:location/download/batch', async (req, res) => { try { const { ids } = req.body; // 前端传入选中数据的ID数组 const dataList = await YourModel.find({ _id: { $in: ids } }); if (dataList.length === 0) { return res.status(404).send('未找到对应文件'); } // 创建zip归档器 const archive = archiver('zip', { zlib: { level: 9 } }); // 设置响应头 res.setHeader('Content-Disposition', 'attachment; filename="批量文件.zip"'); res.setHeader('Content-Type', 'application/zip'); // 将归档器管道到响应 archive.pipe(res); // 遍历文件并添加到zip包 for (const data of dataList) { if (data.fileName) { const bucket = storage.bucket('你的GCS存储桶名称'); const file = bucket.file(data.fileName); archive.append(file.createReadStream(), { name: data.fileName.split('/').pop() }); } } // 完成归档 await archive.finalize(); } catch (error) { console.error('后端批量下载错误:', error); res.status(500).send('批量下载失败'); } });
前端调用后端批量接口代码
const downloadBatchViaBackend = async (selectedRows) => { try { const ids = selectedRows.map(row => row._id); const response = await axios.post(`http://localhost:3000/api/${location}/download/batch`, { ids }, { responseType: 'blob' // 关键:告知axios接收Blob类型响应 }); // 触发浏览器下载 const url = window.URL.createObjectURL(response.data); const a = document.createElement('a'); a.href = url; a.download = '批量文件.zip'; document.body.appendChild(a); a.click(); window.URL.revokeObjectURL(url); document.body.removeChild(a); } catch (error) { console.error('后端批量下载失败:', error); } };
关键注意事项
- CORS配置:确保GCS的CORS规则允许前端域名(包括
http://localhost:3000)的GET请求,示例规则:
[ { "origin": ["http://localhost:3000", "你的生产环境域名"], "method": ["GET"], "responseHeader": ["Content-Type"], "maxAgeSeconds": 3600 } ]
- 本地跨域处理:若前端与后端端口不同,需在Node.js后端配置
cors中间件,允许前端域名的请求。 - 文件名处理:始终从GCS路径中提取最后一段作为下载文件名,避免用户下载后看到完整路径名称。
内容的提问来源于stack exchange,提问作者Joeboulton
相关产品推荐
相关产品推荐

