React+EggJs实现断点续传的示例代码


Posted in Javascript onJuly 07, 2020

技术栈

前端用了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 断点续传内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木! 

Javascript 相关文章推荐
js bind 函数 使用闭包保存执行上下文
Dec 26 Javascript
jquery获取颜色在ie和ff下的区别示例介绍
Mar 28 Javascript
JavaScript中的类(Class)详细介绍
Dec 30 Javascript
深入理解JavaScript系列(19):求值策略(Evaluation strategy)详解
Mar 05 Javascript
深入学习AngularJS中数据的双向绑定机制
Mar 04 Javascript
jQuery使用中可能被XSS攻击的一些危险环节提醒
May 24 Javascript
微信小程序 开发之全局配置
May 05 Javascript
AngularJS自定义指令实现面包屑功能完整实例
May 17 Javascript
详解基于Node.js的微信JS-SDK后端接口实现代码
Jul 15 Javascript
基于Vue2x实现响应式自适应轮播组件插件VueSliderShow功能
May 16 Javascript
vue根据值给予不同class的实例
Sep 29 Javascript
axios异步提交表单数据的几种方法
Aug 11 Javascript
JS实现联想、自动补齐国家或地区名称的功能
Jul 07 #Javascript
jQuery 动态粒子效果示例代码
Jul 07 #jQuery
Electron实现应用打包、自动升级过程解析
Jul 07 #Javascript
基于Electron实现桌面应用开发代码实例
Jul 07 #Javascript
基于javascript处理nginx请求过程详解
Jul 07 #Javascript
vue-i18n实现中英文切换的方法
Jul 06 #Javascript
vue 实现动态路由的方法
Jul 06 #Javascript
You might like
使用php实现截取指定长度
2013/08/06 PHP
PHP-Java-Bridge使用笔记
2014/09/22 PHP
php获取从html表单传递数组的方法
2015/03/20 PHP
php链表用法实例分析
2015/07/09 PHP
Laravel 实现Controller向blade前台模板赋值的四种方式小结
2019/10/22 PHP
js 文件引入实现代码
2010/04/23 Javascript
javascript 折半查找字符在数组中的位置(有序列表)
2010/12/09 Javascript
JQuery之focus函数使用介绍
2013/08/20 Javascript
jquery 构造函数在表单提交过程中修改数据
2015/05/25 Javascript
JavaScript获取键盘按键的键码(参照表)
2017/01/10 Javascript
Angular2学习教程之TemplateRef和ViewContainerRef详解
2017/05/25 Javascript
JS写谷歌浏览器chrome的外挂实例
2018/01/11 Javascript
基于jquery的on和click的区别详解
2018/01/15 jQuery
vue axios请求超时的正确处理方法
2018/04/02 Javascript
JS正则表达式常见用法实例详解
2018/06/19 Javascript
详解JavaScript实现动态的轮播图效果
2019/04/29 Javascript
javascript绘制简单钟表效果
2020/04/07 Javascript
[01:55]2014DOTA2国际邀请赛快报:国土生病 紧急去医院治疗
2014/07/10 DOTA
[48:23]DOTA2上海特级锦标赛主赛事日 - 4 败者组第四轮#1COL VS EG第一局
2016/03/05 DOTA
[01:01:04]2018DOTA2亚洲邀请赛 4.5 淘汰赛 OpTic vs TNC 第一场
2018/04/06 DOTA
Python跳出循环语句continue与break的区别
2014/08/25 Python
Python中逗号的三种作用实例分析
2015/06/08 Python
基于python的Tkinter实现一个简易计算器
2015/12/31 Python
详解Python如何获取列表(List)的中位数
2016/08/12 Python
Python实用技巧之列表、字典、集合中根据条件筛选数据详解
2018/07/11 Python
python安装dlib库报错问题及解决方法
2020/03/16 Python
keras CNN卷积核可视化,热度图教程
2020/06/22 Python
纽约家具、家居装饰和地毯店:ABC Carpet & Home
2017/06/21 全球购物
小班下学期幼儿评语
2014/12/30 职场文书
小学班主任个人总结
2015/03/03 职场文书
自我推荐信格式模板
2015/03/24 职场文书
党风廉政教育心得体会2016
2016/01/22 职场文书
Golang中interface{}转为数组的操作
2021/04/30 Golang
Pygame Draw绘图函数的具体使用
2021/11/17 Python
Pandas-DataFrame知识点汇总
2022/03/16 Python
Android studio 简单计算器的编写
2022/05/20 Java/Android