前端文件流、切片下载和上传

一、前端文件流操作

下面创建一个 fileUpload 的函数式组件,当用户选择文件时,通过FileReader将文件内容读取为 ArrayButter,然后将ArrayBuffer转换为十六进制字符串,并将结果显示在页面上。

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
import React, { useState } from "react";

function FileInput() {
const [fileContent, setFileContent] = useState("");

// 读取文件内容到ArrayBuffer
function readFileToArrayBuffer(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();

// 注册文件读取完成后的回调函数
reader.onload = function (event) {
const arrayBuffer = event.target.result;
resolve(arrayBuffer);
};

// 读取文件内容到ArrayBuffer
reader.readAsArrayBuffer(file);
});
}

// 将ArrayBuffer转为十六进制字符串
function arrayBufferToHexString(arrayBuffer) {
const uint8Array = new Uint8Array(arrayBuffer);
let hexString = "";
for (let i = 0; i < uint8Array.length; i++) {
const hex = uint8Array[i].toString(16).padStart(2, "0");
hexString += hex;
}
return hexString;
}

// 处理文件选择事件
function handleFileChange(event) {
const file = event.target.files[0]; // 获取选中的文件

if (file) {
readFileToArrayBuffer(file)
.then((arrayBuffer) => {
const hexString = arrayBufferToHexString(arrayBuffer);
setFileContent(hexString);
})
.catch((error) => {
console.error("文件读取失败:", error);
});
} else {
setFileContent("请选择一个文件");
}
}

return (
<div>
<input type="file" onChange={handleFileChange} />
<div>
<h4>文件内容:</h4>
<pre>{fileContent}</pre>
</div>
</div>
);
}

export default FileInput;

二、文件切片下载

文件切片下载的好处:

  • 快速启动:客户端可以快速开始下载,因为只需要下载第一个切片即可。
  • 并发下载:通过使用多个并发请求下载切片,可以充分利用带宽,并提高整体下载速度。
  • 断点续传:如果下载中断,客户端只需要重新下载中断的切片,而不需要重新下载整个文件。
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
const [selectedFile, setSelectedFile] = useState(null);
const [progress, setProgress] = useState(0);
// 处理文件选择事件
function handleFileChange(event) {
setSelectedFile(event.target.files[0]);
}

// 处理文件上传事件
function handleFileUpload() {
if (selectedFile) {
// 计算切片数量和每个切片的大小
const fileSize = selectedFile.size;
const chunkSize = 1024 * 1024; // 设置切片大小为1MB
const totalChunks = Math.ceil(fileSize / chunkSize);

// 创建FormData对象,并添加文件信息
const formData = new FormData();
formData.append("file", selectedFile);
formData.append("totalChunks", totalChunks);

// 循环上传切片
for (let chunkNumber = 0; chunkNumber < totalChunks; chunkNumber++) {
const start = chunkNumber * chunkSize;
const end = Math.min(start + chunkSize, fileSize);
const chunk = selectedFile.slice(start, end);
formData.append(`chunk-${chunkNumber}`, chunk, selectedFile.name);
}

// 发起文件上传请求
axios
.post("/upload", formData, {
onUploadProgress: (progressEvent) => {
const progress = Math.round(
(progressEvent.loaded / progressEvent.total) * 100
);
setProgress(progress);
},
})
.then((response) => {
console.log("文件上传成功:", response.data);
})
.catch((error) => {
console.error("文件上传失败:", error);
});
}
}

当用户选择文件后,通过 handleFileChange 函数处理文件选择事件,将选择的文件保存在 selectedFile 状态中。
当用户点击上传按钮时,通过 handleFileUpload 函数处理文件上传事件。
handleFileUpload 函数中,计算切片数量和每个切片的大小,并创建一个 FormData 对象用于存储文件信息和切片数据。

实现客户端切片下载的方案

实现客户端切片下载的基本方案如下:

  1. 服务器端将大文件切割成多个切片,并为每个切片生成唯一的标识符。
  2. 客户端发送请求获取切片列表,同时开始下载第一个切片。
  3. 客户端在下载过程中,根据切片列表发起并发请求下载其他切片,并逐渐拼接合并下载的数据。
  4. 当所有切片都下载完成后,客户端将下载的数据合并为完整的文件
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
function downloadFile() {
// 发起文件下载请求
fetch("/download", {
method: "GET",
headers: {
"Content-Type": "application/json",
},
})
.then((response) => response.json())
.then((data) => {
const totalSize = data.totalSize;
const totalChunks = data.totalChunks;

let downloadedChunks = 0;
let chunks = [];

// 下载每个切片
for (let chunkNumber = 0; chunkNumber < totalChunks; chunkNumber++) {
fetch(`/download/${chunkNumber}`, {
method: "GET",
})
.then((response) => response.blob())
.then((chunk) => {
downloadedChunks++;
chunks.push(chunk);

// 当所有切片都下载完成时
if (downloadedChunks === totalChunks) {
// 合并切片
const mergedBlob = new Blob(chunks);

// 创建对象 URL,生成下载链接
const downloadUrl = window.URL.createObjectURL(mergedBlob);

// 创建 <a> 元素并设置属性
const link = document.createElement("a");
link.href = downloadUrl;
link.setAttribute("download", "file.txt");

// 模拟点击下载
link.click();

// 释放资源
window.URL.revokeObjectURL(downloadUrl);
}
});
}
})
.catch((error) => {
console.error("文件下载失败:", error);
});
}

我们看下代码,首先使用 BLOB 对象创建一共对象 URL,用于生成下载连接,然后创建 a 标签并且设置 href 的属性为刚刚创建的对象 URL,继续设置 a 标签的 download 属性是文件名,方便点击的时候自动下载文件。

显示下载进度和完成状态

为了显示下载进度和完成状态,可以在客户端实现以下功能:

  1. 显示进度条:客户端可以通过监听每个切片的下载进度来计算整体下载进度,并实时更新进度条的显示。
  2. 显示完成状态:当所有切片都下载完成后,客户端可以显示下载完成的状态,例如显示一个完成的图标或者文本。
    这里我们可以继续接着切片上传代码示例里的继续写。
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
function handleFileDownload() {
axios.get('/download', {
responseType: 'blob',
onDownloadProgress: progressEvent => {
const progress = Math.round((progressEvent.loaded / progressEvent.total) * 100);
setProgress(progress);
}
})
.then(response => {
// 创建一个临时的URL对象用于下载
const url = window.URL.createObjectURL(new Blob([response.data]));
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', 'file.txt');
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
})
.catch(error => {
console.error('文件下载失败:', error);
});
}


<button onClick={handleFileDownload}>下载文件</button>
<div>进度:{progress}%</div>
  • 当用户点击下载按钮时,通过 handleFileDownload 函数处理文件下载事件。
  • handleFileDownload 函数中,使用 axios 库发起文件下载请求,并设置 responseType: 'blob' 表示返回二进制数据。
  • 通过监听 onDownloadProgress 属性获取下载进度,并更新进度条的显示。
  • 下载完成后,创建一个临时的 URL 对象用于下载,并通过动态创建 <a> 元素模拟点击下载。

实现断点续传的技术:记录和恢复上传状态

  • 在前端,可以使用 localStoragesessionStorage 来存储已上传的切片信息,包括已上传的切片索引、切片大小等。
  • 每次上传前,先检查本地存储中是否存在已上传的切片信息,若存在,则从断点处继续上传。
  • 在后端,可以使用一个临时文件夹或数据库来记录已接收到的切片信息,包括已上传的切片索引、切片大小等。
  • 在上传完成前,保存上传状态,以便在上传中断后能够恢复上传进度。

在实现大文件上传时要考虑服务器端的处理能力和存储空间,以及安全性问题。同时,为了保障断点续传的准确性,应该尽量避免并发上传相同文件的情况,可以采用文件唯一标识符或用户会话标识符进行区分。