vue 大文件分片上传
小小猴冲刺 人气:0对于大文件的处理,无论是用户端还是服务端,如果一次性进行读取发送、接收都是不可取,很容易导致内存问题。所以对于大文件上传,采用切块分段上传,从上传的效率来看,利用多线程并发上传能够达到最大效率。
本文是基于 springboot + vue 实现的文件上传,本文主要介绍vue实现文件上传的步骤及代码实现,服务端(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) }
加载全部内容