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自动打开页面上链接的实现代码
Sep 25 Javascript
深入理解javascript中的立即执行函数(function(){…})()
Jun 12 Javascript
JavaScript charCodeAt方法入门实例(用于取得指定位置字符的Unicode编码)
Oct 17 Javascript
js时间戳和c#时间戳互转方法(推荐)
Feb 15 Javascript
jquery仿苹果的时间/日期选择效果
Mar 08 Javascript
js实现移动端编辑添加地址【模仿京东】
Apr 28 Javascript
Angular限制input框输入金额(是小数的话只保留两位小数点)
Jul 13 Javascript
史上最全JavaScript数组去重的十种方法(推荐)
Aug 17 Javascript
浅谈React组件之性能优化
Mar 02 Javascript
原来JS还可以这样拆箱转换详解
Feb 01 Javascript
vue移动端模态框(可传参)的实现
Nov 20 Javascript
JavaScript实现浏览器网页自动滚动并点击的示例代码
Dec 05 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的输入输出流
2007/02/14 PHP
php pack与unpack 摸板字符字符含义
2009/10/29 PHP
ThinkPHP处理Ajax返回的方法
2014/11/22 PHP
php array_walk 对数组中的每个元素应用用户自定义函数详解
2016/11/18 PHP
thinkPHP5框架自定义验证器实现方法分析
2018/06/11 PHP
JS 动态加载脚本的4种方法
2009/05/05 Javascript
基于jquery用于查询操作的实现代码
2010/05/10 Javascript
JavaScript对象、属性、事件手册集合方便查询
2010/07/04 Javascript
Javascript中的String对象详谈
2014/03/03 Javascript
对于jQuery性能的一些优化建议
2015/08/13 Javascript
html5+javascript实现简单上传的注意细节
2016/04/18 Javascript
jQuery插件cxSelect多级联动下拉菜单实例解析
2016/06/24 Javascript
jquery+ajax+text文本框实现智能提示完整实例
2016/07/09 Javascript
vue中动态设置meta标签和title标签的方法
2018/07/11 Javascript
vue实现codemirror代码编辑器中的SQL代码格式化功能
2019/08/27 Javascript
layui动态渲染生成左侧3级菜单的方法(根据后台返回数据)
2019/09/23 Javascript
layui时间控件选择时间范围的实现方法
2019/09/28 Javascript
JavaScript写个贪吃蛇小游戏(超详细)
2020/03/17 Javascript
如何使用three.js 制作一个三维的推箱子游戏
2020/07/29 Javascript
JavaScript实现多层颜色选项卡嵌套
2020/09/21 Javascript
[02:19]2014DOTA2国际邀请赛 专访820少年们一起去追梦吧
2014/07/14 DOTA
[00:14]PWL:老朋友Mushi拍VLOG与中国玩家问好
2020/11/04 DOTA
Python登录注册验证功能实现
2018/06/18 Python
对python中for、if、while的区别与比较方法
2018/06/25 Python
python学生管理系统
2019/01/30 Python
浅析Python 简单工厂模式和工厂方法模式的优缺点
2020/07/13 Python
Python SQLAlchemy库的使用方法
2020/10/13 Python
如何用Python 实现全连接神经网络(Multi-layer Perceptron)
2020/10/15 Python
CSS3+font字体文件实现圆形半透明菜单具体步骤(图解)
2013/06/03 HTML / CSS
AmazeUI中各种的导航式菜单与解决方法
2020/08/19 HTML / CSS
游戏商店:Eneba
2020/04/25 全球购物
酒店总经理岗位职责范本
2014/08/08 职场文书
2014年骨干教师工作总结
2014/12/19 职场文书
公司财务部岗位职责
2015/04/14 职场文书
毕业设计工作总结
2015/08/14 职场文书
PHP控制循环操作的时间
2021/04/01 PHP