1.0.5 • Published 3 years ago

aeip_upload v1.0.5

Weekly downloads
-
License
ISC
Repository
-
Last release
3 years ago

前端

简介:一个基于Vue的实现并发数量控制,切片上传的Upload组件

1.组件注册

/* 拉取npm包 */
npm i aeip_uplpoad  

/* 全局注册 */
import AeipUpload from 'aeip_upload'
Vue.use(AeipUpload)

/* 局部注册 */
import { AeipUpload } from 'aeip_upload'
export default {
	components: {
		AeipUpload
    },
    //...
}

2.配置项

<aeip-upload
    url="http://127.0.0.1:4000"
    :limit="limit"
    :maxSize="maxSize"
    :tip="'自定义tip'"
    multiple
    :fileTypes="['image/jpeg']"
    @file-success="success"
    @file-error="error"
    @file-remove="remove"
    @file-click="clickFile">
</aeip-upload>
  • 参数说明
变量名含义
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处理后面的请求

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, // 完成标志
}
// 上传切片
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)为名字的文件夹下,切片名就是切片对应的索引,方便合并

切片未合并时的状况

// 多文件切片上传
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请求,后端接到请求后根据文件名读取文件夹,按照切片索引顺序进行合并,合并一个切片同时删除一个,合并完成后删除文件夹

// 切片合并
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
            }))
        }
    })
}
1.0.5

3 years ago

1.0.4

3 years ago

1.0.3

3 years ago

1.0.2

3 years ago

1.0.1

4 years ago

1.0.0

4 years ago