移动端从Firebase Storage下载大文件失败问题求助
问题描述
基于Firebase开发的React应用,支持从Firebase Storage下载文件,遇到以下问题:
- 桌面端功能完全正常,移动端下载200MB以下文件无异常
- 下载400MB以上文件时必然失败(未找到精确临界值)
- 调试发现下载进度走到99%,执行
anchor.click()后,iPhone Safari弹出报错:「Safari无法打开页面,操作无法完成 (WebkitBlobResource error: 1)」,地址栏显示blob:https://myapp.com/xxx格式链接,最终无文件下载 - 同iOS版本下,3GB RAM的iPhone XR可正常下载,2GB RAM的iPhone SE(2)下载失败
- 尝试过Axios、fetch、
getBlob()等方式,获取Blob环节无问题,问题集中在anchor.click()触发时 - Stack Overflow上的旧解决方案均无效
附相关代码:
const downloadFile = async (storagePath: string): Promise<void> => { return new Promise(async (resolve, reject) => { try { const pathReference = ref( storage, `gs://bucket-url/${storagePath}` ); const url = await getDownloadURL(pathReference); const xhr = new XMLHttpRequest(); xhr.open("GET", url, true); xhr.responseType = "blob"; xhr.addEventListener("progress", (event) => { if (event.lengthComputable) { const percentComplete = (event.loaded / event.total) * 100; setDownloadProgress(Math.floor(percentComplete)); } }); xhr.onload = () => { console.log("loaded!"); const blob = xhr.response; console.log("creating blob object!"); const blobUrl = URL.createObjectURL(blob); console.log("Blob object created!!"); const anchor = document.createElement("a"); anchor.href = blobUrl; anchor.download = episodePath.split("/")[1]; anchor.style.display = "none"; document.body.appendChild(anchor); anchor.addEventListener("download", () => { if (timeoutId.current) { clearTimeout(timeoutId.current); timeoutId.current = null; } if (blobUrl) { URL.revokeObjectURL(blobUrl); } }); console.log("I'm gonna click!"); anchor.click(); // This is a fallback for when the download event doesn't fire timeoutId.current = setTimeout(() => { if (blobUrl) { console.log( `Revoking blob URL in timeout: ${blobUrl}` ); URL.revokeObjectURL(blobUrl); } }, 1000); document.body.removeChild(anchor); currentDownloadTask.current = null; setDownloadProgress(0); setToastMessage("Download completed successfully!"); setToastStatus("success"); setToastVariant("success"); setShowToast(true); setDownloading([]); resolve(); }; xhr.onerror = (error: any) => { console.error("Download failed:", error); setToastMessage("Download Failed."); setToastStatus("error"); setToastVariant("danger"); setShowToast(true); setDownloading([]); currentDownloadTask.current = null; setDownloadProgress(0); reject(error); }; xhr.onabort = () => { console.log("Download aborted."); setToastMessage("Download cancelled."); setToastStatus("aborted"); setToastVariant("danger"); setShowToast(true); setDownloading([]); currentDownloadTask.current = null; setDownloadProgress(0); reject(new Error("Download Aborted")); }; currentDownloadTask.current = xhr; xhr.send(); } catch (error: any) { console.error("Error during download:", error); setToastMessage("Download Failed."); setToastStatus("error"); setToastVariant("danger"); setShowToast(true); setDownloading([]); currentDownloadTask.current = null; setDownloadProgress(0); reject(error); } }); };
解决建议
1. 跳过Blob直接使用Firebase下载链接
Safari对大Blob的内存处理存在硬性限制,直接用Firebase生成的下载URL作为<a>标签的href,避免将整个文件加载到内存:
const downloadFile = async (storagePath: string): Promise<void> => { try { const pathReference = ref(storage, `gs://bucket-url/${storagePath}`); const url = await getDownloadURL(pathReference); const anchor = document.createElement("a"); anchor.href = url; anchor.download = storagePath.split("/")[1]; // 替换原episodePath为正确文件名变量 anchor.style.display = "none"; document.body.appendChild(anchor); anchor.click(); document.body.removeChild(anchor); setToastMessage("Download started successfully!"); // 注意:此方式无法监听下载进度,若需进度需额外实现分块下载逻辑 } catch (error) { console.error("Download failed:", error); setToastMessage("Download Failed."); setToastStatus("error"); setToastVariant("danger"); setShowToast(true); setDownloading([]); currentDownloadTask.current = null; setDownloadProgress(0); } };
说明:让浏览器直接处理下载请求,绕开Blob中转,彻底解决内存占用问题,但无法直接获取下载进度。如果必须保留进度监听,可结合Firebase的getBytes()分块下载并自行统计进度。
2. 调整Blob URL回收时机
当前代码在anchor.click()后1秒就回收Blob URL,Safari可能还未完成文件读取就失去资源,导致报错。修改回收逻辑:
- 移除1秒超时回收,仅在
download事件触发时回收 - 或延长超时时间到5-10秒,给Safari足够的处理窗口
修改后的xhr.onload核心片段:
xhr.onload = () => { // ... 其他代码 anchor.addEventListener("download", () => { if (timeoutId.current) { clearTimeout(timeoutId.current); timeoutId.current = null; } // 延迟3秒回收,给Safari足够处理时间 setTimeout(() => { URL.revokeObjectURL(blobUrl); }, 3000); }); anchor.click(); // 兜底超时延长至10秒 timeoutId.current = setTimeout(() => { URL.revokeObjectURL(blobUrl); }, 10000); };
3. 分块处理大文件(保留进度监听场景)
如果必须保留进度监听,可通过分块下载控制内存占用:
- 使用Firebase Storage的
getBytes()方法,每次下载固定大小的分块(比如10MB) - 逐步将分块数据合并为Blob,或直接通过File System Access API写入本地文件(仅支持现代浏览器)
- 此方式能精准控制内存使用,但实现复杂度较高,且File System Access API在iOS Safari中支持有限
4. 针对低内存设备做适配
iOS Safari在低内存设备上的文件处理限制更严格,可:
- 在下载前通过
navigator.deviceMemory检测设备内存,对2GB及以下内存的设备提示文件过大 - 提供文件压缩版本或分卷下载选项,降低单文件大小
5. 尝试使用navigator.download()(实验性)
部分现代浏览器支持navigator.download()API,可直接传入Blob或URL触发下载,可能规避<a>标签的Blob问题:
// 先做兼容性判断 if ('download' in navigator) { await navigator.download(blob, fileName); } else { // 回退到原<a>标签逻辑 }
内容的提问来源于stack exchange,提问作者Kim
相关产品推荐
相关产品推荐

