React
2024年10月17日
一、认识
并发上传基本思想---文件分片、断点续传
使用 Blob.slice
方法对大文件按照指定的大小进行切割,然后通过多线程进行分块上传,等所有分块都成功上传后,再通知服务端进行分块合并。
File
对象是特殊类型的 Blob
,且可以用在任意的 Blob
类型的上下文中。比如说 FileReader
、URL.createObjectURL()
及 XMLHttpRequest.send()
都能处理 Blob
和 File
。在大文件上传的场景中,我们将使用 Blob.slice
方法对大文件按照指定的大小进行切割,然后对分块进行并行上传。
二、细节
2.1 获取大型文件 MD5 值
获取大型文件 MD5 值 我们使用 FileReader API
分块读取文件的内容,然后通过 spark-md5
这个库提供的方法来计算文件的 MD5
值 。因为文件的体积不确定,分块读取的方式对于大体积的文件计算更加稳定,还可以获得计算进度的信息。
import SparkMD5 from "spark-md5";
function App() {
const chunkSize = 2 * 1025 * 1024;
function calcFileMD5(file, chunkSize) {
return new Promise((resolve, reject) => {
const chunks = Math.ceil(file.size / chunkSize);
const spark = new SparkMD5.ArrayBuffer();
const fileReader = new FileReader();
let currentChunk = 0;
fileReader.onload = (e) => {
spark.append(e.target.result);
currentChunk++;
if (currentChunk < chunks) {
loadNext();
} else {
resolve(spark.end());
}
};
fileReader.onerror = (e) => {
reject(fileReader.error);
fileReader.abort();
};
function loadNext() {
const start = currentChunk * chunkSize;
const end =
start + chunkSize >= file.size ? file.size : start + chunkSize;
fileReader.readAsArrayBuffer(file.slice(start, end));
}
loadNext();
});
}
async function handleChange(e) {
const file = e.target.files[0];
const fileMD5 = await calcFileMD5(file,chunkSize);
console.log(fileMD5);
}
return (
<div className="App">
<input type="file" onChange={(e) => handleChange(e)} />
</div>
);
}
export default App;
2.2 定义并发控制函数
定义并发控制函数 用于实现异步任务的并发控制前往
2.3 定义 checkFileExist 函数
定义 checkFileExist 函数 checkFileExist
函数用于检测文件是否已经上传过了,如果已存在则秒传,否则返回已上传的分块 ID 列表:
2.4 定义 upload 函数
定义 upload 函数
- 文件分片: 通过
file.slice()
将文件分片 - 断点续传: 调用
checkFileExist
函数会返回已上传的分块 ID 列表,跳过这些已上传的部分,继续上传其他分片
2.5 定义 concatFiles 函数
定义 concatFiles 函数 当所有分块都上传完成之后,我们需要通知服务端执行分块合并操作
2.6 实现暂停/恢复上传功能
实现暂停/恢复上传功能 (暂未实现)
三、实现
import React, { useState } from "react";
import axios from "axios";
import SparkMD5 from "spark-md5";
function App() {
let file;
let fileMD5;
const poolLimit = 2;
const chunkSize = 2 * 1024 * 1024;
let chunkFinishList = [];
const [operation, setOperation] = useState("上传");
function calcFileMD5(file,chunkSize) {
return new Promise((resolve, reject) => {
const chunks = Math.ceil(file.size / chunkSize);
let currentChunk = 0;
const spark = new SparkMD5.ArrayBuffer();
const fileReader = new FileReader();
fileReader.onload = (e) => {
spark.append(e.target.result);
currentChunk++;
if (currentChunk < chunks) {
loadNext();
} else {
resolve(spark.end());
}
};
fileReader.onerror = (e) => {
reject(fileReader.error);
fileReader.abort();
};
function loadNext() {
const start = currentChunk * chunkSize;
const end =
start + chunkSize >= file.size ? file.size : start + chunkSize;
fileReader.readAsArrayBuffer(file.slice(start, end));
}
loadNext();
});
}
function checkFileExist() {
const { name: fileName } = file;
return axios
.get("http://localhost:4000/exists", {
params: {
fileName,
fileMD5,
},
})
.then((result) => result.data);
}
function uploadChunk({ chunk, chunkIndex }) {
const fileName = file.name;
const formData = new FormData();
formData.set("file", chunk, fileMD5 + "-" + chunkIndex);
formData.set("fileName", fileName);
formData.set("timestamp", Date.now());
return axios.post("http://localhost:4000/useChunk", formData);
}
async function asyncPool(poolLimit, array, iteratorFn) {
const taskList = [];
const currentTaskList = [];
for (const item of array) {
const promise = Promise.resolve().then(() => iteratorFn(item, array));
taskList.push(promise);
if (poolLimit <= array.length) {
const currentPromise = promise.then(() =>
currentTaskList.splice(currentTaskList.indexOf(currentPromise),1)
);
currentTaskList.push(currentPromise);
if (currentTaskList.length >= poolLimit) {
await Promise.race(currentTaskList);
}
}
}
return Promise.all(taskList);
}
function upload() {
const { size } = file;
const chunks = Math.ceil(size / chunkSize);
return asyncPool(poolLimit, [...new Array(chunks).keys()], (i) => {
if (chunkFinishList.indexOf(i + "") !== -1) {
return Promise.resolve();
}
const start = i * chunkSize;
const end = i + 1 === chunks ? size : (i + 1) * chunkSize;
const chunk = file.slice(start, end);
return uploadChunk({
chunk,
chunkIndex: i,
});
});
}
function concatChunks(fileName, fileMD5) {
return axios.get("http://localhost:4000/concatChunks", {
params: {
fileName,
fileMD5,
},
});
}
async function handleUpload() {
fileMD5 = await calcFileMD5(file,chunkSize);
const fileStatus = await checkFileExist();
if (fileStatus.data && fileStatus.data.isExists) {
alert("文件已经秒传");
return;
} else {
chunkFinishList = fileStatus.data.chunkFinishList;
await upload();
}
await concatChunks(file.name, fileMD5);
}
function handleChange(e) {
const {
target: { files },
} = e;
file = files[0];
}
function handleChangeOperation() {
if (operation === "上传") {
setOperation("暂停");
handleUpload();
} else {
setOperation("上传");
}
}
return (
<div className="App">
<input type="file" onChange={(e) => handleChange(e)} />
<button onClick={() => handleChangeOperation()}>{operation}</button>
</div>
);
}
export default App;