赞
踩
在上一篇文章中,我们讲解了从视频上传到保存在服务端的整个过程,在这个过程中,我们又细分了前端上传视频的几种方式,前端处理视频的几种方式,在前后端通信过程中需要注意的哪些点等等。有不清楚的小伙伴可以看看 上篇文章。
紧接上文,我们来讲下文件的分片上传。
我们都知道分片上传是为了提升文件保存的速度
。那它是如何实现的呢?下面的流程图会是一个很好的解释:
接下来,我们一步步的拆解。
在上篇文章我们知道,通过 Element.files属性
拿到的是FileList集合。FileList集合由File对象组成。File对象又继承Blob对象。所以File对象可以使用slice方法
来完成对文件对象的切割。
slice方法具体明细如下:
含义:Blob.slice() 方法用于创建一个包含源 Blob的指定字节范围内的数据的新 Blob 对象。
返回值:一个新的 Blob 对象,它包含了原始 Blob 对象的某一个段的数据。
参数:3个参数,分别如下:
说了一大堆理论,该是实战了,先说一下思路:
代码如下:
class Video extends React.Component { constructor(props){ super(props); this.state = { fileObj: {} } } // 分片上传 uploadChunkFile = async () => { // 定义每块体积大小为20MB let 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> <input type='file' onChange={(event) => this.inputChange(event)} /> <button onClick={this.uploadChunkFile}>分片上传</button </div> } }
当我们上传一个66M的视频时,我们会发现,总的分片数量是4。符合预期。
这块无非就2种,分别如下:
在这个功能点里,我们采用按顺序的方式上传分片,因为这种方式是最直观的,会了这种方式,相信分片上传你就完全会了。
但是在实际的项目中,更多的还是并发的场景(我们下篇文章再讲)。
这个就是第一个请求成功后,再去发送第二个请求,以此类推…
它也是面试中的一个常考点:如何按照顺序发送请求?
、 如何实现红绿灯效果?
等等。
按顺序发送,2种思路,一种是循环
,一种是递归
。
递归这里不用说,重点讲一下循环。
答案是可以的。
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();
答案也是可以的
。
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();
答案也是可以的
。
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();
不行,绝对不行
。
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请求。
*/
答案是可以的
。
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 () => { // 定义每块体积大小为20MB let 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); } }
主要是3件事,
首先要新增一个接口,用来保存分片数据;
其次当所有的分片都保存成功了,应该去合并分片最终形成文件;合并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集合进行顺序排序。
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(); }
到这一步,我们的分片上传的流程就已经全部打通了,此时大家上传文件后,就会看到后端的目录里不仅有分片数据,而且还有完整的视频文件。
我们这次讲解了文件的分片上传,但是整体跑下来你会发现,有点太顺风顺水,没有包含错误机制,所以下篇文章,我们不仅会将如何并发控制分片的上传,还会有上传过程中的错误控制。
好啦,本篇文章到这里就结束啦,如果上述过程中有错误的地方,欢迎各位大神指出。希望我的文章对你有帮助,我们下期再见啦~~
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。