大文件上传下载
大文件上传下载怎么处理
# 大文件上传
# H5 文件分片下载,合并,另存为
b 站有视频专门讲解大文件上传下载以及断点续传
前端本地生成文件包
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js"></script> // 例如:打包图片并将其保存为ZIP文件 // 创建JSZip实例 const zip = new JSZip(); // 假设你有一个图片路径的数组 const imagePaths = ['image1.jpg', 'image2.jpg', 'image3.jpg']; // 为每个图片创建一个新的zip实例 const promises = imagePaths.map(imagePath => { // 从服务器获取图片的二进制数据 return fetch(imagePath) .then(response => response.blob()) .then(blob => { // 将图片添加到zip实例中 zip.file(imagePath.split('/').pop(), blob); }); }); // 等待所有图片都被添加到zip实例中 Promise.all(promises).then(() => { // 生成ZIP内容,并使用FileSaver保存 zip.generateAsync({ type: "blob" }).then(function(content) { saveAs(content, "images.zip"); }); });1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
本地文件创建链接的方法(文件操作)
了解 blob:https://developer.mozilla.org/zh-CN/docs/Web/API/Blob#%E5%88%9B%E5%BB%BA%E4%B8%80%E4%B8%AA%E6%8C%87%E5%90%91%E7%B1%BB%E5%9E%8B%E5%8C%96%E6%95%B0%E7%BB%84%E7%9A%84_url
前端根据后端返回实现下载或者预览的几种方式:https://github.com/18888628835/Blog/issues/52
// 字符串转为json并下载 saveAsJson = (str: string,fileName: string) => { const blob = new Blob([str], { type: "application/json;charset=utf-8" }); const href = URL.createObjectURL(blob); // 这里如果说是input元素获取的图片,那么得到的就是图片的本地url const alink = document.createElement("a"); alink.style.display = "none"; alink.download = fileName+".json"; // 下载后文件名 alink.href = href; document.body.appendChild(alink); alink.click(); document.body.removeChild(alink); // 下载完成移除元素 URL.revokeObjectURL(href); // 释放掉blob对象 };1
2
3
4
5
6
7
8
9
10
11
12
13
FileReader api:https://juejin.cn/post/7007366357146664997
- 参考:https://developer.mozilla.org/zh-CN/docs/Web/API/FileReader
- 可以把文件读取为多种数据格式
本地图片也可以变成 base64 格式
// 将本地图片转换为Base64编码 function getBase64FromLocalImage(file) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.readAsDataURL(file); reader.onload = () => resolve(reader.result); reader.onerror = error => reject(error); }); }1
2
3
4
5
6
7
8
9
直接读取文件
- https://www.bytenote.net/article/193575731094093825
- https://blog.csdn.net/qq937654393/article/details/115423759
- https://www.bytenote.net/article/193575731094093825
图像的裁剪或者视屏的截图可以使用 createImageBitmap()这个 api
- https://developer.mozilla.org/zh-CN/docs/Web/API/createImageBitmap
- 结合 blob 是使用可以生成本地链接
- https://developer.mozilla.org/zh-CN/docs/Web/API/createImageBitmap
下载大文件,保存到 indexDb,缓存使用
- https://mp.weixin.qq.com/s/BhwAm63t8yKyTLTVv_d66w
不用渲染图片就可以获取图片的原始宽高:
- imgElement.naturalWidth / imgElement.naturalHeight
- https://www.w3school.com.cn/jsref/prop_img_naturalwidth.asp
- https://mp.weixin.qq.com/s/IO9qqXBGas6v37Y3l3uwMQ
- imgElement.naturalWidth / imgElement.naturalHeight
大文件的下载
- https://juejin.cn/post/7346430066152407040
- 另外参考上面的:https://blog.csdn.net/conan729/article/details/105505046
- 并行下载
- https://www.cnblogs.com/zc-lee/p/15272212.html
- 结合 db 下载大文件代码如下(没有并行)
class LargeFileDownloader { async downloadLargeFile(url: string, filename: string) { const response = await fetch(url); const totalSize = parseInt(response.headers.get('Content-Length') || '0'); const chunkSize = 1024 * 1024; // 1MB chunk size const chunks = Math.ceil(totalSize / chunkSize); const db = await this.openDatabase(); const transaction = db.transaction(['chunks'], 'readwrite'); const store = transaction.objectStore('chunks'); for (let i = 0; i < chunks; i++) { const start = i * chunkSize; const end = Math.min(start + chunkSize, totalSize); const response = await fetch(url, { headers: { Range: `bytes=${start}-${end}` } }); const blob = await response.blob(); const chunk = { index: i, data: blob }; store.add(chunk); } transaction.oncomplete = async () => { await this.mergeChunks(db, chunks, filename); }; } async openDatabase() { return new Promise<IDBDatabase>((resolve, reject) => { const request = indexedDB.open('fileChunksDB', 1); request.onupgradeneeded = (event) => { const db = request.result; db.createObjectStore('chunks', { keyPath: 'index' }); }; request.onsuccess = () => { resolve(request.result); }; request.onerror = () => { reject(request.error); }; }); } async mergeChunks(db: IDBDatabase, totalChunks: number, filename: string) { const chunks: Blob[] = []; const transaction = db.transaction(['chunks'], 'readonly'); const store = transaction.objectStore('chunks'); for (let i = 0; i < totalChunks; i++) { const request = store.get(i); request.onsuccess = () => { chunks.push(request.result.data); if (chunks.length === totalChunks) { const blob = new Blob(chunks); const a = document.createElement('a'); const url = URL.createObjectURL(blob); a.href = url; a.download = filename; a.click(); URL.revokeObjectURL(url); db.close(); } }; } } } // 使用示例 const downloader = new LargeFileDownloader(); downloader.downloadLargeFile('https://example.com/largefile.zip', 'largefile.zip');1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
- https://juejin.cn/post/7346430066152407040
# 文件上传
文件上传用户操作方式:
- 点击上传
- 拖拽上传
文件上传方式:
- 原生 JavaScript 表单上传
- XMLHttpRequest(Ajax、axios):数据体是 formdata
- fetch:数据体是 formdata
- 第三方库:vue-simple-uploader 等
- 云存储服务
文件上传的步骤:
- 读取文件
- 获取文件信息,生成(md5 库:比如 spark-md5)唯一 hash
- 文件分片
- 根据 has 和文件名获取文件是否已上传(全部上传、部分上传:获取已上传的分片名列表)
- 上传未上传的分片(formdata 信息:分片名、分片数据、文件 hash)
文件上传优化策略:
断点续传(系统崩溃或者网络中断等异常因素导致上传中断,需要记录上传的进度,后续再次上传)
秒传
使用 web worker 优化主线程 I/O 流阻塞(文件操作、MD5 计算慢)的问题
hash 生成优化:比如使用抽样方式计算 MD5,将文件分成若干切片,每个切片选取固定字节数进行 MD5 计算,然后合并成最终的 hash,速度快,不过精度会降低
请求并发数量控制
实时进度条监听(比如 axios 的 onUploadProgress 方法)
上传失败重试(控制重试次数)
多文件上传
文件夹上传(input 的 webkitdirectory 属性),文件或文件夹二选一,支持多种形式,需要有读取系统文件的能力(比如 electron 构建的 app)
上传前验证(格式、大小)
使用到的技术:
Blob:https://developer.mozilla.org/zh-CN/docs/Web/API/Blob
Formdata:https://developer.mozilla.org/zh-CN/docs/Web/API/FormData
FileReader:https://developer.mozilla.org/zh-CN/docs/Web/API/FileReader
ArrayBuffer:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer
input-file:https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/input/file#%E9%9D%9E%E6%A0%87%E5%87%86%E5%B1%9E%E6%80%A7
Web_Workers:https://developer.mozilla.org/zh-CN/docs/Web/API/Web_Workers_API
# 文件下载
文件下载方式:
- window.open:新标签页,可下载大文件,打开一个无内容的窗口,完成下载后自动关闭,可监听该窗口是否关闭,判断是否下载完成
- location.href:新标签页
- a 标签+download 属性
- blob/base64(文件流):将数据存储到内存,然后资源转换后再进行下载。当资源很大时,浏览器分配到的内存是有限的,过多的占用内存会导致卡顿崩溃
- 分片下载:每个下载的文件块(chunk)在客户端进行缓存/存储(IndexedDB、storage 等),方便实现断点续传,以及后续将这些文件块合并成完整的文件,一般情况下,为了避免占用过多的内存,推荐将文件块暂时保存在客户端的本地存储中,确保在下载大文件时不会因为内存占用过多而导致性能问题
- stream api(ReadableStream),也可结合 service worker 一起使用,在较新的浏览器中支持
- 第三方库:FileSaver (opens new window)(支持 2gb)、StreamSaver (opens new window)(支持 2gb+)
使用到的技术:
- Content-Disposition:https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Content-Disposition
- ReadableStream:https://developer.mozilla.org/zh-CN/docs/Web/API/ReadableStream
- service worker:https://developer.mozilla.org/zh-CN/docs/Web/API/Service_Worker_API/Using_Service_Workers
- IndexedDB:https://developer.mozilla.org/zh-CN/docs/Web/API/IndexedDB_API,封装库有localForage、dexie.js、PouchDB、idb、idb-keyval、JsStore等
参考:
- https://juejin.cn/post/7202036660636287034
- https://github.com/jimmywarting/StreamSaver.js
# 文件下载
- https://www.cnblogs.com/caihongmin/p/17547302.html
# 自己实现的分片下载
// 下载
class LargeFileDownloader {
#beginTime = 0;
#libraryName = "downloadFile";
#tableName = "";
#filename = "";
#getFilenameFromPath(path) {
return path.substring(path.lastIndexOf("/") + 1);
}
async downloadLargeFile({ url, filename, libraryName, size = 5 }) {
this.#beginTime = new Date().getTime();
this.#filename = filename || this.#getFilenameFromPath(url);
this.#libraryName = libraryName || this.#libraryName;
this.#tableName = `${this.#getFilenameFromPath(url)}_${this.#beginTime}`;
const response = await fetch(url);
const totalSize = parseInt(response.headers.get("Content-Length") || "0");
const chunkSize = size * 1024 * 1024; // default 5MB chunk size
const chunks = Math.ceil(totalSize / chunkSize);
const db = await this.openDatabase();
// 这里让transaction事务和下面的add方法分开使用了,transaction会报错————The transaction has finished这个错
// const transaction = db.transaction(["chunks"], "readwrite");
// const store = transaction.objectStore("chunks");
// transaction.oncomplete = async () => {
// console.log("下载完成!");
// await this.mergeChunks(db, chunks, filename);
// };
// 下载所有分片并保存
await Promise.all(
Array.from(Array(chunks).keys()).map(async (i) => {
// 这里要特别注意,我们请求的字节范围和后端是怎么返回的,这决定了最终的文件是否买可用。例如 这里请求 0~50个字节,后端可以能返回0~49个字节也可能能返回0~50个字节,这就决定了下面start的参数不一样。
const start = i * chunkSize + i; // 这里就是返回的范围和参数一样。不会存在少一个的情况。
const end = Math.min(start + chunkSize, totalSize - 1);
const response = await fetch(url, {
// 这里的请求我们可以使用已有的工具函数去限制一次性请求的个数,优化性能。
headers: {
Range: `bytes=${start}-${end}`,
responseType: "blob",
method: "post",
},
});
const blob = await response.blob();
const chunk = { key: `${i}-${this.#filename}`, data: blob };
await new Promise((resolve, reject) => {
// 这里我们要注意,indexDB一旦检测到事务中没有活动请求,indexedDB事务将自动关闭。所以add方法需要和事务一起使用。不能分开始使用。
console.log({ db, name: this.#tableName });
// transaction;就是事务的意思
const transaction = db.transaction([`${this.#tableName}`], "readwrite");
const store = transaction.objectStore(this.#tableName);
const request = store.add(chunk);
request.onsuccess = () => {
resolve();
};
request.onerror = reject;
});
})
);
await this.mergeChunks(db, chunks, this.#filename);
}
// 创建或打开indexDB
async openDatabase() {
return new Promise((resolve, reject) => {
// 我们每次都要去更新库的版本号——new Date().getTime(),否则onupgradeneeded事件不触发,也就无法新创建表,否则(第二个参数不变的情况下)只能是打开某个表。当版本号存在时就是打开indexDB,否则就是创建新的indexDB。
const request = indexedDB.open(this.#libraryName, new Date().getTime());
request.onupgradeneeded = (event) => {
// 库更新的时候会触发,例如版本更新
console.log(`${this.#libraryName}_数据库发生更新`);
const db = request.result || event.target.result;
db.createObjectStore(this.#tableName, { keyPath: "key" });
};
request.onsuccess = async () => {
console.log("打开或创建库成功!");
resolve(request.result);
};
request.onerror = () => {
console.error({ 创建表发生错误: request.error });
reject(request.error);
};
});
}
// 合并所有分片并下载最终的文件
async mergeChunks(db, totalChunks, filename) {
const chunks = [];
const transaction = db.transaction([`${this.#tableName}`], "readonly");
const store = transaction.objectStore(this.#tableName);
// Wait for all chunks to be collected and merged
await Promise.all(
Array.from(Array(totalChunks).keys()).map((i) => {
return new Promise((resolve, reject) => {
const request = store.get(`${i}-${this.#filename}`);
request.onsuccess = () => {
console.log({ requestDDD: request.result.data });
console.log({ i });
chunks[i] = request.result.data;
resolve();
};
request.onerror = reject;
});
})
);
console.log({ chunks });
const blob = new Blob(chunks);
const a = document.createElement("a");
const url = URL.createObjectURL(blob);
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
db.close();
// 下载完成后删除数据库
db.deleteDatabase(this.#tableName)
.then(() => {
console.log(`${this.#tableName}数据库已删除`);
})
.catch(error => {
console.error(`${this.#tableName}删除数据库时发生错误:`, error);
});
}
}
// 使用示例
const downloader = new LargeFileDownloader();
function download() {
// 大文件地址:https:////video-creater-file.atvideo.cc/ufiles/aa8a1df69f241a0d05a480f7e9ad1bd2e8e8bfe4c980b0ec6b0c2ef638c7d89b.mp4
downloader.downloadLargeFile({
url: "https://video-creater-file.atvideo.cc/ufiles/c3f6480935515fe0d6d0252437cc78a602d42d9b2b765d646a533a66d0b0a1e1.mp4",
size: 2,
});
}
// 阿里云的range下载就极为相似:https://help.aliyun.com/zh/oss/developer-reference/range-download-3?spm=a2c4g.11186623.help-menu-31815.d_3_2_7_5_1.680a398bpBnmiL&scm=20140722.H_384023._.OR_help-T_cn~zh-V_1
indexDB的IndexedDB 浏览器存储限制和清理标准:https://developer.mozilla.org/zh-CN/docs/Web/API/Storage_API/Storage_quotas_and_eviction_criteria
indexDB的存储位置以及使用注意事项:https://www.imooc.com/article/287014
- IDB存储空间的大小,主要由IDB存储位置的磁盘大小确定,官网上说最多是剩余空间50%,一般是符合2^n定律。chrome存储文件位置为:C:\Users[用户名]\AppData\Local\Google\Chrome\User Data\Default\IndexedDB[http域名],之前官网看到和域名相关(组),经验证并没有。
- https://blog.csdn.net/weixin_42900858/article/details/115859805
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144