React+EggJs實現(xiàn)斷點續(xù)傳的示例代碼
技術棧
前端用了React,后端則是EggJs,都用了TypeScript編寫。
斷點續(xù)傳實現(xiàn)原理
斷點續(xù)傳就是在上傳一個文件的時候可以暫停掉上傳中的文件,然后恢復上傳時不需要重新上傳整個文件。
該功能實現(xiàn)流程是先把上傳的文件進行切割,然后把切割之后的文件塊發(fā)送到服務端,發(fā)送完畢之后通知服務端組合文件塊。
其中暫停上傳功能就是前端取消掉文件塊的上傳請求,恢復上傳則是把未上傳的文件塊重新上傳。需要前后端配合完成。
前端實現(xiàn)
前端主要分為:切割文件、獲取文件MD5值、上傳切割后的文件塊、合并文件、暫停和恢復上傳等功能。
切割文件:這個功能點在整個斷點續(xù)傳中屬于比較重要的一環(huán),這里仔細說明下。我們用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值:我們不能通過文件名來判斷服務端是否存在上傳的文件,因為用戶上傳的文件很可能會有重名的情況。所以應該通過文件內容來區(qū)分,這樣就需要獲取文件的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(()=>{
// ...
})
合并文件:就是等所有文件塊上傳成功后發(fā)送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);
}
后端實現(xiàn)
后端主要的工作是針對文件的操作,比如使用fs-extra模塊獲取文件信息、使用formidable模塊解析上傳的文件等。
大致編寫過程:在egg項目中的app目錄里面找到router.ts文件定義路由,定義路由需要傳入controller方法。所以我們接著編寫controller方法,而該方法主要對請求參數進行處理,調用service方法處理業(yè)務,然后返回結果。主要是router、controller、service三個部分。
環(huán)境搭建
egg文檔蠻全的,可以直接參考egg的文檔。這里就簡單說下搭建步驟。egg文檔
首先執(zhí)行npm init egg --type=ts安裝egg項目,然后找到router.ts文件定義一些路由,比如處理上傳的接口router.post('api/uploadChunk', controller.file.upload);接著分別在controller目錄跟service目錄下創(chuàng)建對應文件,比如cd app/controller/ && touch file.ts;最后在對應的文件編寫具體業(yè)務。
接口編寫
主要有三個接口,分別是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模塊解析上傳的文件塊,把上傳的文件塊統(tǒng)一放到一個目錄,用文件的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結尾。
編寫案例:首先創(chuàng)建測試文件cd test/app/controller && touch file.test.ts,然后在file.test.ts里編寫測試代碼,最后執(zhí)行npm run test-local運行測試案例。
使用app.httpRequest()可以發(fā)送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安裝依賴,本地環(huán)境啟動使用npm run dev即可。生產環(huán)境則先把ts編譯成js,執(zhí)行npm run tsc,然后執(zhí)行npm run start啟動服務。
代碼地址
最后
如果理解了整個斷點續(xù)傳的原理,具體的代碼編寫就比較容易了,可以按照自己的項目需求定制。本文提供的代碼只是基礎實現(xiàn),僅供大家參考。
到此這篇關于React+EggJs實現(xiàn)斷點續(xù)傳的示例代碼的文章就介紹到這了,更多相關React EggJs 斷點續(xù)傳內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
解決React報錯Expected?`onClick`?listener?to?be?a?function
這篇文章主要為大家介紹了React報錯Expected?`onClick`?listener?to?be?a?function解決,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-12-12
React Router中Link和NavLink的學習心得總結
這篇文章主要介紹了React Router中Link和NavLink的學習心得總結,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-12-12
React useContext與useReducer函數組件使用
useContext和useReducer 可以用來減少層級使用, useContext,可以理解為供貨商提供一個公共的共享值,然后下面的消費者去接受共享值,只有一個供貨商,而有多個消費者,可以達到共享的狀態(tài)改變的目的2023-02-02
react中fetch之cors跨域請求的實現(xiàn)方法
本篇文章主要介紹了react中fetch之cors跨域請求的實現(xiàn)方法,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-03-03

