# aeip_upload **Repository Path**: learnintap/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**: 0 - **Forks**: 1 - **Created**: 2020-12-21 - **Last Updated**: 2020-12-21 ## 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处理后面的请求 ```javascript // 上传切片 uploadChunks(fileObj) { // console.log(fileObj) let { file } = fileObj; fileObj.chunks.map((chunk, index) => { // 生成formdata let fd = $utils.getFd(fileObj, chunk, index); // 添加请求 const req = () => { let params = { url: this.url, data: fd, path: '/multi' }; $utils.ajax(params) .then(data => { // 所有切片上传完毕 if (fileObj.finishCnt + 1 == fileObj.chunks.length) { fileObj.finishCnt += 0.2; console.log(fileObj.name + '----所有切片上传完毕'); // 合并切片 $utils.ajax({ url: this.url, path: '/merge', data: `filename=${fileObj.name}` }).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); }) } 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(); }, // 发请求 request() { // 还有剩余并发数就取出队列元素 while (this.reqCnt > 0 && this.reqQueue.length > 0) { this.reqCnt--; this.reqQueue.shift()(); } }, ``` # 后端 ## 1. 切片接收接口 multiparty插件对post请求进行解析,获取切片相关参数,将切片放到以文件名为名字的文件夹下,切片名就是切片对应的索引,方便合并 ![切片未合并时的状况](https://gitee.com/aeipyuan/picture_bed/raw/master/picture_bed/images/20201122091205.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 filename = fields.filename[0];// 文件名 let suffix = fields.suffix[0];// 后缀 let index = fields.index[0];// 切片索引 let total = fields.total[0];// 总的切片数量 let chunk = file.data[0];// 切片数据 console.log(filename, suffix, index, total) // 文件夹 let dirPath = path.resolve('./static/uploads', filename); // 不存在文件的情况下才会处理切片 if (!fs.existsSync(dirPath + suffix)) { // 创建文件夹 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 { res.end(JSON.stringify({ msg: '文件已存在', imgSrc: `http://${CONFIG.uploadIP}:${CONFIG.port}/${CONFIG.uploadDir}/${filename}` })); } } 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 } = qs.parse(data);// 文件名 let lastIndex = filename.lastIndexOf('.'); let suffix = filename.slice(lastIndex), // 后缀 dirname = filename.slice(0, lastIndex);// 切片所在文件夹名字 console.log(filename, dirname, suffix) // 文件夹路径 let dirPath = path.resolve(CONFIG.uploadDir, dirname); let filePath = path.resolve(CONFIG.uploadDir, filename); if (fs.existsSync(filePath)) { res.end(JSON.stringify({ msg: '文件已存在', filename, fileSrc: `http://${CONFIG.uploadIP}:${CONFIG.port}/${CONFIG.uploadDir}/${filename}` })) } 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}/${filename}` })) } }); } mergeChunk(0); } } catch (err) { res.statusCode = 400; res.end(JSON.stringify({ msg: err })) } }) } ```