炫酷的移动端网站设计,wordpress 密码生成,wordpress主动推送所有网址插件,互联网营销师培训课程大文件分片上传是前端一种常见的技术#xff0c;用于提高大文件上传的效率和可靠性。主要原理和步骤如下 
文件分片 确定分片大小#xff1a;确定合适的分片大小。通常分片大小在 1MB 到 5MB 之间使用 Blob.slice 方法#xff1a;将文件分割成多个分片。每个分片可以使用 Bl…大文件分片上传是前端一种常见的技术用于提高大文件上传的效率和可靠性。主要原理和步骤如下 
文件分片 确定分片大小确定合适的分片大小。通常分片大小在 1MB 到 5MB 之间使用 Blob.slice 方法将文件分割成多个分片。每个分片可以使用 Blob.slice 方法从文件对象中切出 文件哈希 计算哈希值 使用 Web Workers 来计算每个分片的哈希值以避免阻塞主线程。(可以根据业务方向进行选择)使用 spark-md5 库来计算 MD5 哈希值 并发上传 使用 Promise.all 或 async/await 来同时上传多个分片或者使用plimit进行并发管理断点续传 记录已上传的分片使用本地存储如 localStorage 或 IndexedDB记录已上传的分片信息根据业务情况而定在上传前向服务器查询已上传的分片只上传未完成的分片 重试机制对于上传失败的分片可以设置重试次数并在重试失败后提示用户 根据业务情况而定进度监控 监听上传进度 使用 XMLHttpRequest 的 upload.onprogress 事件或 Fetch API 的 ReadableStream 来监听上传进度或者通过后端返回已上传内容进行计算计算每个分片的上传进度并累加到总进度中 错误处理 在上传过程中捕获网络错误、服务器错误等并进行相应的处理 
大文件上传源码及其解析 
示例代码和上面原理步骤实现可能有点不同(根据业务情况进行修改)但整体流程一致 
HTML布局 
div classkh-idxdiv classkh-idx-banner{{ msg }}/divform iduploadForm classkh-idx-forminputreffileInputtypefilenamefileacceptapplication/pdfbuttontypebuttonclickuploadFileUpload File/button/formprogress v-ifprocessVal :valueprocessVal max100/progress/divCSS 
.kh-idx {-banner {background-color: brown;color: aliceblue;text-align: center;}-form {margin-top: 20px;}
}TS 逻辑 
import { defineComponent } from vue;
import sparkMD5 from spark-md5;
import pLimit from p-limit;
import { postUploadFile, postUploadFileCheck } from client/api/index;
import axios, { CancelTokenSource } from axios;/*** 前端大文件上传技术点* 1.文件切片Chunking将大文件分割成多个小片段切片这样可以减少单次上传的数据量降低上传失败的概率并支持断点续传。* 2.文件hash助验证文件的完整性和唯一性* 3.并发上传利用JavaScript的异步特性可以同时上传多个文件切片提高上传效率。* 4.断点续传在上传过程中如果发生中断下次再上传可以从中断点继续上传而不是重新上传整个文件。这通常通过记录已上传的切片索引来实现。* 5.进度监控通过监听上传事件可以实时获取上传进度并显示给用户。* 6.错误处理在上传过程中要及时处理可能出现的错误如网络错误、服务器错误等*/
export default defineComponent({name: KhIndex,data() {return {msg: 文件上传demo,chunkSize: 5 * 1024 * 1024, // 设置分片大小 5 MBprocessVal: 0};},methods: {// 分割文件splitFileByChunkSize(file: File, chunkSize: number) {let splitedFileArr  [];let fileSize  file.size; // 获取文件大小let totalChunkNumber  Math.ceil(fileSize / chunkSize); // 向上取整获取 chunk 数量for (let i  0; i  totalChunkNumber; i) {// File类型继承BlobsplitedFileArr.push(file.slice(i * chunkSize, (i  1) * chunkSize));}return {originFile: file,name: file.name,splitedFile: splitedFileArr}},// 计算分割后的文件 hash 值calcuateFileHash(splitedFiles: ArrayBlob, chunkSize: number): Promisestring {let spark  new sparkMD5.ArrayBuffer();let chunks: Blob[]  [];splitedFiles.forEach((chunk, idx)  {if (idx  0 ||idx  splitedFiles.length - 1) {chunks.push(chunk);} else {// 中间剩余切片分别在前面、后面和中间取2个字节参与计算chunks.push(chunk.slice(0, 2)); // 前面的2字节chunks.push(chunk.slice(chunkSize / 2, (chunkSize / 2)  2)); // 中间的2字节chunks.push(chunk.slice(chunkSize - 2, chunkSize)); // 后面的2字节}});return new Promise((resolve, reject)  {let reader  new FileReader(); //异步 APIreader.readAsArrayBuffer(new Blob(chunks));reader.onload  (e: Event)  {spark.append((e.target as any).result as ArrayBuffer);resolve(spark.end());};reader.onerror  ()  {reject();};})},// 生成 formDatagenFormDataByChunkInfo(chunkList: Array{fileName: string,fileHash: string,index: number,chunk: Blob,chunkHash:  string,size: number,chunkTotal: number}) {return chunkList.map(({fileName,fileHash,index,chunk,chunkHash,chunkTotal,size})  {let formData  new FormData();formData.append(chunk, chunk);formData.append(chunkHash, chunkHash);formData.append(size,  String(size));formData.append(chunkTotal, String(chunkTotal));formData.append(fileName, fileName);formData.append(fileHash, fileHash);formData.append(index, String(index));return formData;});},// 取消请求createReqControl() {let cancelToken  axios.CancelToken;let cancelReq: CancelTokenSource[]  [];return {addCancelReq(req: CancelTokenSource) {cancelReq.push(req);},cancelAllReq(msg  已取消请求) {cancelReq.forEach((req)  {req.cancel(msg); // 全部取消后续请求})},createSource() {return cancelToken.source();},print() {console.log(cancelReq);}}},// 上传文件前的检查async uploadFileCheck(splitedFileObj: {originFile: File,name: string,splitedFile: ArrayBlob},fileMd5: string): Promise{isError: booleanisFileExist: boolean,uploadedChunks: []} {try {let { data }  await postUploadFileCheck({fileHash: fileMd5,chunkTotal: splitedFileObj.splitedFile.length,fileName: splitedFileObj.name});if (data.code  200 !(data.result?.isFileExist)) {return {isError: false,isFileExist: data.result?.isFileExist,uploadedChunks: data.result.uploadedChunks};}return {isError: true,isFileExist: false,uploadedChunks: []};} catch (error) {return {isError: true,isFileExist: false,uploadedChunks: []}}},// 并发请求async uploadFilesConcurrently(splitedFileObj: {originFile: File,name: string,splitedFile: ArrayBlob},fileMd5: string,concurrentNum  3,uploadedChunks: Arraynumber) {let cancelControlReq  this.createReqControl();const LIMIT_FUN  pLimit(concurrentNum); // 初始化并发限制let fileName  splitedFileObj.name; // 文件名let chunkTotalNum  splitedFileObj.splitedFile.length;let chunkList  splitedFileObj.splitedFile.map((chunk, idx)  {if (uploadedChunks.includes(idx)) return null;return {fileName, fileHash: fileMd5,index: idx,chunk,chunkTotal: chunkTotalNum,chunkHash: ${ fileMd5 }-${ idx },size: chunk.size}}).filter((fileInfo)  fileInfo ! null); // 过滤掉已经上传的chunklet formDataArr  this.genFormDataByChunkInfo(chunkList);let allPromises  formDataArr.map((formData)  {let source  cancelControlReq.createSource(); // 生成sourcecancelControlReq.addCancelReq(source); //添加 sourcereturn LIMIT_FUN(()  new Promise(async (resolve, reject)  {try {let result  await postUploadFile(formData, source.token);if (result.data.code  100) {cancelControlReq.cancelAllReq(); // 取消后续全部请求}if (result.data.code  201|| result.data.code  200) {let data  result.data.result;this.setPropress(Number(data.uploadedChunks.length), Number(data.chunkTotal));}resolve(result);} catch (error) {this.setPropress(0, 0); // 关闭进度条// 报错后取消后续请求cancelControlReq.cancelAllReq(); // 取消后续全部请求reject(error);}}));})return await Promise.all(allPromises);},// 设置进度条setPropress(uploadedChunks: number, chunkTotal: number) {this.processVal  (uploadedChunks / chunkTotal) * 100;},// 文件上传async uploadFile() {// 获取文件输入元素中的文件列表let files  (this.$refs.fileInput as HTMLInputElement).files || [];if (files.length  0) return;// 将选择的文件按照指定的分片大小进行分片处理	let fileSplitedObj  this.splitFileByChunkSize(files[0], this.chunkSize);// 计算整个文件的哈希值用于后续的文件校验和秒传功能let fileMd5  await this.calcuateFileHash(fileSplitedObj.splitedFile, this.chunkSize);// 检查服务器上是否已存在该文件的分片以及整个文件let uploadedChunksObj  await this.uploadFileCheck(fileSplitedObj, fileMd5);// 如果检查过程中发生错误或者文件已存在则直接返回	if (!(!uploadedChunksObj.isError !uploadedChunksObj.isFileExist)) return;// 并发上传文件分片最多同时上传3个分片let uploadFileResultArr  await this.uploadFilesConcurrently(fileSplitedObj,fileMd5,3,uploadedChunksObj.uploadedChunks);// 上传成功后重置进度条if (uploadFileResultArr Array.isArray(uploadFileResultArr)) {this.setPropress(0, 0);}}}
}); 
uploadFile函数逻辑分析 
检查是否选择了要上传的文件 let files  (this.$refs.fileInput as HTMLInputElement).files || [];if (files.length  0) return; // 没有选择文件后续就不走文件分片 let fileSplitedObj  this.splitFileByChunkSize(files[0], this.chunkSize);// 分割文件
splitFileByChunkSize(file: File, chunkSize: number) {let splitedFileArr  [];let fileSize  file.size; // 获取文件大小let totalChunkNumber  Math.ceil(fileSize / chunkSize); // 向上取整获取 chunk 数量for (let i  0; i  totalChunkNumber; i) {// File类型继承BlobsplitedFileArr.push(file.slice(i * chunkSize, (i  1) * chunkSize));}return {originFile: file,name: file.name,splitedFile: splitedFileArr}
},splitFileByChunkSize 函数功能分析 
初始化变量 splitedFileArr用于存储分割后的文件分片数组。fileSize获取文件的总大小。totalChunkNumber计算文件需要被分割成的分片数量。通过文件大小除以每个分片的大小然后向上取整得到。 文件分片 使用 for 循环遍历每个分片。在循环中使用 file.slice 方法从文件中切出每个分片。file.slice 方法接受两个参数起始位置和结束位置分别对应当前分片的开始和结束字节。将每个分片添加到 splitedFileArr 数组中。 返回结果返回一个对象包含原始文件、文件名和分割后的文件分片数组。 
生成文件MD5 
let fileMd5  await this.calcuateFileHash(fileSplitedObj.splitedFile, this.chunkSize);// 计算分割后的文件 hash 值
calcuateFileHash(splitedFiles: ArrayBlob, chunkSize: number): Promisestring {let spark  new sparkMD5.ArrayBuffer();let chunks: Blob[]  [];splitedFiles.forEach((chunk, idx)  {if (idx  0 ||idx  splitedFiles.length - 1) {chunks.push(chunk);} else {// 中间剩余切片分别在前面、后面和中间取2个字节参与计算chunks.push(chunk.slice(0, 2)); // 前面的2字节chunks.push(chunk.slice(chunkSize / 2, (chunkSize / 2)  2)); // 中间的2字节chunks.push(chunk.slice(chunkSize - 2, chunkSize)); // 后面的2字节}});return new Promise((resolve, reject)  {let reader  new FileReader(); //异步 APIreader.readAsArrayBuffer(new Blob(chunks));reader.onload  (e: Event)  {spark.append((e.target as any).result as ArrayBuffer);resolve(spark.end());};reader.onerror  ()  {reject();};})},calcuateFileHash 函数功能分析 
初始化变量 spark创建一个 sparkMD5.ArrayBuffer 实例用于计算文件的 MD5 哈希值。chunks初始化一个数组用于存储参与哈希计算的文件片段。 选择文件片段 遍历 splitedFiles 数组该数组包含了文件的所有分片。对于第一个和最后一个分片直接将它们添加到 chunks 数组中。对于中间的分片只选择每个分片的前 2 个字节、中间的 2 个字节和最后的 2 个字节参与哈希计算。这样可以减少计算量同时保持一定的哈希准确性。 读取文件片段 创建一个 FileReader 实例用于读取文件片段。使用 FileReader.readAsArrayBuffer 方法将 chunks 数组中的文件片段读取为 ArrayBuffer 格式。 计算哈希值 在 FileReader 的 onload 事件中将读取到的 ArrayBuffer 数据添加到 spark 实例中。调用 spark.end() 方法计算最终的 MD5 哈希值并通过 resolve 回调函数返回该哈希值。 错误处理 在 FileReader 的 onerror 事件中如果读取文件片段发生错误则通过 reject 回调函数返回一个空字符串表示哈希计算失败。 
检查是否已存在该文件的分片以及整个文件 let uploadedChunksObj  await this.uploadFileCheck(fileSplitedObj, fileMd5);// 如果检查过程中发生错误或者文件已存在则直接返回	if (!(!uploadedChunksObj.isError !uploadedChunksObj.isFileExist)) return;// 上传文件前的检查async uploadFileCheck(splitedFileObj: {originFile: File,name: string,splitedFile: ArrayBlob},fileMd5: string): Promise{isError: booleanisFileExist: boolean,uploadedChunks: []} {try {let { data }  await postUploadFileCheck({fileHash: fileMd5,chunkTotal: splitedFileObj.splitedFile.length,fileName: splitedFileObj.name});if (data.code  200 !(data.result?.isFileExist)) {return {isError: false,isFileExist: data.result?.isFileExist,uploadedChunks: data.result.uploadedChunks};}return {isError: true,isFileExist: false,uploadedChunks: []};} catch (error) {return {isError: true,isFileExist: false,uploadedChunks: []}}},uploadFileCheck 函数功能分析 
参数接收 splitedFileObj包含原始文件信息和分割后的文件分片数组的对象。 originFile原始文件对象。name文件名。splitedFile分割后的文件分片数组。 fileMd5文件的哈希值。 发送请求 使用 postUploadFileCheck 函数假设这是一个封装好的 HTTP POST 请求函数向服务器发送文件上传前的检查请求。 请求体中包含文件的哈希值 fileHash、分片总数 chunkTotal 和文件名 fileName。处理响应 如果服务器返回的状态码为 200 且文件不存在data.result?.isFileExist 为 false则返回一个对象表示没有错误发生文件不存在以及已上传的分片列表 uploadedChunks。如果服务器返回的状态码不是 200 或文件已存在则返回一个对象表示发生了错误文件不存在已上传的分片列表为空。 错误处理 如果在发送请求或处理响应过程中发生错误例如网络错误或服务器错误则捕获错误并返回一个对象表示发生了错误文件不存在已上传的分片列表为空。 
并发上传 
// 上传文件
let uploadFileResultArr  await this.uploadFilesConcurrently(fileSplitedObj,fileMd5,3,uploadedChunksObj.uploadedChunks
);// 并发请求
async uploadFilesConcurrently(splitedFileObj: {originFile: File,name: string,splitedFile: ArrayBlob},fileMd5: string,concurrentNum  3,uploadedChunks: Arraynumber
) {let cancelControlReq  this.createReqControl();const LIMIT_FUN  pLimit(concurrentNum); // 初始化并发限制let fileName  splitedFileObj.name; // 文件名let chunkTotalNum  splitedFileObj.splitedFile.length;let chunkList  splitedFileObj.splitedFile.map((chunk, idx)  {if (uploadedChunks.includes(idx)) return null;return {fileName, fileHash: fileMd5,index: idx,chunk,chunkTotal: chunkTotalNum,chunkHash: ${ fileMd5 }-${ idx },size: chunk.size}}).filter((fileInfo)  fileInfo ! null); // 过滤掉已经上传的chunklet formDataArr  this.genFormDataByChunkInfo(chunkList);let allPromises  formDataArr.map((formData)  {let source  cancelControlReq.createSource(); // 生成sourcecancelControlReq.addCancelReq(source); //添加 sourcereturn LIMIT_FUN(()  new Promise(async (resolve, reject)  {try {let result  await postUploadFile(formData, source.token);if (result.data.code  100) {cancelControlReq.cancelAllReq(); // 取消后续全部请求}if (result.data.code  201|| result.data.code  200) {let data  result.data.result;this.setPropress(Number(data.uploadedChunks.length), Number(data.chunkTotal));}resolve(result);} catch (error) {this.setPropress(0, 0); // 关闭进度条// 报错后取消后续请求cancelControlReq.cancelAllReq(); // 取消后续全部请求reject(error);}}));})return await Promise.all(allPromises);
},uploadFilesConcurrently 函数功能分析 
初始化并发控制 cancelControlReq创建一个请求控制对象用于管理上传请求的取消操作。LIMIT_FUN使用 pLimit 函数初始化并发限制concurrentNum 指定了同时上传的最大分片数量默认为 3。 准备上传数据 fileName获取文件名。chunkTotalNum获取分片总数。chunkList将分片数组映射为包含上传所需信息的对象数组。每个对象包含文件名、文件哈希值、分片索引、分片数据、分片总数、分片哈希值和分片大小。过滤掉已上传的分片。 生成表单数据 formDataArr调用 genFormDataByChunkInfo 方法根据分片信息生成对应的 FormData 对象数组。 创建并发上传任务 使用 map 方法遍历 formDataArr为每个分片创建一个上传任务。source为每个上传任务生成一个取消令牌 source并将其添加到请求控制对象中。LIMIT_FUN使用 pLimit 函数限制并发上传的数量。在每个上传任务中使用 postUploadFile 函数发送上传请求并传递 FormData 和取消令牌。如果上传成功更新上传进度。如果上传失败取消后续所有上传请求并返回错误。 等待所有上传任务完成 使用 Promise.all 等待所有上传任务完成返回一个包含所有上传结果的数组。 
nodeJs 逻辑 
index入口文件 
const EXPRESS  require(express);
const PATH  require(path);
const HISTORY  require(connect-history-api-fallback);
const COMPRESSION  require(compression);
const REQUEST  require(./routes/request);
const ENV  require(./config/env);
const APP  EXPRESS();
const PORT  3000;APP.use(COMPRESSION());// 开启gzip压缩// 设置静态资源缓存
const SERVE  (path, maxAge)  EXPRESS.static(path, { maxAge });APP.use(EXPRESS.json());
APP.all(*, (req, res, next)  {res.header(Access-Control-Allow-Origin,*);res.header(Access-Control-Allow-Headers,Content-Type);res.header(Access-Control-Allow-Methods,*);next() 
});
APP.use(REQUEST);APP.use(HISTORY());// 重置单页面路由APP.use(/dist, SERVE(PATH.resolve(__dirname, ../dist), ENV.maxAge));//根据环境变量使用不同环境配置
APP.use(require(ENV.router));APP.listen(PORT, ()  {console.log(APP listening at http://localhost:${PORT}\n);
}); 
request处理请求 
const express  require(express);
const requestRouter  express.Router();
const { resolve, join }   require(path);
const multer  require(multer);
const UPLOAD_DIR  resolve(__dirname, ../upload);
const UPLOAD_FILE_DIR  join(UPLOAD_DIR, files);
const UPLOAD_MULTER_TEMP_DIR  join(UPLOAD_DIR, multerTemp);
const upload  multer({ dest: UPLOAD_MULTER_TEMP_DIR });
const fse  require(fs/promises);
const fs  require(fs);
require(events).EventEmitter.defaultMaxListeners  20; // 将默认限制增加到// 合并chunks
function mergeChunks(fileName,tempChunkDir,destDir,fileHash,chunks,cb
) {let writeStream  fs.createWriteStream(${ destDir }/${ fileHash }-${ fileName });writeStream.on(finish, async ()  {writeStream.close(); // 关闭try {await fse.rm(tempChunkDir, { recursive: true, force: true });} catch (error) {console.error(tempChunkDir, error);}})let readStreamFun  function(chunks, cb) {try {let val  chunks.shift();let path  join(tempChunkDir, ${ fileHash }-${ val });let readStream  fs.createReadStream(path);readStream.pipe(writeStream, { end: false });readStream.once(end, ()  {console.log(path, path);if(fs.existsSync(path)) {fs.unlinkSync(path);}if (chunks.length  0) {readStreamFun(chunks, cb);} else {cb();}});} catch (error) {console.error( error);}}readStreamFun(chunks, ()  {cb();writeStream.end();});
}// 判断当前文件是否已经存在
function isFileOrDirInExist(filePath) {return fs.existsSync(filePath);
};// 删除文件夹内的内容胆保留文件夹
function rmDirContents(dirPath) {fs.readdirSync(dirPath).forEach(file  {let curPath  join(dirPath, file);if (fs.lstatSync(curPath).isDirectory()) {rmDirContents(curPath);} else {fs.unlinkSync(curPath);}});
}// 获取已上传chunks序号
async function getUploadedChunksIdx(tempChunkDir, fileHash) {if (!isFileOrDirInExist(tempChunkDir)) return []; // 不存在直接返回[]let uploadedChunks  await fse.readdir(tempChunkDir);let uploadedChunkArr  uploadedChunks.filter(file  file.startsWith(fileHash  -)).map(file  parseInt(file.split(-)[1], 10));return [ ...(new Set(uploadedChunkArr.sort((a, b)  a - b))) ];
}requestRouter.post(/api/upload/check, async function(req, res) {try {let fileHash  req.body?.fileHash;let chunkTotal  req.body?.chunkTotal;let fileName  req.body?.fileName;let tempChunkDir  join(UPLOAD_DIR, temp, fileHash); // 存储切片的临时文件夹if (!fileHash || chunkTotal  null) {return res.status(200).json({code: 400,massage: 缺少必要的参数,result: null});}let isFileExist  fs.existsSync(join(UPLOAD_FILE_DIR, ${ fileHash }-${ fileName }));// 如果文件存在则清除temp中临时文件和文件夹if (isFileExist) {// 当前文件夹存在if (fs.existsSync(tempChunkDir)){fs.rmSync(tempChunkDir, { recursive: true, force: true });}return res.status(200).json({code: 200,massage: 成功,result: {uploadedChunks: [],isFileExist}})}let duplicateUploadedChunks  await getUploadedChunksIdx(tempChunkDir, fileHash);return res.status(200).json({code: 200,massage: 成功,result: {uploadedChunks: duplicateUploadedChunks,isFileExist}});} catch (error) {console.error(error);rmDirContents(UPLOAD_MULTER_TEMP_DIR); // 删除临时文件return res.status(500).end();}
});requestRouter.post(/api/upload, upload.single(chunk), async function (req, res) {try {let chunk  req.file; // 获取 chunklet index  req.body?.index;let fileName  req.body?.fileName;let fileHash  req.body?.fileHash; // 文件 hashlet chunkHash  req.body?.chunkHash;let chunkTotal  req.body?.chunkTotal; // chunk 总数let tempChunkDir  join(UPLOAD_DIR, temp, fileHash); // 存储切片的临时文件夹if (isFileOrDirInExist(join(UPLOAD_FILE_DIR, ${ fileHash }-${ fileName }))) {rmDirContents(UPLOAD_MULTER_TEMP_DIR); // 删除临时文件return res.status(200).json({code: 100,massage: 该文件已存在,result: fileHash}).end();}// 切片目录不存在则创建try {await fse.access(tempChunkDir, fse.constants.F_OK)} catch (error) {await fse.mkdir(tempChunkDir, { recursive: true });}if (!fileName || !fileHash) {res.status(200).json({code: 400,massage: 缺少必要的参数,result: null});}await fse.rename(chunk.path, join(tempChunkDir, chunkHash));let duplicateUploadedChunks  await getUploadedChunksIdx(tempChunkDir, fileHash); // 获取已上传的chunks// 当全部chunks上传完毕后进行文件合并if (duplicateUploadedChunks.length  Number(chunkTotal)) {mergeChunks(fileName,tempChunkDir,UPLOAD_FILE_DIR,fileHash,duplicateUploadedChunks,()  {rmDirContents(UPLOAD_MULTER_TEMP_DIR); // 删除临时文件res.status(200).json({code: 200,massage: 成功,result: {uploadedChunks: new Array(Number(chunkTotal)).fill().map((_, index)  index),chunkTotal: Number(chunkTotal)}})});} else {res.status(200).json({code: 201,massage: 部分成功,result: {uploadedChunks: duplicateUploadedChunks,chunkTotal: Number(chunkTotal)}})}} catch (error) {console.error(error);rmDirContents(UPLOAD_MULTER_TEMP_DIR); // 删除临时文件return res.status(500).end();}
});module.exports  requestRouter;/api/upload/check接口分析 
获取请求参数 从请求体 req.body 中获取文件的哈希值 fileHash、分片总数 chunkTotal 和文件名 fileName。 参数验证 检查是否获取到了必要的参数fileHash 和 chunkTotal。如果缺少必要的参数则返回状态码 200 和错误信息提示缺少必要的参数。 文件存在性检查 使用 fs.existsSync 方法检查服务器上是否已存在完整的文件文件名由 fileHash 和 fileName 组成。如果文件已存在则 如果存在临时文件夹 tempChunkDir则删除该临时文件夹及其内容。返回状态码 200 和成功信息提示文件已存在并返回已上传的分片列表为空以及文件存在状态为 true。  获取已上传的分片索引 如果文件不存在则调用 getUploadedChunksIdx 函数获取已上传的分片索引。返回状态码 200 和成功信息返回已上传的分片索引列表和文件存在状态为 false。 错误处理 如果在处理过程中发生错误例如文件系统操作失败则捕获错误删除临时文件夹 UPLOAD_MULTER_TEMP_DIR 的内容并返回状态码 500表示服务器内部错误。 
/api/upload接口分析 
获取请求参数和文件 使用 upload.single(‘chunk’) 中间件从请求中获取单个文件分片 chunk。从请求体 req.body 中获取分片索引 index、文件名 fileName、文件哈希值 fileHash、分片哈希值 chunkHash 和分片总数 chunkTotal。 检查文件是否已存在 使用 isFileOrDirInExist 函数检查服务器上是否已存在完整的文件文件名由 fileHash 和 fileName 组成。如果文件已存在则删除临时文件夹 UPLOAD_MULTER_TEMP_DIR 的内容并返回状态码 200 和成功信息提示文件已存在返回文件哈希值。 创建切片目录 使用 fse.access 检查临时切片目录 tempChunkDir 是否存在如果不存在则使用 fse.mkdir 创建该目录。 参数验证 检查是否获取到了必要的参数fileName 和 fileHash。如果缺少必要的参数则返回状态码 200 和错误信息提示缺少必要的参数。 保存分片文件 使用 fse.rename 将上传的分片文件重命名并移动到临时切片目录中文件名使用分片哈希值 chunkHash。 获取已上传的分片索引 调用 getUploadedChunksIdx 函数获取已上传的分片索引列表。 文件合并 如果已上传的分片数量等于分片总数则调用 mergeChunks 函数进行文件合并。文件合并成功后删除临时文件夹 UPLOAD_MULTER_TEMP_DIR 的内容并返回状态码 200 和成功信息返回已上传的分片列表和分片总数。如果文件合并失败返回状态码 500表示服务器内部错误。 返回部分成功信息 如果分片上传成功但未达到分片总数则返回状态码 200 和部分成功信息返回已上传的分片列表和分片总数。 错误处理 如果在处理过程中发生错误例如文件系统操作失败则捕获错误删除临时文件夹 UPLOAD_MULTER_TEMP_DIR 的内容并返回状态码 500表示服务器内部错误。  
效果