js大文件上分片上传类

发布于:

#Upload代码

  1. 获取 md5 使用了 spark-md5,后续考虑优化为使用 worker 获取 md5
  2. uploadChunk 额外传入了一个回调,用于实时获取分片上传进度
  3. 根据 file md5 获取已存在在服务器的分片索引,跳过已存在的下载(即秒传)
  4. 按顺序 slice 切片上传
  5. 上传完毕通知服务器合并文件
js
import axios from 'axios' import SparkMD5 from 'spark-md5' const BaseUrl = 'http://localhost:1111' export default class UploadLogic { constructor(chunkSize) { this.chunkSize = chunkSize || 3000 * 1024 // 3000kb一切片 this.chunks = 0 } // 实现文件转换成md5并进行切片的逻辑 async md5File(file) { return new Promise((resolve, reject) => { // 文件截取 const blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice const spark = new SparkMD5.ArrayBuffer() const fileReader = new FileReader() this.chunks = file.size > this.chunkSize ? Math.ceil(file.size / this.chunkSize) : 1 // 总分片数量 fileReader.onload = e => { spark.append(e.target.result) this.checkCurrentChunk += 1 if (this.checkCurrentChunk < this.chunks) { loadNext() } else { let result = spark.end() resolve(result) } } fileReader.onerror = function () { console.error('文件读取错误') } const loadNext = () => { const start = this.checkCurrentChunk * this.chunkSize const end = start + this.chunkSize >= file.size ? file.size : start + this.chunkSize // 文件切片 fileReader.readAsArrayBuffer(blobSlice.call(file, start, end)) this.checkCurrentChunk += 1 } loadNext() }) } // 实现校验文件的md5的逻辑 async checkFileMD5(fileName, fileMd5Value) { let url = BaseUrl + '/check/file?fileName=' + fileName + '&fileMd5Value=' + fileMd5Value return axios.get(url) } // 实现上传chunk的逻辑 async uploadChunk({ i, file, fileMd5Value, callback }) { let end = (i + 1) * this.chunkSize >= file.size ? file.size : (i + 1) * this.chunkSize const formData = new FormData() formData.append('data', file.slice(i * this.chunkSize, end)) formData.append('total', this.chunks) formData.append('index', i) formData.append('fileMd5Value', fileMd5Value) return axios .post(BaseUrl + '/upload', formData, { headers: { 'Content-Type': 'multipart/form-data' }, }) .then(({ data }) => { if (data.stat) { const uploadPercent = ((i / this.chunks) * 100).toFixed(2) typeof callback === 'function' && callback(uploadPercent) } }) } // 实现检查并上传chunk的逻辑 async checkAndUploadChunk(file, fileMd5Value, chunkList, callback) { const uploadPromise = [] for (let i = 0; i < this.chunks; i++) { const isExist = chunkList.indexOf(`${i}`) !== -1 // 该切片尚未上传到服务器 if (!isExist) { uploadPromise.push(this.uploadChunk({ i, file, fileMd5Value, callback })) } } if (uploadPromise.length) { await Promise.all(uploadPromise) } } // 实现通知服务器所有服务器分片已经上传完成的逻辑 async notifyServer(file, fileMd5Value, callback) { let url = BaseUrl + '/merge?md5=' + fileMd5Value + '&fileName=' + file.name + '&size=' + file.size return axios.get(url).then(({ data }) => { if (data.stat) { if (typeof callback === 'function') callback() } else { console.log('上传失败') } }) } }

#React使用代码

jsx
import React, { useEffect, useRef, useState } from "react"; import { Button, Progress, message } from "antd"; import { SlideDown } from "react-slidedown"; import UploadLogic from "./UploadLogic"; import "./style.css"; import "react-slidedown/lib/slidedown.css"; const Upload = () => { const inputRef = useRef(null); const [uploadPercent, setUploadPercent] = useState(0); // 1. 使用状态来存储上传进度 useEffect(() => { const changeFile = ({ target }) => { const file = target.files[0]; responseChange(file); }; const inputDom = inputRef.current; inputDom.addEventListener("change", changeFile); return () => { inputDom.removeEventListener("change", changeFile); }; }, []); const responseChange = async (file) => { setUploadPercent(0); const upload = new UploadLogic(); // 1.校验文件,获取md5 const fileMd5Value = await upload.md5File(file); // 2.校验文件的md5 const { data } = await upload.checkFileMD5(file.name, fileMd5Value); // 如果文件已存在, 就秒传 if (data?.file) { message.success("文件秒传成功"); setUploadPercent(100); return; } // 3:检查并上传切片 await upload.checkAndUploadChunk( file, fileMd5Value, data.chunkList, setUploadPercent ); // // 4:通知服务器所有服务器分片已经上传完成 await upload.notifyServer(file, fileMd5Value, () => { message.success("上传成功"); setUploadPercent(100); }); }; return ( <div className="wrap"> <div className="upload"> <span>点击上传文件:</span> <input ref={inputRef} type="file" id="file" /> <Button type="primary" onClick={() => inputRef.current.click()}> 上传 </Button> </div> {uploadPercent > 0 && ( <SlideDown className={"my-dropdown-slidedown"}> <div className="uploading"> 上传文件进度: <Progress type="circle" percent={uploadPercent} /> </div> </SlideDown> )} </div> ); }; export default Upload;

#后端代码

后端代码见根目录server.js

#仓库地址