大文件分片上传基本思路
- 通过input表单,或者拖拽的方式拿到文件对象
- 通过FIlereader对象拿到处理后的二进制的文件通过spark-md5计算文件哈希值
- 通过正则表达式拿到文件的后缀名
- 由于拿到的文件对像原型链指向了blob对象,所以可以使用blob对象身上的slice方法对文件进行切割
- 将切割的文件放到一个对象中,对象添加文件名属性,值为哈希值加上切割文件的块号加后缀名,包装好的对象组成数组返回一个切割好的数组
- 放到并发控制的函数中进行上传
具体实现
获取文件
- input获取
input.addEventListener('change', e => {
const files = e.target.files
})
- 拖拽获取
box.addEventListener('dragover', e => {
e.preventDefault()
})
box.addEventListener('drop', function (e) {
e.preventDefault()
const files = e.dataTransfer.files
})
计算哈希值,获取文件后缀名
const readerFile = new FileReader()
readerFile.readAsArrayBuffer(file.file)
readerFile.onload = e => {
let buffer = e.target.result
spark = new SparkMD5.ArrayBuffer()
var HASH = null
var SUFFIX = null
spark.append(buffer)
HASH = spark.end() //获取计算的哈希结果
SUFFIX = /\.([a-zA-Z0-9]+)$/.exec(file.fileName)[1] //得到文件后缀名
对文件进行分块包装
let max = 1024 * 100, //这里设置为每一快为100KB
count = Math.ceil(file.file.size / max), //计算要分多少块
index = 0
chunks = []
if (count > 100) { //如果大于100块按一百块算重新分配每一块的大小,后期有并发控制可以不用写这个
max = file.file.size / 100
count = 100
}
while (index < count) {
chunks.push({
file: file.file.slice(index * max, (index + 1) * max), //对文件进行切割slice截取不到end位置
filename: `${HASH}_${index + 1}.${SUFFIX}` //加上文件名用来让服务器做判断
})
index++
}
性能优化
由于js线程和GUI线程是互斥的,所以在js线程进行大量计算的时候,会造成页面进入假死状态,为了避免这种问题需要我们手动开启一个线程,
webworker
- webworker可以让浏览器单独开一一个线程运行脚本
- 在这个独立的线程中无法操作DOM对象,跟主线程的内存不共享可以使用BOM的相关API
- 数据需要特定的方式传递给worder线程,这种传递方式是复制传递的方式,当主线程中的数据更新时,没有计时传递给wordkr线程,这时woker线程中的数据还是之前的数据
- worker线程可以通过importScripts()方法加载辅助包
创建worker
const worker = new Worker('worder.ja') //创建worker,worker.js是你要执行的脚本路径同级目录下直接写名字
worker.postMessage(files) //将得到的文件对象传递给worker线程
worker.onmessage = (e)=>{ // 接受worder传递回来的参数
console.log(e.data) //参数在e.data中
}
在worker中接受文件对象
onmessage = (e)=>{
const files = e.data //拿到文件对象后既可以执行相关切片操作了
}
并发处理
可以在worker中进行文件上传,但是我使用时遇到了问题表单请求头的boundary这个参数不会自动生成,所以在主线程中进行了上传还好http请求时单独的线程,不知道在webpack中是否有这个问题
这个是根据Promise结合递归的理念实现的
这种实现方式并不是最优解,只是阐述了一种思路
let chunk = 0 //首先初始化一个标记用来记录上传了几个数据块
lalala() //调用函数
async function lalala() {
if (chunk === chunks.length) { //当上传的数据块跟总数据快相等时向服务器发送合并文件请求,并结束
return request.post(
'/upload_merge',
{ HASH, count: chunks.length },
{
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
}
)
}
if(uploadChunk.includes(item => item === chunks[chunk].filename)){
chunk++
return lalala() //uploadChunk是向服务器请求的已经上传完毕的数据块的名字,已经上传过的
//不需要在上传一遍,这里进行了判断如果上传过了,就结束对下一个数据块进行上传,实现断点续传功能
}
for (let i = 0; i <= 5; i++) { //主要是for巡航控制了并发数量这了设置了5
if (chunk === chunks.length) return //进行判断如果已经上传的块和总数相等结束循环
let fm = new FormData() //添加到formdata对象中发送给服务器
fm.append('file', chunks[chunk].file)
fm.append('filename', chunks[chunk].filename)
++chunk //添加完毕后将要上传,对上传标记加一
try {
const rs = await request.post('/upload_chunk', fm)
if (rs.code === 0) { //当前发送成功时再次调用并发函数
lalala()
} else {
return Promise.reject()
}
} catch (err) {
console.log(err)
}
}