1.0.5 • Published 3 years ago
aeip_upload v1.0.5
前端
简介:一个基于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
}))
}
})
}