使用React+EggJs實現斷點續傳

使用React+EggJs實現斷點續傳

技術棧

前端用了React,後端則是EggJs,都用了TypeScript編寫。前端

斷點續傳實現原理

斷點續傳就是在上傳一個文件的時候能夠暫停掉上傳中的文件,而後恢復上傳時不須要從新上傳整個文件。

該功能實現流程是先把上傳的文件進行切割,而後把切割以後的文件塊發送到服務端,發送完畢以後通知服務端組合文件塊。

其中暫停上傳功能就是前端取消掉文件塊的上傳請求,恢復上傳則是把未上傳的文件塊從新上傳。須要先後端配合完成。ios

前端實現

前端主要分爲:切割文件、獲取文件MD5值、上傳切割後的文件塊、合併文件、暫停和恢復上傳等功能。git

  • 切割文件:這個功能點在整個斷點續傳中屬於比較重要的一環,這裏仔細說明下。咱們用ajax上傳一個大文件用的時間會比較長,在上傳途中若是取消掉請求,那在下一次上傳時又要從新上傳整個文件。而經過把大文件分解成若干個文件塊去上傳,這樣在上傳中取消請求,已經上傳的文件塊會保存到服務端,下一次上傳就只須要上傳其餘沒上傳成功的文件塊(不用傳整個文件)。github

    這裏把文件塊放入一個fileChunkList數組,方便後面去獲取文件的MD5值、上傳文件塊等。ajax

    // 使用HTML5的file.slice對文件進行切割,file.slice方法返回Blob對象
    let start = 0;
    while (start < file.size) {
            fileChunkList.push({ file: file.slice(start, start + CHUNK_SIZE) });
            start += CHUNK_SIZE;
    }
  • 獲取文件MD5值:咱們不能經過文件名來判斷服務端是否存在上傳的文件,由於用戶上傳的文件極可能會有重名的狀況。因此應該經過文件內容來區分,這樣就須要獲取文件的MD5值。
    npm

    使用spark-md5模塊獲取文件的MD5值。模塊詳情點擊這裏axios

    // 部分代碼展現
    let spark = new SparkMD5.ArrayBuffer();
    let fileReader = new FileReader();
    fileReader.onload = e => {
            if (e.target && e.target.result) {
                    count++;
                    spark.append(e.target.result as ArrayBuffer);
            }
            if (count < totalCount) {
                    loadNext();
            } else {
                    resolve(spark.end());
            }
    };
    function loadNext() {
            fileReader.readAsArrayBuffer(fileChunkList[count].file);
    }
    loadNext();
  • 上傳切割後的文件塊:根據前面的fileChunkList數組,使用FormData上傳文件塊。後端

    // 部分代碼展現
    Axios.post(uploadChunkPath, formData, {
            headers: { 'Content-Type': 'multipart/form-data' },
            cancelToken: source.token,
    }).then(()=>{
            // ...
    })
  • 合併文件:就是等全部文件塊上傳成功後發送ajax通知服務端,讓服務端把文件塊進行合併。api

    // 部分代碼展現
    Axios.get(mergeChunkPath, {
            params: {
                    fileHash: targetFile,
                    fileName,
            },
    })
  • 暫停功能:把上傳文件塊的請求放到一個數組裏,請求完成的則從數組中刪除;點擊暫停的時候把數組裏全部的請求暫停。數組

    /* 文件塊請求放入數組 */
    const source = CancelToken.source();
    // ...
    axiosList.push(source);
    
    /* 暫停請求 */
    axiosList.forEach((item) => item.cancel('abort'));
    axiosList.length = 0;
    message.error('上傳暫停');
  • 恢復上傳:去服務端查詢已經上傳的文件塊有哪些,而後上傳沒有上傳成功的文件塊。

    // 部分代碼展現
    let uploadedFileInfo = await getFileChunks(this.fileName, this.fileMd5Value);
    if (this.handleUploaded(uploadedFileInfo.fileExist) && uploadedFileInfo.chunkList) {
            this.uploadChunks(this.chunkListInfo, uploadedFileInfo.chunkList, this.fileName);
    }

後端實現

後端主要的工做是針對文件的操做,好比使用fs-extra模塊獲取文件信息、使用formidable模塊解析上傳的文件等。

大體編寫過程:在egg項目中的app目錄裏面找到router.ts文件定義路由,定義路由須要傳入controller方法。因此咱們接着編寫controller方法,而該方法主要對請求參數進行處理,調用service方法處理業務,而後返回結果。主要是router、controller、service三個部分。

  • 環境搭建

    egg文檔蠻全的,能夠直接參考egg的文檔。這裏就簡單說下搭建步驟。egg文檔

    首先執行npm init egg --type=ts安裝egg項目,而後找到router.ts文件定義一些路由,好比處理上傳的接口router.post('api/uploadChunk', controller.file.upload);接着分別在controller目錄跟service目錄下建立對應文件,好比cd app/controller/ && touch file.ts;最後在對應的文件編寫具體業務。
  • 接口編寫

    主要有三個接口,分別是checkChunk、uploadChunk接口和mergeChunk接口。

    • checkChunk接口:首先判斷上傳的文件是否存在,若是存在則告訴前端文件已經上傳成功。文件不存在則再查看存放文件塊的目錄是否存在,目錄存在則把上傳成功的文件塊列表返回給前端。目錄不存在則把空列表返回給前端。

      if (fileInfo.isFileExist) {
        checkResponse.fileExist = true;
      } else {
        const fileList = await ctx.service.file.getFileList(fileMd5Val);
        checkResponse.chunkList = fileList;
        checkResponse.fileExist = false;
      }
      ctx.body = checkResponse;
    • uploadChunk接口:使用formidable模塊解析上傳的文件塊,把上傳的文件塊統一放到一個目錄,用文件的MD5值給目錄命名。

      import { IncomingForm } from 'formidable';
      const form = new IncomingForm();
      form.parse(req, async (err, fields, file) => {
          if (err) return err;
          const md5AndFileNo = fields.md5AndFileNo;
          const fileHash = fields.fileHash;
          const chunkFolder = resolve(this.config.uploadsPath, fileHash as string);
          if (!existsSync(chunkFolder)) {
              await mkdirs(chunkFolder);
          }
          move(file.chunk.path, resolve(`${chunkFolder}/${md5AndFileNo}`));
      });
    • mergeChunk接口:經過文件MD5值,把對應目錄裏面的文件塊用createReadStream跟createWriteStream組合成一個文件。最後在文件組合完成以後刪除文件塊目錄。

      const readStream = createReadStream(path);
      readStream.on('end', () => {
        unlinkSync(path);
        resolve();
      });
      readStream.pipe(writeStream);
  • 單元測試

    測試文件都放在test目錄裏,同時必須用.test.ts結尾。

    編寫案例:首先建立測試文件cd test/app/controller && touch file.test.ts,而後在file.test.ts裏編寫測試代碼,最後執行npm run test-local運行測試案例。

    使用app.httpRequest()能夠發送HTTP請求,而後傳入參數,驗證返回值是否跟預期相等。

    describe('api/checkChunk', () => {
        // 文件不存在的狀況
        it('should GET / file nonExist', async () => {
            const testHash = 'e62d28dd31fc4d1e92a81e7ae5be3cc6';
            const result = await app.httpRequest()
                .get('/api/checkChunk')
                .query({ fileName: '歸檔 2.zip', fileMd5Val: testHash })
                .expect(200);
            assert.deepEqual(result.body, { hash: testHash, fileExist: false, chunkList: [] });
        });
    });
  • 運行

    使用npm i安裝依賴,本地環境啓動使用npm run dev便可。生產環境則先把ts編譯成js,執行npm run tsc,而後執行npm run start啓動服務。

代碼地址

前端代碼
後端代碼

最後

若是理解了整個斷點續傳的原理,具體的代碼編寫就比較容易了,能夠按照本身的項目需求定製。本文提供的代碼只是基礎實現,僅供你們參考。

相關文章
相關標籤/搜索