Upload组件基本实现
仓库:https://gitee.com/aeipyuan/upload_component
前端
1. 组件结构
<template>
<div class="uploadWrap">
<div class="upload">
<input type="file" class="fileUp" @change="uploadChange" multiple :accept="acceptTypes">
<button>点击上传
</button>
</div>
<span class="tips">
只能上传小于{{maxSize}}M的
<span v-for="type in fileTypes" :key="type">
{{type}}
</span>
格式图片,自动过滤
</span>
<transition-group appear tag="ul">
<li class="imgWrap" v-for="item in fileList" :key="item.src">
<div class="left">
<img :src="item.src" @load="revokeSrc(item.src)">
</div>
<div class="right">
<span class="name">{{item.name}}
</span>
<span class="num">
<span>{{item.progress}} %
</span>
<span class="continue" v-if="item.isFail" @click="continueUpload(item)">重试
</span>
</span>
<div class="bar" :style="`width:${item.progress}%`"></div>
</div>
<span class="cancle" @click="removeImg(item)">×
</span>
<span v-if="item.isFinished||item.isFail" :class="['flag',item.isFail?'redBd':(item.isFinished?'greenBd':'')]">
<span>{{item.isFail?'✗':(item.isFinished?'✓':'')}}
</span>
</span>
</li>
</transition-group>
</div>
</template>
2. 响应式数据
data() {
return {
fileList
: [],
maxLen
: 6,
finishCnt
: 0
}
}
3. 父子传值
父组件可以通过属性传值设置上传的url,文件大小,文件类型限制,并且可监听上传输入改变和上传完成事件获取文件列表信息
<Upload
:uploadUrl
="`http://127.0.0.1:4000/multi`"
:maxSize
="5"
:reqCnt
="6"
:fileTypes
="['gif','jpeg','png']"
@fileListChange
="upChange"
@finishUpload
="finishAll" />
props
: {
maxSize
: {
type
: Number
,
default: 2
},
fileTypes
: {
type
: Array
,
default: () => ['img', 'png', 'jpeg']
},
uploadUrl
: {
type
: String
,
default: 'http://127.0.0.1:4000/multi'
},
reqCnt
: {
default: 4,
validator
: val
=> {
return val
> 0 && val
<= 6;
}
}
}
4. 所有upload组件公用的属性和方法
let cbList
= [], map
= new WeakMap;
function filterFiles(files
, fileTypes
, maxSize
) {
return files
.filter(file
=> {
let index
= file
.name
.lastIndexOf('.');
let ext
= file
.name
.slice(index
+ 1).toLowerCase();
if (['jfif', 'pjpeg', 'jepg', 'pjp', 'jpg'].includes(ext
))
ext
= 'jpeg';
if (fileTypes
.includes(ext
) && file
.size
<= maxSize
* 1024 * 1024) {
return true;
} else {
return false;
}
})
}
function formatName(filename
) {
let lastIndex
= filename
.lastIndexOf('.');
let suffix
= filename
.slice(0, lastIndex
);
let fileName
= suffix
+ new Date().getTime() + filename
.slice(lastIndex
);
return fileName
;
}
function Ajax(options
) {
options
= Object
.assign({
url
: 'http://127.0.0.1:4000',
method
: 'POST',
progress
: Function
.prototype
}, options
);
return new Promise((resolve
, reject
) => {
let xhr
= new XMLHttpRequest;
xhr
.upload
.onprogress = e
=> {
options
.progress(e
, xhr
);
}
xhr
.open(options
.method
, options
.url
);
xhr
.send(options
.data
);
xhr
.onreadystatechange = () => {
if (xhr
.readyState
=== 4) {
if (/^(2|3)\d{2}$/.test(xhr
.status
)) {
resolve(JSON.parse(xhr
.responseText
));
} else {
reject({ msg
: "请求已中断" });
}
}
}
})
}
5. input标签change事件
根据父组件传入的规则对选中的文件进行过滤遍历过滤后的数组,生成监听的数组(直接监听原数组浪费性能)设置属性监听各种操作将请求函数存入队列,延迟执行调用request,若有剩余并发量则发起请求
uploadChange(e
) {
let files
= filterFiles([...e
.target
.files
], this.fileTypes
, this.maxSize
);
this.fileList
= this.fileList
.concat(files
.map((file
, index
) => {
let newFile
= {};
newFile
.name
= formatName(file
.name
);
newFile
.src
= window
.URL.createObjectURL(file
);
newFile
.progress
= 0;
newFile
.abort
= false;
newFile
.imgSrc
= "";
newFile
.isFinished
= false;
newFile
.isFail
= false;
newFile
.start
= 0;
newFile
.total
= file
.size
;
cbList
.push(() => this.handleUpload(file
, newFile
));
this.request();
return newFile
;
}));
},
6. request函数
request函数用于实现请求并发
request() {
while (this.maxLen
> 0 && cbList
.length
) {
let cb
= cbList
.shift();
this.maxLen
--;
cb();
}
}
7. handleUpload函数
handleUpload函数用于文件切片,发起Ajax请求,触发各种请求处理事件等功能
handleUpload(file
, newFile
) {
let chunkSize
= 1 * 2048 * 1024;
let fd
= new FormData();
let start
= newFile
.start
;
let total
= newFile
.total
;
let end
= (start
+ chunkSize
) > total
?
total
: (newFile
.start
+ chunkSize
);
let fileName
= newFile
.name
;
fd
.append('chunk', file
.slice(start
, end
));
fd
.append('fileInfo', JSON.stringify({
fileName
, start
}));
return Ajax({
url
: this.uploadUrl
,
data
: fd
,
progress
: (e
, xhr
) => {
let proNum
= Math
.floor((newFile
.start
+ e
.loaded
) / newFile
.total
* 100);
newFile
.progress
= Math
.min(proNum
, 95);
if (newFile
.abort
) {
xhr
.abort();
}
}
}).then(res
=> {
if (end
>= total
) {
newFile
.progress
= 100;
newFile
.imgSrc
= res
.imgSrc
;
newFile
.isFinished
= true;
this.finishCnt
++;
this.fileListChange();
} else {
newFile
.start
= end
+ 1;
cbList
.push(() => this.handleUpload(file
, newFile
));
}
}, err
=> {
newFile
.isFail
= true;
map
.set(newFile
, file
);
}).finally(() => {
this.maxLen
++;
this.request();
});
}
8. 清理图片缓存
window.URL.createObjectURL(file)创建的src对应图片加载完毕以后需要移除缓存
revokeSrc(url
) {
window
.URL.revokeObjectURL(url
);
}
9. 取消上传
removeImg(item
) {
item
.abort
= true;
let index
= this.fileList
.indexOf(item
);
if (index
!== -1) {
this.fileList
.splice(index
, 1);
this.fileListChange();
}
}
10. 重试
遇到断网等特殊情况请求处理失败后可通过点击重试重新发起请求
continueUpload(newFile
) {
newFile
.isFail
= false;
let file
= map
.get(newFile
);
cbList
.push(() => this.handleUpload(file
, newFile
));
this.request();
}
后端
1. 路由处理
const app
= require('http').createServer();
const fs
= require('fs');
const CONFIG = require('./config');
const controller
= require('./controller');
const path
= require('path');
app
.on('request', (req
, res
) => {
res
.setHeader('Access-Control-Allow-Origin', '*');
res
.setHeader('Access-Control-Allow-Methods', '*');
let { method
, url
} = req
;
console
.log(method
, url
)
method
= method
.toLowerCase();
if (method
=== "post") {
if (url
=== '/multi') {
controller
.multicleUpload(req
, res
);
}
}
else if (method
=== 'options') {
res
.end();
}
else if (method
=== 'get') {
if (url
.startsWith('/static/')) {
fs
.readFile(path
.join(__dirname
, url
), (err
, data
) => {
if (err
)
return res
.end(JSON.stringify({ msg
: err
}));
res
.end(data
);
})
}
}
})
app
.listen(CONFIG.port
, CONFIG.host
, () => {
console
.log(`Server start at ${CONFIG.host}:${CONFIG.port}`);
})
2. 文件解析和写入
function multicleUpload(req
, res
) {
new multiparty.Form().parse(req
, (err
, fields
, file
) => {
if (err
) {
res
.statusCode
= 400;
res
.end(JSON.stringify({
msg
: err
}))
}
try {
let { fileName
, start
} = JSON.parse(fields
.fileInfo
[0]);
let chunk
= file
.chunk
[0];
let end
= start
+ chunk
.size
;
let filePath
= path
.resolve(__dirname
, CONFIG.uploadDir
, fileName
);
console
.log(start
, end
);
let ws
;
let rs
= fs
.createReadStream(chunk
.path
);
if (start
== 0)
ws
= fs
.createWriteStream(filePath
, { flags
: 'w' });
else
ws
= fs
.createWriteStream(filePath
, { flags
: 'r+', start
});
rs
.pipe(ws
);
rs
.on('end', () => {
res
.end(JSON.stringify({
msg
: '上传成功',
imgSrc
: `http://${CONFIG.uploadIP}:${CONFIG.port}/${CONFIG.uploadDir}/${fileName}`
}))
})
} catch (err) {
res
.end(JSON.stringify({
msg
: err
}))
}
})
}