发送大文件时,大家都会用的“切片和合并”技术
前言
大概有这么一个需求:
-
在发送大文件的时候,提高文件的发送速度,将时间最好控制在5分钟内。 -
增加进度条提示。
界面截图:

总体思路:分片上传,且每个分片都都需要有个编号,以便后端去校验。因此,每一次分片上传,都需要上传该片段的chunk
,以及chunkIndex
和chunkTotal
,和整个文件的fileHash
和。同时,前后端采用arrayBuffer的blob
格式来进行文件传输。等后端收到所有的切片后,前端再去发起合并请求。(网上的文章都是大体思路)


show code
项目采用react + antd框架为主
1、上传组件得到fileList,可多选文件
<Upload className="uploadbtn" action={baseUrl + '/Email/send'} showUploadList={false} beforeUpload={this.handleBeforeUpload} multiple={true} >
<span className="iconfont iconfujian"></span>添加附件
</Upload>
byteConvert(bytes) {//字节转换
if (isNaN(bytes)) {
return '';
}
var symbols = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
var exp = Math.floor(Math.log(bytes) / Math.log(2));
if (exp < 1) {
exp = 0;
}
var i = Math.floor(exp / 10);
bytes = bytes / Math.pow(2, 10 * i);
if (bytes.toString().length > bytes.toFixed(2).toString().length) {
bytes = bytes.toFixed(2);
}
return bytes + ' ' + symbols[i];
}
handleBeforeUpload = (file) => {//上传附件
let fileList = this.state.fileList
let size = this.byteConvert(file.size)
file.sizeC = size
fileList.push(file)
this.setState({
fileList: fileList
})
return false
}
得到的fileList是个数组,格式如下:

2、接着我们就可以开心的,对这fileList一系列疯狂输出了。
点击发送后
const DEFAULT_CHUNK_SIZE = 100 * 1024;
const MAX_CHUNK_COUNT = 30;
//生成uuid,用于判断是哪次邮件的发送
guuid() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
var r = Math.random() * 16 | 0,
v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
//计算hash值
computeHash = (fileChunks) => {
return new Promise((resolve, reject) => {
const hashWorker = new Worker('./hash.js'); //创建实例
hashWorker.postMessage({ fileChunks }); //向work进程发送消息,发送被分片后的数组对象
//主进程接受消息
hashWorker.onmessage = (e) => {
const { percentage, hash } = e.data;
if (hash) {
resolve(hash);
}
};
});
}
//创建区块(分成多少块)
createChunks = (file, chunkSize = DEFAULT_CHUNK_SIZE) => {
const fileChunkList = [];
let cur = 0;
while (cur < file.size) {
fileChunkList.push({ fileChunk: file.slice(cur, cur + chunkSize) });
cur += chunkSize;
}
return fileChunkList;
}
//定义区块的大小
fileChunkSize(fileList) {
if (fileList.length === 0) return;
const chunkCount = Math.ceil(fileList.size / DEFAULT_CHUNK_SIZE);
if (chunkCount > MAX_CHUNK_COUNT) {
return Math.ceil(fileList.size / MAX_CHUNK_COUNT);
} else {
return DEFAULT_CHUNK_SIZE;
}
}
//上传切片
uploadChunks = async (chunks) => {
let modifyNum = 0
if (chunks.length < 1) return;
this.setState({
chunks: chunks
})
let reqList = chunks.map(({ chunk, chunkIndex, fileHash, chunkSize, filename, chunkTotal }) => {
let formData = new FormData();
formData.append('chunk', chunk);
formData.append('chunkIndex', chunkIndex);
formData.append('fileHash', fileHash);
formData.append('filename', filename);
formData.append('EMID', this.state.uuid);
formData.append('chunkTotal', chunkTotal);
formData.append('chunkSize', chunkSize);
return { formData, chunkIndex };
}).map(({ formData, chunkIndex }, index) => {
return sendPartitionFile(formData).then(res => {
if (res.code === 200) {
modifyNum ++
//设置进度条
this.setState({
stepPercent: Math.floor(modifyNum / chunks.length * 100)
})
} else {
//如果code不为200,抛错误到外面的catch
throw '系统错误,请重发'
}
}).catch(err =>{
//接受到错误,抛错误到外面的catch
throw '系统错误,请重发'
})
});
// 发送切片
await Promise.all(reqList).then(res => {
}).catch(err => {
//接受到错误,在这里终止断js的进程程序,不再继续往下走了
message.error('系统错误,请重发');
this.setState({
chunks: [],
stepPercent: 0,
saveEmailLoading: false // 点发送、存草稿按钮时的loading
})
throw '系统错误,请重发'
});
if (reqList.length === chunks.length) {
// 发送合并请求
await this.mergeRequest(this.state.uuid);
this.setState({
saveEmailLoading: false // 点发送、存草稿按钮时的loading
})
message.success('发送成功');
this.setState({
fileAffixId: '',
uuid: '',
chunks: [],
stepPercent: 0,
fileList: [],
receiptTreechecked: [],
carbonTreechecked: [],
receiptCheckedList: [],
receiptCheckedIDList: [],
carbonCheckedList: [],
carbonCheckedIDList: [],
themeTitle: '',
editorContent: '',
receiptSelectName: [], // 收件人展示框显示的机构和人员名称
carbonSelectName: [] // 抄送人展示框显示的机构和人员名称
})
}
}
//合并切片的请求
mergeRequest = async (uuid) => {
let data = {
emid: uuid,
affids: this.state.fileAffixId,
title: this.state.themeTitle,
content: this.state.editorContent,
userid: store.getState().userId,
username: store.getState().userName,
receuser: null,
receusername: null,
copyuser: null,
copyusername: null,
}
data.receusername = this.state.receiptCheckedList.join(',') // 人员的名字
data.receuser = this.state.receiptCheckedIDList.join(',') // 收件人id
data.copyusername = this.state.carbonCheckedList.join(',')
data.copyuser = this.state.carbonCheckedIDList.join(',')
return mergeFile(data).then(res => {
console.log(res);
if (res.code === 200) {
} else {
message.error(res.msg);
this.setState({
saveEmailLoading: false // 点发送、存草稿按钮时的loading
})
}
}).catch(err =>{
message.error(res.msg);
this.setState({
saveEmailLoading: false // 点发送、存草稿按钮时的loading
})
})
}
SendEmail = async (TYPE) => {//发送邮件
let fileList = this.state.fileList;
if (fileList.length > 0) {
//循环整个fileList,对所有文件进行分片
for (let i = 0; i < fileList.length; i++) {
let chunkSize = this.fileChunkSize(fileList[i]); //获得每个文件的总size
const fileChunkList = this.createChunks(fileList[i], chunkSize); //每个文件分成多少块。返回一个对象数组。
let fileHashRef = { current: {} };
let fileAffixIdArr = this.state.fileAffixId.split(',')
fileHashRef.current = await this.computeHash(fileChunkList); //计算hash值
this.setState({
fileHash: fileHashRef.current
})
const primaryFileChunks = fileChunkList.map(
({ fileChunk }, index) => ({
fileHash: fileHashRef.current,
chunk: fileChunk,
chunkIndex: `${index}`,
percent: 0,
chunkSize: chunkSize,
filename: fileList[i].name,
chunkTotal: fileChunkList.length
})
);
let chunkArr = [];
chunkArr = primaryFileChunks.map(
({ fileHash, chunk, chunkIndex, percent, filename, chunkTotal }) => ({
fileHash,
chunk,
chunkIndex,
percent: 100,
chunkSize: chunkSize,
filename: fileList[i].name,
chunkTotal: chunkTotal
})
);
chunkArrTotal = chunkArrTotal.concat(chunkArr);
}
await this.uploadChunks(chunkArrTotal);
}
}
3、函数讲解以及遇到的难点:
(1)guuid函数
,主要是后端用来辨别你是哪一次发的邮件。
(2)computeHash函数
,这里运用了Worker进程,因为js是单线程的,所有任务只能在一个线程上完成,一次只能做一件事。Web Worker 的作用,就是为 JavaScript 创造多线程环境,允许主线程创建 Worker 线程,将一些任务分配给后者运行。在主线程运行的同时,Worker 线程在后台运行,两者互不干扰。等到 Worker 线程完成计算任务,再把结果返回给主线程。这样的好处是,一些计算密集型或高延迟的任务,被 Worker 线程负担了,主线程(通常负责 UI 交互)就会很流畅,不会被阻塞或拖慢。(所以这里hash计算的过程交给了Worker 线程去处理。)
(3)hash.js
的代码如下:
/* eslint-disable no-restricted-globals */
/* eslint-disable @typescript-eslint/no-explicit-any*/
//selft用法是Worker进程的内置用法
self.importScripts('./spark-md5.min.js'); //引入了md5.js文件,读者可自行百度搜索文件
self.onmessage = (e) => {
const { fileChunks } = e.data; //拿到从主进程发过来的消息
const spark = new self.SparkMD5.ArrayBuffer();
let percentage = 0,
count = 0;
const loadNext = (index) => {
const reader = new FileReader();
reader.readAsArrayBuffer(fileChunks[index].fileChunk);
reader.onload = (e) => {
count++;
spark.append(e.target.result);
if (count === fileChunks.length) {//直到遍历完后,返回文件的hash
self.postMessage({
percentage: 100,
hash: spark.end(),
});
self.close();
} else {
percentage += 100 / fileChunks.length;
self.postMessage({
percentage,
});
loadNext(count);
}
};
};
//迭代遍历
loadNext(0);
};
(4)fileChunkSize
函数定义每个分片的大小,如果觉得文件特别大,可以通过改变MAX_CHUNK_COUNT
的数量,进而去改变分片的大小。
(5)难点:分片上传的时候,由于采用了map方法,所有的分片请求都并发上去了。那么问题了就来了,如果某一个切片出错,或者某一个切片的网络不好了呢。那咋办?读者肯定会说重新发送这个切片呗,这其实也可以做到哈!但我们这里的做法是直接抛错,然后中止运行程序,不让程序继续往下走了。 有个骚操作,通过throw
一层一层的往外抛,等抛到promise.all
的时候的catch
捕捉之后,就会终止程序的运行。后面不会再发送合并切片的请求了。(throw和promise.all
完美结合)
reqList.map(({ formData, chunkIndex }, index) => {
return sendPartitionFile(formData).then(res => {
if (res.code === 200) {
modifyNum ++
//设置进度条
this.setState({
stepPercent: Math.floor(modifyNum / chunks.length * 100)
})
} else {
//如果code不为200,抛错误到外面的catch
throw '系统错误,请重发'
}
}).catch(err =>{
//接受到错误,抛错误到外面的catch
throw '系统错误,请重发'
})
});
// 发送切片
await Promise.all(reqList).then(res => {
}).catch(err => {
//接受到错误,在这里终止断js的进程程序,不再继续往下走了
message.error('系统错误,请重发');
this.setState({
chunks: [],
stepPercent: 0,
saveEmailLoading: false // 点发送、存草稿按钮时的loading
})
throw '系统错误,请重发'
});
(6)bug: 用了Worker进程,会报错误Uncaught SyntaxError: Unexpected token <
错误。针对react项目的代码可能需要经过webpack打包。所以引入外部worker.js
可能导致路径不对,可以将外部worker.js
放在项目根目录,和html
同一个目录,这样创建worker的时候就不会报错了!测试有效。
文章出自:https://juejin.cn/post/7040012368159440910
作者:佩奇是只猫
原文始发于微信公众号(前端24):发送大文件时,大家都会用的“切片和合并”技术
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/216721.html