| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197 | <!DOCTYPE html><html lang="en"><head>  <meta charset="UTF-8">  <meta http-equiv="X-UA-Compatible" content="IE=edge">  <meta name="viewport" content="width=device-width, initial-scale=1.0">  <title>BigFileUpload</title></head><body>  <div class="upload-box">    <h2>big file upload</h2>    <div class="get-file">      <input type="file" name="file" id="file">    </div>    <div class="upload-file">      <button id="upload">上传文件</button>    </div>  </div>  <script crossorigin="anonymous" integrity="sha512-bZS47S7sPOxkjU/4Bt0zrhEtWx0y0CRkhEp8IckzK+ltifIIE9EMIMTuT/mEzoIMewUINruDBIR/jJnbguonqQ==" src="https://lib.baomitu.com/axios/0.21.1/axios.min.js"></script>  <script crossorigin="anonymous" integrity="sha384-45XT1VzQggQADTAenPH2Ecf0gLIwfiZ1J+nlE27AA9qXjtUXaplXshIamSqaco/e" src="https://lib.baomitu.com/spark-md5/3.0.0/spark-md5.js"></script>  <script type="module">    const file = document.querySelector('#file')    const uploadBtn = document.querySelector('#upload')    const DEFAULT_CHUNK_SIZE = 20 * 1024 * 1024; // 20MB    const DEFAULT_OPTIONS = {      chunkSize: DEFAULT_CHUNK_SIZE,    };    let uploadId    class FileUploader {      constructor(options) {        this.fileUploaderClientOptions = Object.assign(DEFAULT_OPTIONS, options);      }      /**       * 将file对象进行切片,然后根据切片计算md5       * @param file 要上传的文件       * @returns 返回md5和切片列表       */      async getChunkListAndFileMd5(        file,      ) {        return new Promise((resolve, reject) => {          let currentChunk = 0;          const chunkSize = this.fileUploaderClientOptions.chunkSize;          const chunks = Math.ceil(file.size / chunkSize);          const spark = new SparkMD5.ArrayBuffer();          const fileReader = new FileReader();          const blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;          const chunkList = [];          fileReader.onload = function (e) {            if (e.target.result instanceof ArrayBuffer) {              spark.append(e.target.result);            }            currentChunk++;            if (currentChunk < chunks) {              loadNextChunk();            } else {              const computedHash = spark.end();              resolve({ md5: computedHash, chunkList });            }          };          fileReader.onerror = function (e) {            console.warn('read file error', e);            reject(e);          };          function loadNextChunk() {            const start = currentChunk * chunkSize;            const end = (start + chunkSize) >= file.size ? file.size : start + chunkSize;            const chunk = blobSlice.call(file, start, end);            chunkList.push(chunk);            fileReader.readAsArrayBuffer(chunk);          }          loadNextChunk();        });      }      /**       * 上传文件方法,当FileUploaderClient的配置项中传入了requestOptions才能使用       * 会依次执行getChunkListAndFileMd5、配置项中的initFilePartUploadFunc、配置项中的uploadPartFileFunc、配置项中的finishFilePartUploadFunc       * 执行完成后返回上传结果,若有分片上传失败,则会自动重试       * @param file 要上传的文件       * @returns finishFilePartUploadFunc函数Promise resolve的值       */      async uploadFile(file) {        const requestOptions = this.fileUploaderClientOptions;        const { md5, chunkList } = await this.getChunkListAndFileMd5(file);        const retryList = [];        if (          requestOptions.retryTimes === undefined ||          !requestOptions.initFilePartUploadFunc ||          !requestOptions.uploadPartFileFunc ||          !requestOptions.finishFilePartUploadFunc        ) {          throw Error(            'invalid request options, need retryTimes, initFilePartUploadFunc, uploadPartFileFunc and finishFilePartUploadFunc',          );        }        await requestOptions.initFilePartUploadFunc();        // await requestOptions.uploadPartFileFunc(chunkList[0], 0);        for (let index = 0; index < chunkList.length; index++) {          try {            await requestOptions.uploadPartFileFunc(chunkList[index], index);          } catch (e) {            console.warn(`${index} part upload failed`);            retryList.push(index);          }        }        for (let retry = 0; retry < requestOptions.retryTimes; retry++) {          if (retryList.length > 0) {            console.log(`retry start, times: ${retry}`);            for (let a = 0; a < retryList.length; a++) {              const blobIndex = retryList[a];              try {                await requestOptions.uploadPartFileFunc(                  chunkList[blobIndex],                  blobIndex,                );                retryList.splice(a, 1);              } catch (e) {                console.warn(                  `${blobIndex} part retry upload failed, times: ${retry}`,                );              }            }          }        }        if (retryList.length === 0) {          return await requestOptions.finishFilePartUploadFunc(md5);        } else {          throw Error(            `upload failed, some chunks upload failed: ${JSON.stringify(              retryList,            )}`,          );        }      }    }    const fileUpload = new FileUploader({      retryTimes: 2,      initFilePartUploadFunc: async () => {        const fileName = file.files[0].name        const { data: { data } } = await axios.get('/api/upload-init', {          params: {            name: fileName,          }        })        uploadId = data.uploadId        console.log('初始化上传完成')      },      uploadPartFileFunc: async (chunk, index) => {        const formData = new FormData()        formData.append('uploadId', uploadId)        formData.append('partIndex', index.toString())        formData.append('partFile', chunk)        const { data: { success, data } } = await axios.post('/api/upload-part', formData, {          headers: { 'Content-Type': 'multipart/form-data' },        })        if (success) {          console.log(data)          console.log(`上传分片${data.index}完成`)        }      },      finishFilePartUploadFunc: async (md5) => {        const fileName = file.files[0].name        const { data: { success, data } } = await axios.get('/api/upload-finish', {          params: {            name: fileName,            uploadId,            md5,          }        })        if (success) {          console.log(`上传完成,存储地址为:${data.path}`)          const p = document.createElement('p')          p.textContent = `文件地址:${data.path}`          document.body.appendChild(p)        }      },    })    uploadBtn.addEventListener('click', () => {      fileUpload.uploadFile(file.files[0])    })  </script></body></html>
 |