浏览器实际大文件下载解决方案
背景
现在大多数业务都是和内容生产相关的业务,我们公司实际业务是频繁和素材打交道,尤其是电商短视频(抖音,快手,tiktok)打交道,文件的上传和下载尤为重要,目前国内好像还没发现靠存储收费的大型服务商,文件的频繁下载这是一个常见的场景,比如我们一开始遇到一个实际业务问题是,用户上传的交付物都是素材(图片,视频),都是一批一批的,我们想着是打包下载,用户下载后也方便找,更有规则,但是现在正常的存储的服务商基本上不提供这样的服务,开会的时候讨论方案,方案其实挺多的,问AI或者cursor 也是实现非常容易,但是没有考虑实际业务场景, 如果下载是一个高频操作,且文件非常大,如何处理
方式一:浏览器插件下载(推荐)
export const batchDownload = (detail: DownloadRequest) => {
detail?.list?.forEach((item) => {
let filename = '';
if (detail?.dir) {
filename = `${detail.dir}/${item.filename}`;
} else {
filename = item.filename;
}
chrome.downloads.download({
url: item.url,
filename,
saveAs: detail?.saveAs || false,
// 防止重名覆盖
conflictAction: 'uniquify'
});
});
};
优点
- 流水线下载,可以下载的时候在前面加一个目录名字,会将文件下载到目录下面(别想多了,有些人这里就想偏了,不能是任意目录,是浏览器默认设置的下载目录下你可以创建的目录)
- 下载进度用户可见,不吃内存,随时可以取消
缺点
- 需要用户安装插件
- 插件需要支持的多每家的标准都不一样(Chrome, firefox, edge, 360极速浏览器,360安全浏览器)
- 开发框架也是比较多,难选(我们用的是Wxt)
- 上架(chrome插件国内需要自己安装,firefox可以自动化走内部托管,一键化发布,自动更新)
提示,记得开发插件的时候记得样式问题多检查一下,最好使用沙箱机制。
方式二:使用跨平台的方案,比如套壳electron
优点
- 自定义下载,完全突破浏览器限制
- 自定义程度高
缺点
- 不容易管理内存,内存如果不能合理分配,会直接内存爆炸(实际项目我试过,用户电脑死机了)
- 浏览器开发和客户端开发不是一个标准,客户端其实更多是基于内存和业务的合理分配和调度
方式三:FileSystem Access API + Streams API + ZipWriterStream (Web Streams 压缩库)
import { ZipWriterStream } from **********/zip.js';
export const startZipStreamDownload = async (
fileHandle: FileSystemFileHandle,
detail: DownloadRequest
): Promise<DownloadResult> => {
const result: DownloadResult = {
success: true,
successFiles: [],
failedFiles: []
};
let writable: FileSystemWritableFileStream | null = null;
let zipStream: ZipWriterStream | null = null;
let pipePromise: Promise<void> | null = null;
try {
writable = await fileHandle.createWritable();
zipStream = new ZipWriterStream({ zip64: true });
pipePromise = zipStream.readable.pipeTo(writable);
for (let i = 0; i < detail.list.length; i++) {
const item = detail.list[i];
const filename = item.filename;
try {
const response = await fetch(item.url);
if (!response.ok) {
console.warn(`跳过文件:HTTP ${response.status} - ${item.url}`);
result.failedFiles.push({
url: item.url,
filename,
result: 'failed',
error: `HTTP ${response.status}`
});
continue;
}
if (!response.body) {
throw new Error('Response body is null');
}
await response.body.pipeTo(zipStream.writable(filename));
result.successFiles.push({
url: item.url,
filename,
result: 'completed',
error: ''
});
console.log(`已完成: ${filename}`);
} catch (innerError) {
console.error(`下载中断或出错 [${filename}]:`, innerError);
result.failedFiles.push({
url: item.url,
filename,
result: 'failed',
error:
innerError instanceof Error
? innerError.message
: String(innerError)
});
// 继续下一个文件
}
}
// 正常关闭 ZIP 流
await zipStream.close();
await pipePromise;
} catch (error) {
console.error('ZIP 流写入失败:', error);
result.success = false;
// 安全清理
try {
if (writable) {
await writable.abort();
}
} catch {
// 忽略 abort 错误
}
}
// 设置最终状态
if (result.failedFiles.length > 0 && result.successFiles.length === 0) {
result.success = false;
}
return result;
};
优点
- 下载10GB,浏览器也可能只消耗几十MB的内存,流水线处理数据,不需一次性将整个大文件加载到内存中,极大地节省了内存开销。
- 不需要浏览器插件就可以实现
缺点
- 兼容性差,Chrome/Edge 等 Chromium 系浏览器支持较好,Safari 和 Firefox 的支持程度有限。
- 需要用户点击授权
- 页面标签不可关闭(一旦关闭,文件下载失败,服务器还要计算流量费用)
- 下载进度不可见
方式四:客户端使用blob流将所有的字节流都堆到内存中,全都接收完毕后进行下载
代码就懒得写了,随便搜搜都是这种方式
优点
- 打包压缩下载可以实现
缺点
- 文件大了,内存爆炸,浏览器分配的内存是4G,但实际安全内存只有2G,超过2G的内存直接被V8干掉了
- 浏览器标签关闭也是下载失败,服务器还是会计算出网费用
我们就是使用方式1,方式2也有,如果需要插件的朋友,可以到时候评论区说,有需求我可以发布一下,都是可以通过api调用下载的。或者有人想二次开发的话我也可以直接开源发布一下。看需求吧
如果有想获取抖音实际视频地址 或者 下载保存抖音无水印视频 需求的朋友可以使用下面的工具直接获取视频地址,这个原本是公司内网用的Easydown
