对于大文件的处理,无论是用户端还是服务端,如果一次性进行读取发送、接收都是不可取,很容易导致内存问题。所以对于大文件上传,采用切块分段上传,从上传的效率来看,利用多线程并发上传能够达到最大效率。
本文是基于 springboot + vue 实现的文件上传,本文主要介绍vue实现文件上传的步骤及代码实现,服务端(springboot)的实现步骤及实现请移步本人的另一篇文章:
springboot 大文件上传、分片上传、断点续传、秒传
上传分步:
本人分析上传总共分为:
- MD5读取文件,获取文件的MD5编码
- 请求服务端判断文件是否上传,如上传完成就直接返回文件地址
- 如未上传,判断是否是断点续传
- 判断是并发上传还是顺序上传
- 开始分片文件上传,分片上传完成后写入已上传列表中
- 判断是否上传完成
直接上代码
文件上传:
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) }
到此这篇关于vue 大文件分片上传(断点续传、并发上传、秒传)的文章就介绍到这了,更多相关vue 大文件分片上传内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!