大文件上传大概是以下几个步骤:
选择文件: 用户选择要上传的文件,可以通过文件选择框或拖放文件到上传区域来实现。
<template>
<div id="app">
<!-- 上传组件 -->
<el-upload action drag :auto-upload="false" :show-file-list="false" :on-change="handleChange">
<i class="el-icon-upload"></i>
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
<div class="el-upload__tip" slot="tip">大小不超过 10G</div>
</el-upload>
<!-- 进度显示 -->
<div class="progress-box">
<span>上传进度:{{ percent.toFixed() }}%</span>
<el-button type="primary" size="mini" @click="handleClickBtn">{{ upload | btnTextFilter }}</el-button>
</div>
<!-- 展示上传成功的操作 -->
</div>
</template>
- 计算切片文件: 将大文件切分成固定大小的切片,通常使用类似于 2MB 大小的切片。
// 获取文件并转成 ArrayBuffer 对象
const fileObj = file.raw
let buffer;
try {
buffer = await this.fileToBuffer(fileObj)
} catch (e) {
console.log(e)
}
// 将文件按固定大小(2M)进行切片,注意此处同时声明了多个常量
const chunkSize = 2097152,
chunkList = [], // 保存所有切片的数组
chunkListLength = Math.ceil(fileObj.size / chunkSize), // 计算总共多个切片
suffix = /\.([0-9A-z]+)$/.exec(fileObj.name)[1] // 文件后缀名
- 计算文件 hash并开始切片: 使用类似于 MD5 或 SHA256 等算法计算文件的唯一标识,用于后续校验文件完整性。我用的md5。
// 根据文件内容生成 hash 值
const spark = new SparkMD5.ArrayBuffer()
spark.append(buffer)
const hash = spark.end()
// 生成切片,这里后端要求传递的参数为字节数据块(chunk)和每个数据块的文件名(fileName)
let curChunk = 0 // 切片时的初始位置
for (let i = 0; i < chunkListLength; i++) {
const item = {
chunk: fileObj.slice(curChunk, curChunk + chunkSize),
fileName: `${hash}_${i}.${suffix}` // 文件名规则按照 hash_1.jpg 命名
}
curChunk += chunkSize
chunkList.push(item)
}
- 并发上传切片: 将切片文件并发上传到服务器,可以使用多线程或异步请求来提高上传效率。其中可以从列表中传入索引。
chunkList.splice(index, 1) // 一旦上传成功就删除这一个 chunk,方便断点续传
// 发送请求
sendRequest() {
const totalChunks = this.chunkList.length; // 总切片数量
const progressPerChunk = 100 / totalChunks; // 每个切片上传所占的进度百分比
const requestList = [] // 请求集合
this.chunkList.forEach((item, index) => {
const fn = () => {
const formData = new FormData()
formData.append('chunk', item.chunk)
formData.append('filename', item.fileName)
formData.append('index', index);
return fileApi.postChunks(formData).then(res => {
if (res.code === 20000) { // 成功
this.percent += progressPerChunk // 改变进度
this.chunkList.splice(index, 1) // 一旦上传成功就删除这一个 chunk,方便断点续传
}
})
}
requestList.push(fn)
})
- 合并切片: 在服务器端接收到所有切片后,发送merge请求将切片按顺序合并成完整的文件。
let i = 0 // 记录发送的请求个数
// 文件切片全部发送完毕后,需要请求 '/merge' 接口,把文件的 hash 传递给服务器
const complete = () => {
fileApi.merge(this.hash).then(res => {
if (res.code === 0) { // 请求发送成功
// this.videoUrl = res.data.path
}
})
}
const send = async () => {
if (!this.upload) return
if (i >= requestList.length) {
// 发送完毕
complete()
return
}
await requestList[i]()
i++
send()
}
send() // 发送请求
},
小问题:并发请求是异步的,响应的时间不确定那怎么确认进度? 通过计算总切片数量,每次响应后this.percent += progressPerChunk // 改变进度