1. 程式人生 > 程式設計 >React+EggJs實現斷點續傳的示例程式碼

React+EggJs實現斷點續傳的示例程式碼

技術棧

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

斷點續傳實現原理

斷點續傳就是在上傳一個檔案的時候可以暫停掉上傳中的檔案,然後恢復上傳時不需要重新上傳整個檔案。

該功能實現流程是先把上傳的檔案進行切割,然後把切割之後的檔案塊傳送到服務端,傳送完畢之後通知服務端組合檔案塊。

其中暫停上傳功能就是前端取消掉檔案塊的上傳請求,恢復上傳則是把未上傳的檔案塊重新上傳。需要前後端配合完成。

前端實現

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

切割檔案:這個功能點在整個斷點續傳中屬於比較重要的一環,這裡仔細說明下。我們用ajax上傳一個大檔案用的時間會比較長,在上傳途中如果取消掉請求,那在下一次上傳時又要重新上傳整個檔案。而通過把大檔案分解成若干個檔案塊去上傳,這樣在上傳中取消請求,已經上傳的檔案塊會儲存到服務端,下一次上傳就只需要上傳其他沒上傳成功的檔案塊(不用傳整個檔案)。

這裡把檔案塊放入一個fileChunkList陣列,方便後面去獲取檔案的MD5值、上傳檔案塊等。

// 使用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值。

使用spark-md5模組獲取檔案的MD5值。模組詳情點選這裡

// 部分程式碼展示
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通知服務端,讓服務端把檔案塊進行合併。

// 部分程式碼展示
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啟動服務。

程式碼地址

前端程式碼
後端程式碼

最後
如果理解了整個斷點續傳的原理,具體的程式碼編寫就比較容易了,可以按照自己的專案需求定製。本文提供的程式碼只是基礎實現,僅供大家參考。

到此這篇關於React+EggJs實現斷點續傳的示例程式碼的文章就介紹到這了,更多相關React EggJs 斷點續傳內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!