# aeip_upload **Repository Path**: aeipyuan/aeip_upload ## Basic Information - **Project Name**: aeip_upload - **Description**: No description available - **Primary Language**: Unknown - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 3 - **Forks**: 1 - **Created**: 2020-11-22 - **Last Updated**: 2022-05-29 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 前端 > 简介:一个基于Vue的实现并发数量控制,切片上传的Upload组件 ## 1.组件注册 ```javascript /* 拉取npm包 */ npm i aeip_uplpoad /* 全局注册 */ import AeipUpload from 'aeip_upload' Vue.use(AeipUpload) /* 局部注册 */ import { AeipUpload } from 'aeip_upload' export default { components: { AeipUpload }, //... } ``` ## 2.配置项 ```html ``` - 参数说明 |变量名|含义| |--|--| |url|上传的目标地址,后台需要有`/multi`和`/merge`两个路径负责接受切片和合并切片| |limit|文件数量限制,Number类型| |maxSize|每个文件大小限制,Number类型| |tip|关于上传的用户提示,String类型| |multiple|多选开关,Boolean类型| |fileTypes|支持的文件类型,Array类型| - 事件 |事件名|含义| |--|--| |file-success|文件上传成功的回调,类型function(file,fileList) |file-error|文件上传失败的回调,类型function(file,fileList) |file-remove|文件移除的回调,类型function(file,fileList) |file-click|点击单个文件的回调,类型function(file,fileList) ## 3.并发控制原理 为了实现同时发送多个请求的功能,利用最大并发数和请求队列控制请求发送的时机,具体操作为:将file的每个切片生成一个req,然后将req放置到请求队列reqQueue中,request函数负责验证是否还有并发数和未发送的请求,每次发起一个请求将会减少一个并发数,在两种情况下或调用request,一是生成请求的时候,二是完成请求释放并发数后会调用request处理后面的请求 ```json fileObj数据结构: { "name": "图片1.jpg",// 文件全名 "filename": "图片1",// 文件前缀 "suffix": ".jpg",//文件后缀 "hash": "6003290-1547625882000", // 文件标记,size+lastmodified(md5虽然准确但是费时间不划算) "upTime": "1610457308869",// 上传时间 "prevSrc": "blob:http://localhost:8080/e191fe8f-923e-4f62-a6ab-a8f05cf92733",// 临时预览路径 "src": "",// 文件最终src "chunks": [], // 切片 "finishCnt": 0,// 完成的切片数量 "errReqs": [],// 失败的请求回调 "isFinished": false, // 完成标志 } // { // chunks: Array(32) // errReqs: Array(32) // filename: "图片1" // finishCnt: 0 // hash: "551494691-1592645001441" // isFinished: false // name: "图片1.jpg" // prevSrc: "data:image/png..." // src: "" // suffix: ".jpg" // upTime: "1610456783283" // } ``` ```javascript // 上传切片 uploadChunks(fileObj) { // console.log(fileObj) fileObj.chunks.map((chunk, index) => { // 生成formdata let fd = $utils.getFd(fileObj, chunk, index); // 添加请求 const req = () => { // 已经成功则不发送请求 if (fileObj.isFinished) { this.reqCnt++; this.request(); return; } let params = { url: this.url, data: fd, path: '/multi' }; $utils.ajax(params).then(data => { if (data.isFinished == true) { fileObj.finishCnt = fileObj.chunks.length; // 第一次完成触发回调 fileObj.isFinished || this.$emit('file-success', fileObj, this.fileList); fileObj.isFinished = true; } else { // 所有切片上传完毕 if (fileObj.finishCnt + 1 == fileObj.chunks.length) { fileObj.finishCnt += 0.2; // console.log(fileObj.name + '----所有切片上传完毕'); // 合并切片 const merge = () => { $utils.ajax({ url: this.url, path: '/merge', data: `filename=${fileObj.filename}&suffix=${fileObj.suffix}&hash=${fileObj.hash}&upTime=${fileObj.upTime}` }).then(data => { fileObj.finishCnt += 0.8; // console.log("合并完成 ---- ", data); // 清除临时缓存 URL.revokeObjectURL(fileObj.prevSrc); fileObj.prevSrc = fileObj.src = data.fileSrc; this.$emit('file-success', fileObj, this.fileList); }) } merge(); } else { fileObj.finishCnt++; } } }) .catch(err => { this.$emit('file-error', fileObj, this.fileList); // 回收错误请求 fileObj.errReqs.push(req); }) .finally(() => { // 剩余并发 this.reqCnt++; this.request(); }) } this.reqQueue.push(req); }); this.request(); }, ``` **优化:** - `filename+size+lastmodefied`作为文件标识,验证文件是否上传过,上传过直接返回路径 - 前端得知文件已存在则不再发起该文件切片上传请求 - 传入上传时间戳,防止同一个文件切片文件夹冲突 # 后端 ## 1. 切片接收接口 multiparty插件对post请求进行解析,获取切片相关参数,将切片放到以`文件名+hash(size+lastmodified)`为名字的文件夹下,切片名就是切片对应的索引,方便合并 ![切片未合并时的状况](https://gitee.com/aeipyuan/picture_bed/raw/master/picture_bed/images/20210112212841.png) ```javascript // 多文件切片上传 function multipleUpload(req, res) { new multiparty.Form().parse(req, (err, fields, file) => { if (err) { res.statusCode = 400; res.end(JSON.stringify({ msg: err })) } try { // 获取参数 let { upTime, hash, filename, suffix, index, total } = JSON.parse(fields.chunkInfo[0]); let chunk = file.data[0];// 切片数据 console.log(hash, upTime, filename, suffix, index, total) // 文件夹 let fullname = `${filename}-${hash}${suffix}`;// 文件名-hash.后缀 let filePath = path.resolve('./static/uploads', `${fullname}`); // 文件名-hash-上传时间戳(时间戳防止同一文件切片覆盖) let dirPath = path.resolve('./static/uploads', `${filename}-${hash}-${upTime}`); // 不存在该文件的情况下才会处理切片 if (!fs.existsSync(filePath)) {// 文件名-hash(size+lastmodified)相同视为同一文件 // 创建文件夹 fs.existsSync(dirPath) || fs.mkdirSync(dirPath); // 创建读写流 let rs = fs.createReadStream(chunk.path); //'C:\\Users\\14329\\AppData\\Local\\Temp\\-mpvktFV0iyjEfWvJz6Q1h_x' let ws = fs.createWriteStream(path.resolve(dirPath, index + '')); // 0.jpg rs.pipe(ws); rs.on('end', function () { // 上传成功 res.end(JSON.stringify({ msg: '切片' + index + '上传成功' })); }); } else { rmDir(dirPath);// 检查是否有多余切片并删除 res.end(JSON.stringify({ msg: '文件已存在', isFinished: true, imgSrc: `http://${CONFIG.uploadIP}:${CONFIG.port}/${CONFIG.uploadDir}/${fullname}` })); } } catch (err) { console.log(err) res.statusCode = 500; res.end(JSON.stringify({ msg: err })) } }) } ``` ## 2. 切片合并接口 前端组件验证切片完成后会自动发起merge请求,后端接到请求后根据文件名读取文件夹,按照切片索引顺序进行合并,合并一个切片同时删除一个,合并完成后删除文件夹 ```javascript // 切片合并 function mergeUpload(req, res) { let data = ''; req.on('data', chunk => data += chunk); req.on('end', () => { try { let { filename, hash, suffix, upTime } = qs.parse(data); // console.log(filename, hash, suffix, upTime); let dirname = `${filename}-${hash}-${upTime}`; let fullname = `${filename}-${hash}${suffix}`; // 文件夹路径 let dirPath = path.resolve(CONFIG.uploadDir, dirname); let filePath = path.resolve(CONFIG.uploadDir, fullname); if (fs.existsSync(filePath)) { rmDir(dirPath);// 检查是否有多余切片并删除 res.end(JSON.stringify({ msg: '文件已存在', isFinished: true, filename, fileSrc: `http://${CONFIG.uploadIP}:${CONFIG.port}/${CONFIG.uploadDir}/${fullname}` })) } else if (fs.existsSync(dirPath)) { let chunks = fs.readdirSync(dirPath), total = chunks.length; // 排序 chunks.sort((a, b) => a - b); // 合并 let to = fs.createWriteStream(filePath, { flags: 'w+' }); const mergeChunk = function (chunkIdx) { let chunkPath = path.resolve(dirPath, chunkIdx + ''); // console.log(filePath, chunkIdx, chunkPath) console.log(dirname + "--合并切片--" + chunkIdx); let from = fs.createReadStream(chunkPath); from.pipe(to, { end: false }); from.on('end', () => { fs.unlinkSync(chunkPath); if (chunkIdx + 1 < total) { mergeChunk(chunkIdx + 1); } else { console.log(dirname + " -- 合并完成"); fs.rmdirSync(dirPath); res.end(JSON.stringify({ msg: '合并成功', filename, fileSrc: `http://${CONFIG.uploadIP}:${CONFIG.port}/${CONFIG.uploadDir}/${fullname}` })) } }); } mergeChunk(0); } } catch (err) { console.log(err) res.statusCode = 400; res.end(JSON.stringify({ msg: err })) } }) } ```