对于大文件的处理,无论是用户端还是服务端,如果一次性进行读取发送、接收都是不可取,很容易导致内存问题。所以对于大文件上传,采用切块分段上传,从上传的效率来看,利用多线程并发上传能够达到最大效率。
本文是基于 springboot + vue 实现的文件上传,本文主要介绍vue实现文件上传的步骤及代码实现,服务端(springboot)的实现步骤及实现请移步本人的另一篇文章:
springboot 大文件上传、分片上传、断点续传、秒传https://blog.csdn.net/qq_43040552/article/details/122510154
本人分析上传总共分为:
文件上传:
import md5 from 'js-md5' //引入MD5加密
import UpApi from '@/api/common.js'
import { concurrentExecution } from '@/utils/jnxh'
/**
* 文件分片上传
* @params file {File} 文件
* @params pieceSize {Number} 分片大小 默认3MB
* @params concurrent {Number} 并发数量 默认2
* @params process {Function} 进度回调函数
* @params success {Function} 成功回调函数
* @params error {Function} 失败回调函数
*/
export const uploadByPieces = ({
file,
pieceSize = 3,
concurrent = 3,
success,
process,
error
}) => {
// 如果文件传入为空直接 return 返回
if (!file || file.length < 1) {
return error('文件不能为空')
}
let fileMD5 = '' // 总文件列表
const chunkSize = pieceSize * 1024 * 1024 // 1MB一片
const chunkCount = Math.ceil(file.size / chunkSize) // 总片数
const chunkList = [] // 分片列表
let uploaded = [] // 已经上传的
let fileType = '' // 文件类型
// 获取md5
/***
* 获取md5
**/
const readFileMD5 = () => {
// 读取视频文件的md5
fileType = file.name.substring(file.name.lastIndexOf('.') + 1, file.name.length)
console.log('获取文件的MD5值')
let fileRederInstance = new FileReader()
console.log('file', file)
fileRederInstance.readAsBinaryString(file)
fileRederInstance.addEventListener('load', e => {
let fileBolb = e.target.result
fileMD5 = md5(fileBolb)
var index = file.name.lastIndexOf('.')
var tp = file.name.substring(index + 1, file.name.length)
let form = new FormData()
form.append('filename', file.name)
form.append('identifier', fileMD5)
form.append('objectType', fileType)
form.append('chunkNumber', 1)
UpApi.uploadChunk(form).then(res => {
if (res.skipUpload) {
console.log('文件已被上传')
success && success(res)
} else {
// 判断是否是断点续传
if (res.uploaded && res.uploaded.length != 0) {
uploaded = [].concat(res.uploaded)
}
console.log('已上传的分片:' + uploaded)
// 判断是并发上传或顺序上传
if (concurrent == 1 || chunkCount == 1) {
console.log('顺序上传')
sequentialUplode(0)
} else {
console.log('并发上传')
concurrentUpload()
}
}
}).catch((e) => {
console.log('文件合并错误')
console.log(e)
})
})
}
/***
* 获取每一个分片的详情
**/
const getChunkInfo = (file, currentChunk, chunkSize) => {
let start = currentChunk * chunkSize
let end = Math.min(file.size, start + chunkSize)
let chunk = file.slice(start, end)
return {
start,
end,
chunk
}
}
/***
* 针对每个文件进行chunk处理
**/
const readChunkMD5 = () => {
// 针对单个文件进行chunk上传
for (var i = 0; i < chunkCount; i++) {
const {
chunk
} = getChunkInfo(file, i, chunkSize)
// 判断已经上传的分片中是否包含当前分片
if (uploaded.indexOf(i + '') == -1) {
uploadChunk({
chunk,
currentChunk: i,
chunkCount
})
}
}
}
/***
* 原始上传
**/
const uploadChunk = (chunkInfo) => {
var sd = parseInt((chunkInfo.currentChunk / chunkInfo.chunkCount) * 100)
console.log(sd, '进度')
process(sd)
console.log(chunkInfo, '分片大小')
let inde = chunkInfo.currentChunk + 1
if (uploaded.indexOf(inde + '') > -1) {
const {
chunk
} = getChunkInfo(file, chunkInfo.currentChunk + 1, chunkSize)
uploadChunk({
chunk,
currentChunk: inde,
chunkCount
})
} else {
var index = file.name.lastIndexOf('.')
var tp = file.name.substring(index + 1, file.name.length)
// 构建上传文件的formData
let fetchForm = new FormData()
fetchForm.append('identifier', fileMD5)
fetchForm.append('chunkNumber', chunkInfo.currentChunk + 1)
fetchForm.append('chunkSize', chunkSize)
fetchForm.append('currentChunkSize', chunkInfo.chunk.size)
const chunkfile = new File([chunkInfo.chunk], file.name)
fetchForm.append('file', chunkfile)
// fetchForm.append('file', chunkInfo.chunk)
fetchForm.append('filename', file.name)
fetchForm.append('relativePath', file.name)
fetchForm.append('totalChunks', chunkInfo.chunkCount)
fetchForm.append('totalSize', file.size)
fetchForm.append('objectType', tp)
// 执行分片上传
let config = {
headers: {
'Content-Type': 'application/json',
'Accept': '*/*'
}
}
UpApi.uploadChunk(fetchForm, config).then(res => {
if (res.code == 200) {
console.log('分片上传成功')
uploaded.push(chunkInfo.currentChunk + 1)
// 判断是否全部上传完
if (uploaded.length == chunkInfo.chunkCount) {
console.log('全部完成')
success(res)
process(100)
} else {
const {
chunk
} = getChunkInfo(file, chunkInfo.currentChunk + 1, chunkSize)
uploadChunk({
chunk,
currentChunk: chunkInfo.currentChunk + 1,
chunkCount
})
}
} else {
console.log(res.msg)
}
}).catch((e) => {
error && error(e)
})
// if (chunkInfo.currentChunk < chunkInfo.chunkCount) {
// setTimeout(() => {
//
// }, 1000)
// }
}
}
/***
* 顺序上传
**/
const sequentialUplode = (currentChunk) => {
const {
chunk
} = getChunkInfo(file, currentChunk, chunkSize)
let chunkInfo = {
chunk,
currentChunk,
chunkCount
}
var sd = parseInt((chunkInfo.currentChunk / chunkInfo.chunkCount) * 100)
process(sd)
console.log('当前上传分片:' + currentChunk)
let inde = chunkInfo.currentChunk + 1
if (uploaded.indexOf(inde + '') > -1) {
console.log('分片【' + currentChunk + '】已上传')
sequentialUplode(currentChunk + 1)
} else {
let uploadData = createUploadData(chunkInfo)
let config = {
headers: {
'Content-Type': 'application/json',
'Accept': '*/*'
}
}
// 执行分片上传
UpApi.uploadChunk(uploadData, config).then(res => {
if (res.code == 200) {
console.log('分片【' + currentChunk + '】上传成功')
uploaded.push(chunkInfo.currentChunk + 1)
// 判断是否全部上传完
if (uploaded.length == chunkInfo.chunkCount) {
console.log('全部完成')
success(res)
process(100)
} else {
sequentialUplode(currentChunk + 1)
}
} else {
console.log(res.msg)
}
}).catch((e) => {
error && error(e)
})
}
}
/***
* 并发上传
**/
const concurrentUpload = () => {
for (var i = 0; i < chunkCount; i++) {
chunkList.push(Number(i))
}
console.log('需要上传的分片列表:' + chunkList)
concurrentExecution(chunkList, concurrent, (curItem) => {
return new Promise((resolve, reject) => {
const {
chunk
} = getChunkInfo(file, curItem, chunkSize)
let chunkInfo = {
chunk,
currentChunk: curItem,
chunkCount
}
var sd = parseInt((chunkInfo.currentChunk / chunkInfo.chunkCount) * 100)
process(sd)
console.log('当前上传分片:' + curItem)
let inde = chunkInfo.currentChunk + 1
if (uploaded.indexOf(inde + '') == -1) {
// 构建上传文件的formData
let uploadData = createUploadData(chunkInfo)
// 请求头
let config = {
headers: {
'Content-Type': 'application/json',
'Accept': '*/*'
}
}
UpApi.uploadChunk(uploadData, config).then(res => {
if (res.code == 200) {
uploaded.push(chunkInfo.currentChunk + 1)
console.log('已经上传完成的分片:' + uploaded)
// 判断是否全部上传完
if (uploaded.length == chunkInfo.chunkCount) {
success(res)
process(100)
}
resolve()
} else {
reject(res)
console.log(res.msg)
}
}).catch((e) => {
reject(res)
error && error(e)
})
} else {
console.log('分片【' + chunkInfo.currentChunk + '】已上传')
resolve()
}
})
}).then(res => {
console.log('finish', res)
})
}
/***
* 创建文件上传参数
**/
const createUploadData = (chunkInfo) => {
let fetchForm = new FormData()
fetchForm.append('identifier', fileMD5)
fetchForm.append('chunkNumber', chunkInfo.currentChunk + 1)
fetchForm.append('chunkSize', chunkSize)
fetchForm.append('currentChunkSize', chunkInfo.chunk.size)
const chunkfile = new File([chunkInfo.chunk], file.name)
fetchForm.append('file', chunkfile)
// fetchForm.append('file', chunkInfo.chunk)
fetchForm.append('filename', file.name)
fetchForm.append('relativePath', file.name)
fetchForm.append('totalChunks', chunkInfo.chunkCount)
fetchForm.append('totalSize', file.size)
fetchForm.append('objectType', fileType)
return fetchForm
}
readFileMD5() // 开始执行代码
}
并发控制:
/**
* 并发执行
* @params list {Array} - 要迭代的数组
* @params limit {Number} - 并发数量控制数,最好小于3
* @params asyncHandle {Function} - 对`list`的每一个项的处理函数,参数为当前处理项,必须 return 一个Promise来确定是否继续进行迭代
* @return {Promise} - 返回一个 Promise 值来确认所有数据是否迭代完成
*/
export function concurrentExecution(list, limit, asyncHandle) {
// 递归执行
let recursion = (arr) => {
// 执行方法 arr.shift() 取出并移除第一个数据
return asyncHandle(arr.shift()).then(() => {
// 数组还未迭代完,递归继续进行迭代
if (arr.length !== 0) {
return recursion(arr)
} else {
return 'finish'
}
})
}
// 创建新的并发数组
let listCopy = [].concat(list)
// 正在进行的所有并发异步操作
let asyncList = []
limit = limit > listCopy.length ? listCopy.length : limit
console.log(limit)
while (limit--) {
asyncList.push(recursion(listCopy))
}
// 所有并发异步操作都完成后,本次并发控制迭代完成
return Promise.all(asyncList)
}