溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

React+EggJs如何實(shí)現(xiàn)斷點(diǎn)續(xù)傳的

發(fā)布時(shí)間:2020-07-08 11:16:14 來源:億速云 閱讀:311 作者:清晨 欄目:開發(fā)技術(shù)

這篇文章將為大家詳細(xì)講解有關(guān)React+EggJs如何實(shí)現(xiàn)斷點(diǎn)續(xù)傳的,小編覺得挺實(shí)用的,因此分享給大家做個(gè)參考,希望大家閱讀完這篇文章后可以有所收獲。

技術(shù)棧

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

斷點(diǎn)續(xù)傳實(shí)現(xiàn)原理

斷點(diǎn)續(xù)傳就是在上傳一個(gè)文件的時(shí)候可以暫停掉上傳中的文件,然后恢復(fù)上傳時(shí)不需要重新上傳整個(gè)文件。

該功能實(shí)現(xiàn)流程是先把上傳的文件進(jìn)行切割,然后把切割之后的文件塊發(fā)送到服務(wù)端,發(fā)送完畢之后通知服務(wù)端組合文件塊。

其中暫停上傳功能就是前端取消掉文件塊的上傳請求,恢復(fù)上傳則是把未上傳的文件塊重新上傳。需要前后端配合完成。

前端實(shí)現(xiàn)

前端主要分為:切割文件、獲取文件MD5值、上傳切割后的文件塊、合并文件、暫停和恢復(fù)上傳等功能。

切割文件:這個(gè)功能點(diǎn)在整個(gè)斷點(diǎn)續(xù)傳中屬于比較重要的一環(huán),這里仔細(xì)說明下。我們用ajax上傳一個(gè)大文件用的時(shí)間會(huì)比較長,在上傳途中如果取消掉請求,那在下一次上傳時(shí)又要重新上傳整個(gè)文件。而通過把大文件分解成若干個(gè)文件塊去上傳,這樣在上傳中取消請求,已經(jīng)上傳的文件塊會(huì)保存到服務(wù)端,下一次上傳就只需要上傳其他沒上傳成功的文件塊(不用傳整個(gè)文件)。

這里把文件塊放入一個(gè)fileChunkList數(shù)組,方便后面去獲取文件的MD5值、上傳文件塊等。

// 使用HTML5的file.slice對文件進(jìn)行切割,file.slice方法返回Blob對象
let start = 0;
while (start < file.size) {
    fileChunkList.push({ file: file.slice(start, start + CHUNK_SIZE) });
    start += CHUNK_SIZE;
}

獲取文件MD5值:我們不能通過文件名來判斷服務(wù)端是否存在上傳的文件,因?yàn)橛脩羯蟼鞯奈募芸赡軙?huì)有重名的情況。所以應(yīng)該通過文件內(nèi)容來區(qū)分,這樣就需要獲取文件的MD5值。

使用spark-md5模塊獲取文件的MD5值。模塊詳情點(diǎn)擊這里

// 部分代碼展示
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();

上傳切割后的文件塊:根據(jù)前面的fileChunkList數(shù)組,使用FormData上傳文件塊。

// 部分代碼展示
Axios.post(uploadChunkPath, formData, {
    headers: { 'Content-Type': 'multipart/form-data' },
    cancelToken: source.token,
}).then(()=>{
    // ...
})

合并文件:就是等所有文件塊上傳成功后發(fā)送ajax通知服務(wù)端,讓服務(wù)端把文件塊進(jìn)行合并。

// 部分代碼展示
Axios.get(mergeChunkPath, {
    params: {
        fileHash: targetFile,
        fileName,
    },
})

暫停功能:把上傳文件塊的請求放到一個(gè)數(shù)組里,請求完成的則從數(shù)組中刪除;點(diǎn)擊暫停的時(shí)候把數(shù)組里所有的請求暫停。

/* 文件塊請求放入數(shù)組 */
const source = CancelToken.source();
// ...
axiosList.push(source);

/* 暫停請求 */
axiosList.forEach((item) => item.cancel('abort'));
axiosList.length = 0;
message.error('上傳暫停');

恢復(fù)上傳:去服務(wù)端查詢已經(jīng)上傳的文件塊有哪些,然后上傳沒有上傳成功的文件塊。

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

后端實(shí)現(xiàn)

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

大致編寫過程:在egg項(xiàng)目中的app目錄里面找到router.ts文件定義路由,定義路由需要傳入controller方法。所以我們接著編寫controller方法,而該方法主要對請求參數(shù)進(jìn)行處理,調(diào)用service方法處理業(yè)務(wù),然后返回結(jié)果。主要是router、controller、service三個(gè)部分。

環(huán)境搭建

egg文檔蠻全的,可以直接參考egg的文檔。這里就簡單說下搭建步驟。egg文檔

首先執(zhí)行npm init egg --type=ts安裝egg項(xiàng)目,然后找到router.ts文件定義一些路由,比如處理上傳的接口router.post('api/uploadChunk', controller.file.upload);接著分別在controller目錄跟service目錄下創(chuàng)建對應(yīng)文件,比如cd app/controller/ && touch file.ts;最后在對應(yīng)的文件編寫具體業(yè)務(wù)。

接口編寫

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

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

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模塊解析上傳的文件塊,把上傳的文件塊統(tǒng)一放到一個(gè)目錄,用文件的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值,把對應(yīng)目錄里面的文件塊用createReadStream跟createWriteStream組合成一個(gè)文件。最后在文件組合完成之后刪除文件塊目錄。

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

單元測試

測試文件都放在test目錄里,同時(shí)必須用.test.ts結(jié)尾。

編寫案例:首先創(chuàng)建測試文件cd test/app/controller && touch file.test.ts,然后在file.test.ts里編寫測試代碼,最后執(zhí)行npm run test-local運(yùn)行測試案例。

使用app.httpRequest()可以發(fā)送HTTP請求,然后傳入?yún)?shù),驗(yàn)證返回值是否跟預(yù)期相等。

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: [] });
  });
});

運(yùn)行

使用npm i安裝依賴,本地環(huán)境啟動(dòng)使用npm run dev即可。生產(chǎn)環(huán)境則先把ts編譯成js,執(zhí)行npm run tsc,然后執(zhí)行npm run start啟動(dòng)服務(wù)。

代碼地址

關(guān)于React+EggJs如何實(shí)現(xiàn)斷點(diǎn)續(xù)傳的就分享到這里了,希望以上內(nèi)容可以對大家有一定的幫助,可以學(xué)到更多知識。如果覺得文章不錯(cuò),可以把它分享出去讓更多的人看到。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI