视频上传-分片上传那点事

在上一篇文章中,我们讲解了从视频上传到保存在服务端的整个过程,在这个过程中,我们又细分了前端上传视频的几种方式,前端处理视频的几种方式,在前后端通信过程中需要注意的哪些点等等。有不清楚的小伙伴可以看看 上篇文章。

紧接上文,我们来讲下文件的分片上传。

我们都知道分片上传是为了提升文件保存的速度。那它是如何实现的呢?下面的流程图会是一个很好的解释:

在这里插入图片描述

接下来,我们一步步的拆解。

前端如何进行分割操作

在上篇文章我们知道,通过 Element.files属性 拿到的是FileList集合。FileList集合由File对象组成。File对象又继承Blob对象。所以File对象可以使用slice方法来完成对文件对象的切割。

slice方法具体明细如下:

含义:Blob.slice() 方法用于创建一个包含源 Blob的指定字节范围内的数据的新 Blob 对象。
返回值:一个新的 Blob 对象,它包含了原始 Blob 对象的某一个段的数据。
参数:3个参数,分别如下:

  • start。第一个会被拷贝进新的 Blob 的字节的起始位置。
  • end。这个下标的对应的字节将会是被拷贝进新的Blob 的最后一个字节。
  • contentType。给新的 Blob 赋予一个新的文档类型。这将会把它的 type 属性设为被传入的值。它的默认值是一个空的字符串。

说了一大堆理论,该是实战了,先说一下思路:

  • 先通过input标签上传文件。
  • 上传文件后,会触发input标签的change事件,在这个事件里,通过event.target.files可以获取到上传的文件对象,并且将它保存在state里。
  • 定义每个文件块的大小,然后使用slice进行分割。

代码如下:

class Video extends React.Component {constructor(props){super(props);this.state = {fileObj: {}}}// 分片上传uploadChunkFile = async () => {// 定义每块体积大小为20MBlet chunk_size = 20 * 1024 * 1024;// 获取上传的文件对象let fileObj = this.state.fileObj;// 获取上传的文件对象的体积let allSize = this.state.fileObj.size;// 获取文件对应的总的分片的数量let allChunkCount = Math.ceil(allSize / chunk_size);// chunk文件集合let chunkArr = [];for (let index = 0; index < allChunkCount; index++){let startIndex = index * chunk_size;let endIndex = Math.min(startIndex + chunk_size, allSize);chunkArr.push({data: fileObj.slice(index * chunk_size,endIndex),filename: `chunk-${index}`,chunkIndex: index});}}// 上传文件触发inputChange = async (event) => {let self = this;let uploadFileObj = event.target.files[0] || {};this.setState(state => {return {...state,fileObj: uploadFileObj}});return}render(){return <div><button onClick={this.testConnect}>测试连接</button><inputtype='file'onChange={(event) => this.inputChange(event)}/><button onClick={this.uploadChunkFile}>分片上传</button</div>}
}

当我们上传一个66M的视频时,我们会发现,总的分片数量是4。符合预期。

发送chunk的几种方式

这块无非就2种,分别如下:

  • 将这些分片按照顺序发送给后端。
  • 将这些分片并发的方式发送给后端。这种方式下,需要考虑浏览器一次只能并发6个请求的情况,并且这种方式也是面试中高频考点(如何控制并发)。

在这个功能点里,我们采用按顺序的方式上传分片,因为这种方式是最直观的,会了这种方式,相信分片上传你就完全会了。

但是在实际的项目中,更多的还是并发的场景(我们下篇文章再讲)。

按照顺序发送

这个就是第一个请求成功后,再去发送第二个请求,以此类推…

它也是面试中的一个常考点:如何按照顺序发送请求?如何实现红绿灯效果?等等。

按顺序发送,2种思路,一种是循环,一种是递归

递归这里不用说,重点讲一下循环。

普通的for循环可以做到吗?

答案是可以的。

let arr = [{name: 1},{name: 2},{name: 3},{name: 4},{name: 5}
];async function ax(){for (let index = 0; index < arr.length; index++){let result = await new Promise((resolve, reject) => {setTimeout(() => {resolve(arr[index].name);}, 1000);});console.log('result:', result);}
}ax();
for…in… 能做到吗?

答案也是可以的

let arr = [{name: 1},{name: 2},{name: 3},{name: 4},{name: 5}
];async function ax(){for (let index in arr){let result = await new Promise((resolve, reject) => {setTimeout(() => {resolve(arr[index].name);}, 1000);});console.log('result:', result);}}ax();
for…of…能做到吗?

答案也是可以的

let arr = [{name: 1},{name: 2},{name: 3},{name: 4},{name: 5}
];async function ax(){for (let index of arr){let result = await new Promise((resolve, reject) => {setTimeout(() => {resolve(index.name);}, 1000);});console.log('result:', result);}}ax();
forEach可以做到吗?

不行,绝对不行

let arr = [{name: 1},{name: 2},{name: 3},{name: 4},{name: 5}
];async function ax(){arr.forEach(async item => {let result = await new Promise((resolve, reject) => {setTimeout(() => {resolve(item.name);}, 1000);});console.log('result:', result);});
}ax();

在这里插入图片描述

MDN上也是这么说的,但是你要问具体原因,那就只能看forEach源码了。我感觉啊,forEach应该是个while循环实现的,外层的函数是个同步函数,所以导致forEach不能按照顺序发送Promise请求。

forEach伪代码如下:

Array.prototype.myForEach = function (cb){let originArr = this;let index = 0;while(index < originArr.length){cb(originArr[index], index);}
}/**即使cb内部是异步操作,但是cb外面的调用方不是异步的,所以导致这种写法并不能按顺序发送Promise请求。
*/
while循环可以做到吗?

答案是可以的

let arr = [{name: 1},{name: 2},{name: 3},{name: 4},{name: 5}
];async function ax(){let index = 0;while(index < arr.length){let result = await new Promise((resolve, reject) => {setTimeout(() => {resolve(arr[index].name);}, 1000);});index++;console.log('result:', result);}}ax();
数组里哪些方法能做到?

这个就要看数组方法的源码了,但是分析过程跟forEach一样,这里就不一一例举了。

代码实践

在上面的分割章节里,我们讲解了File对象的分割,我们继续在原方法里进行改造,从而添加按顺序发送chunk块的需求。


// 分片上传请求
uploadChunkReq = async (fileBlob, chunkIndex, type) => {let formData = new FormData();let result = await axiosInstance.post('/video/uploadChunk',{chunkIndex,type,videoDict: fileBlob,},{headers: {'Content-Type': 'multipart/form-data'}});return result;
}// 分片上传动作
uploadChunkFile = async () => {// 定义每块体积大小为20MBlet chunk_size = 20 * 1024 * 1024;// 获取上传的文件对象let fileObj = this.state.fileObj;// 获取上传的文件对象的体积let allSize = this.state.fileObj.size;// 获取文件对应的总的分片的数量let allChunkCount = Math.ceil(allSize / chunk_size);// chunk文件集合let chunkArr = [];for (let index = 0; index < allChunkCount; index++){let startIndex = index * chunk_size;let endIndex = Math.min(startIndex + chunk_size, allSize);/**每个分片信息都包含:分片的数据、分片的编号、分片的名称*/chunkArr.push({data: fileObj.slice(index * chunk_size,endIndex),filename: `chunk-${index}`,chunkIndex: index});}// 按顺序发送chunk分片for (let item of chunkArr){let result = await this.uploadChunkReq(item.data, item.chunkIndex, 'chunk');console.log('分片上传的结果:', result);}
}

后端如何合并chunk

主要是3件事,

首先要新增一个接口,用来保存分片数据;

其次当所有的分片都保存成功了,应该去合并分片最终形成文件;合并chunk的时机可以是后端自己判断,也可以是前端触发,具体要看场景

最后,删除分片数据。

单独保存chunk

这里我们需要改造原有的方法,看过上一篇的小伙伴都知道,如果express是通过multer第三方库来解析的form-data数据,那它就一定会经过multer里定义的中间件,我们要在这里去将不同类型的文件存放到不同的文件夹里。

// 定义chunk的临时存放路径
var tempChunkPosition = multer({// dest: 'tempChunk'storage: multer.diskStorage({destination: function (req, file, cb) {if (req.body.type == 'chunk'){// 说明是分片上传cb(null, path.join(__dirname, '../tempChunk'));} else {// 说明上传的是小文件cb(null, path.join(__dirname, '../videoDest'));}},filename: function (req, file, cb) {if (req.body.type == 'chunk'){// 如果是分片上传cb(null, file.fieldname + '-' + `${req.body.chunkIndex}` + '-' + Date.now());} else {// 说明上传的是小文件cb(null, file.fieldname + '-' + Date.now() + '.mp4');}}})
});/ 上传切片
router.post('/uploadChunk', tempChunkPosition.single("videoDict"),(req, res, next) => {return res.send({success: true,msg: '上传成功'});
});

合并chunk

这一步就是将临时的分片数据全都读取出来,然后依次将他们写入到文件中。

因为我们在上传分片的过程中,已经将分片的标识索引传给了后端,所以后端无需再对读出来的chunk集合进行顺序排序。

router.post('/mergeChunk',(req, res, next) => {// 获取文件切片的路径let chunkPath = path.join(__dirname, '../tempChunk');// 开始读取切片const chunkArr = fs.readdirSync(chunkPath);chunkArr.forEach(file => {fs.appendFileSync(path.join(__dirname, `../videoDest/${req.body.originFileName}.mp4`),fs.readFileSync(`${chunkPath}/${file}`));});return res.send({success: true,msg: '文件合并成功'});
});

此时我们再对前端的上传分片的函数进行改造,主要就是新增 “合并分片”的动作触发。


// 合并分片请求
mergeChunk = async () => {let result = await axiosInstance.post('/video/mergeChunk',{originFileName: '11'},{headers: {'Content-Type': 'application/json'}});return result;
}//分片上传
uploadChunkFile = async () => {// 前面的都不变......for (let item of chunkArr){let result = await this.uploadChunkReq(item.data, item.chunkIndex, 'chunk');console.log('分片上传的结果:', result);}// 前面的都不变......// 合并请求(新增的++++++++++++++)this.mergeChunk();
}

到这一步,我们的分片上传的流程就已经全部打通了,此时大家上传文件后,就会看到后端的目录里不仅有分片数据,而且还有完整的视频文件。

敬请期待

我们这次讲解了文件的分片上传,但是整体跑下来你会发现,有点太顺风顺水,没有包含错误机制,所以下篇文章,我们不仅会将如何并发控制分片的上传,还会有上传过程中的错误控制。

最后

好啦,本篇文章到这里就结束啦,如果上述过程中有错误的地方,欢迎各位大神指出。希望我的文章对你有帮助,我们下期再见啦~~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/450706.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Spring IoC容器(二)Bean的自定义及容器的扩展

Spring提供了一些接口来让我们定制bean 及扩展容器。 1 定制Bean 我们可以通过bean及容器的生命周期回调及一些Aware接口来定制bean。 1.1 生命周期回调 1.1.1 InitializingBean 与 DisposableBean 我们可以通过让Bean 实现InitializingBean 及DisposableBean 接口&#x…

软件测试学习笔记-测试用例的编写

7中测试分类 按照阶段可划分单元测试、集成测试、系统测试、验收测试。代码可见度划分黑盒测试、灰盒测试、白盒测试 单元测试&#xff1a;针对源代码的测试 集成测试&#xff1a;针对接口进行测试 系统测试&#xff1a;针对功能和非功能的测试 验收测试&#xff1a;公测、内测…

在Vue中如何构建复杂表单?

概述 很有可能&#xff0c;在我们的软件工程旅程中&#xff0c;我们至少要构建一次复杂的表单。本文将介绍如何创建一个复杂的表单&#xff0c;该表单可以使用一些Vue特性(如v-for和v-model)逐步增强。它还提供了一些基本的Vue核心功能的复习&#xff0c;这些功能将在您日常使…

Vue引入Axios

1.命令安装axios和vue-axios npm install axios --save npm install vue-axios --save 2.package.json查看版本 3.在main.js中引用 import axios from axios; import VueAxios from vue-axios; Vue.use(VueAxios,axios) 4.如何使用 &#xff08;初始化方法&#xff09; 将下列代…

sqli.labs靶场(29到40关)

29、第二十九关 id1 id1 尝试发现是单引号闭合&#xff0c; -1 union select 1,2,3-- -1 union select 1,2,database()-- -1 union select 1,2,(select group_concat(table_name) from information_schema.tables where table_schemasecurity)-- -1 union select 1,2,(select…

跳表详解和实现|深挖Redis底层数据结构

文章目录 跳表前言项目代码仓库认识跳表跳表的实现思路跳表性能分析对比平衡树&#xff08;avl和红黑树&#xff09;和哈希表使用手册成员变量成员函数构造析构迭代器sizeclearemptyoperatorfindinserterase 跳表细节实现节点定义跳表结构定义构造、析构、拷贝构造和赋值重载si…

vue3 之 组合式API—reactive和ref函数

ref&#xff08;&#xff09; 作用&#xff1a;接收简单类型或者对象类型的数据传入并返回一个响应式的对象 核心步骤&#xff1a; 1️⃣ 从 vue 包中导入 ref 函数 2️⃣在 <script setup>// 导入import { ref } from vue// 执行函数 传入参数 变量接收const count …

QT 应用中集成 Sentry

QT 应用中集成 Sentry QT应用中集成 SentrySentry SDK for C/C注册 Sentry 账号QT 应用中集成 Sentry触发 Crash 上报 QT应用中集成 Sentry Sentry 是一个开源的错误监控和日志记录平台&#xff0c;旨在帮助开发团队实时捕获、跟踪和解决软件应用程序中的错误和异常。它提供了…

开关电源学习之Buck电路

一、引言 观察上方的电路&#xff0c;当开关闭合到A点时&#xff0c;电流流过电感线圈&#xff0c;形成阻碍电流流过的磁场&#xff0c;即产生相反的电动势&#xff1b;电感L被充磁&#xff0c;流经电感的电流线性增加&#xff0c;在电感未饱和前&#xff0c;电流线性增加&…

07-使用Package、Crates、Modules管理项目

上一篇&#xff1a;06-枚举和模式匹配 当你编写大型程序时&#xff0c;组织代码将变得越来越重要。通过对相关功能进行分组并将具有不同功能的代码分开&#xff0c;您可以明确在哪里可以找到实现特定功能的代码&#xff0c;以及在哪里可以改变功能的工作方式。 到目前为止&…

Python学习从0到1 day12 Python数据容器.3.字符串与数据容器的切片操作

世界之大&#xff0c;从不在手心拙劣的掌纹之下 ——24.2.3 一、字符串的定义和操作 1.定义 字符串是字符的容器&#xff0c;一个字符串可以存放任意数量的字符 2.字符串的下标&#xff08;索引&#xff09; 和其他容器如&#xff1a;列表、元组一样&#xff0c;字符串也可以通…

react+ts+antd-mobile 动态tabs➕下拉加载

1.初始化项目 //搭建项目 npm create vitelatest react-jike-mobile -- --template react-ts//安装依赖 npm i //运行 npm run dev清理项目目录结构 安装ant design mobile ant design mobile是ant design家族里专门针对于移动端的组件库 npm install --save antd-mobile测试…