技术栈
前端用了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启动服务。
代码地址
前端代码
后端代码
最后
如果理解了整个断点续传的原理,具体的代码编写就比较容易了,可以按照自己的项目需求定制。本文提供的代码只是基础实现,仅供大家参考。
《魔兽世界》大逃杀!60人新游玩模式《强袭风暴》3月21日上线
暴雪近日发布了《魔兽世界》10.2.6 更新内容,新游玩模式《强袭风暴》即将于3月21 日在亚服上线,届时玩家将前往阿拉希高地展开一场 60 人大逃杀对战。
艾泽拉斯的冒险者已经征服了艾泽拉斯的大地及遥远的彼岸。他们在对抗世界上最致命的敌人时展现出过人的手腕,并且成功阻止终结宇宙等级的威胁。当他们在为即将于《魔兽世界》资料片《地心之战》中来袭的萨拉塔斯势力做战斗准备时,他们还需要在熟悉的阿拉希高地面对一个全新的敌人──那就是彼此。在《巨龙崛起》10.2.6 更新的《强袭风暴》中,玩家将会进入一个全新的海盗主题大逃杀式限时活动,其中包含极高的风险和史诗级的奖励。
《强袭风暴》不是普通的战场,作为一个独立于主游戏之外的活动,玩家可以用大逃杀的风格来体验《魔兽世界》,不分职业、不分装备(除了你在赛局中捡到的),光是技巧和战略的强弱之分就能决定出谁才是能坚持到最后的赢家。本次活动将会开放单人和双人模式,玩家在加入海盗主题的预赛大厅区域前,可以从强袭风暴角色画面新增好友。游玩游戏将可以累计名望轨迹,《巨龙崛起》和《魔兽世界:巫妖王之怒 经典版》的玩家都可以获得奖励。
更新日志
- 中国武警男声合唱团《辉煌之声1天路》[DTS-WAV分轨]
- 紫薇《旧曲新韵》[320K/MP3][175.29MB]
- 紫薇《旧曲新韵》[FLAC/分轨][550.18MB]
- 周深《反深代词》[先听版][320K/MP3][72.71MB]
- 李佳薇.2024-会发光的【黑籁音乐】【FLAC分轨】
- 后弦.2012-很有爱【天浩盛世】【WAV+CUE】
- 林俊吉.2012-将你惜命命【美华】【WAV+CUE】
- 晓雅《分享》DTS-WAV
- 黑鸭子2008-飞歌[首版][WAV+CUE]
- 黄乙玲1989-水泼落地难收回[日本天龙版][WAV+CUE]
- 周深《反深代词》[先听版][FLAC/分轨][310.97MB]
- 姜育恒1984《什么时候·串起又散落》台湾复刻版[WAV+CUE][1G]
- 那英《如今》引进版[WAV+CUE][1G]
- 蔡幸娟.1991-真的让我爱你吗【飞碟】【WAV+CUE】
- 群星.2024-好团圆电视剧原声带【TME】【FLAC分轨】