在React中使用WebUploader实现大文件分片上传的踩坑日记!
前段时间公司项目有个大文件分片上传的需求,项目是用React写的,大文件分片上传这个功能使用了WebUploader这个组件。
具体交互是:
1. 点击上传文件button后出现弹窗,弹窗内有选择文件和开始上传button。
2. 每个文件显示序号、文件名、进度条、上传操作按钮(开始/暂停、删除)。
3. 选择好文件之后点击开始上传,文件按照顺序自动从第一个开始上传。
4. 期间如果用户点了弹窗“X”关闭,则暂停任务,弹窗关闭。
5. 弹窗关闭之后重新点击上传文件button后将用户上次选择的未完成的文件展示出来,并可以继续上传。
6. 全部上传完成之后自动关闭弹窗。
开发过程中踩了不少坑,好在自己始终没有放弃,慢慢研究探索,终于是实现了需求,或许这就叫做匠人精神吧😂😂。。
下面来分享一下开发过程中遇到的坑(博主React菜鸟一枚,写的不好勿喷,望各路大神指点😌)
首先说一下实现以上交互需求的具体思路吧:
注册uploader,在uploader实例化之后,把uploader保存在state里,在上传过程中更新文件状态,当上传完成时再更新一下状态。
更新状态的目的是后面会根据这些文件的状态渲染按钮,“待开始”状态的渲染“开始”按钮,“上传中”状态的渲染“暂停”按钮,已完成渲染“成功”按钮,“异常”状态的渲染“错误”按钮。
部分代码如下:
//WebUploader hook
var chunkSize = 10 * 1024 * 1024;//分片上传,每片5M,默认是5M
var that = this; //保存this指针
WebUploader.Uploader.register({
name:'my-uploader',
'before-send-file': 'beforeSendFile',
'before-send': 'beforeSend'
}, {
beforeSendFile: function (file) {
// console.log("beforeSendFile");
// Deferred对象在钩子回掉函数中经常要用到,用来处理需要等待的异步操作。
var task = new $.Deferred();
// 根据文件内容来查询MD5
uploader.md5File(file,0,chunkSize).progress(function (percentage) {})
.then(function (val) { // md5计算完成
// console.log('md5 result:', val);
file.md5 = val;
file.uid = WebUploader.Base.guid();
// 进行md5判断
$.post("后端checkMd5的url", {uid: file.uid, md5: file.md5, fileName:file.name},
function (data) {
// console.log(data,'md5 res');
if(data.code=='500'){
message.error(data.msg)
let updateFileList = that.state.fileQueuedList; //更新文件状态,所有选择的文件保存在fileQueuedList中
let res = updateFileList.map(item=>{
if(item.fileId === file.id){
item.status = "ERROR";
item.statusName = "错误";
}
return item
})
that.setState({
fileQueuedList:res,
})
task.reject(); //遇到不符合要求的文件调用reject方法,可以上传后面正常的文件
}else{
var status = data.status.value;
task.resolve();
if (status == 101) {
// 文件不存在,那就正常流程
}else if (status == 100) {
// 文件存在 忽略上传过程,直接标识上传成功;
message.error(file.name+data.msg);
uploader.skipFile(file);
file.pass = true;
}else if (status == 102) {
// 部分已经上传到服务器了,但是差几个模块。
file.missChunks = data.data;
}
}
}
);
});
return $.when(task);
},
beforeSend: function (block) {
var task = new $.Deferred();
var file = block.file;
var missChunks = file.missChunks;
var blockChunk = block.chunk;
// console.log("当前分块:" + blockChunk);
// console.log("missChunks:" + missChunks);
if (missChunks !== null && missChunks !== undefined && missChunks !== '') {
var flag = true;
for (var i = 0; i < missChunks.length; i++) {
if (blockChunk == missChunks[i]) {
// console.log(file.name + ":" + blockChunk + ":还没上传,现在上传去吧。");
flag = false;
break;
}
}
if (flag) {
task.reject();
} else {
task.resolve();
}
} else {
task.resolve();
}
return $.when(task);
}
});
// 实例化
var uploader = WebUploader.create({
pick: {
id:'#picker',
multiple:true
},
formData: {
uid: 0,
md5: '',
chunkSize: chunkSize,
},
swf: '../webUploader/Uploader.swf', // swf文件路径
chunked: true, //是否要分片处理大文件上传
chunkSize: chunkSize,
threads: 3, //上传并发数。允许同时最大上传进程数。
server: '/dynamic/video/fileUpload', // 文件接收服务端。
auto: false,
duplicate:false,
withCredentials:true,
// accept: {
// extensions: 'avi,asf,avs,mpg,mov,mp4,m4a,3gp,ogg,flv,ps,ts,dav,rmvb,SV4,SV5,SSDV',
// },
// 禁掉全局的拖拽功能。这样不会出现图片拖进页面的时候,把图片打开。
disableGlobalDnd: true,
// fileNumLimit: 1024, //验证文件总数量, 超出则不允许加入队列。
// fileSizeLimit: 1024 * 1024 * 1024, // 1G 验证文件总大小是否超出限制, 超出则不允许加入队列。
// fileSingleSizeLimit: 20*1024 * 1024 * 1024 // 20G 验证单个文件大小是否超出限制, 超出则不允许加入队列。
});
that.setState({ //把实例保存到state中
uploader:uploader
})
// 当有文件被添加进队列的时候
uploader.on('fileQueued', function (file) {
let appendFile = that.state.fileQueuedList;
let res = appendFile.some(item=>{
return item.file.name==file.name
})
if(res){
// message.error(file.name+'文件重复。')
return
}
appendFile.push({
file:file, //把file对象也保存下来
fileId:file.id,
progress:'0%',
status:'START',
statusName:'待开始',
})
that.setState({
fileQueuedList:appendFile,
})
});
//当某个文件的分块在发送前触发,主要用来询问是否要添加附带参数,大文件在开起分片上传的前提下此事件可能会触发多次。
uploader.onUploadBeforeSend = function (obj, data) {
// console.log("onUploadBeforeSend");
var file = obj.file;
data.md5 = file.md5 || '';
data.uid = file.uid;
};
// 上传中
uploader.on('uploadProgress', function (file, percentage) {
let updateFileList = that.state.fileQueuedList;
let res = updateFileList.map(item=>{ //文件上传中时更新文件状态和进度条
if(item.fileId === file.id){
item.progress=Math.floor(percentage * 100) + '%';
item.status = "UPLOADING";
item.statusName = "上传中";
}
return item
})
that.setState({
fileQueuedList:res,
})
// console.log(Math.floor(percentage * 100) + '%',file.name,'上传进度')
});
// 上传返回结果
uploader.on('uploadSuccess', function (file) {
// console.log('success')
let updateFileList = that.state.fileQueuedList;
let res = updateFileList.map(item=>{ //文件上传成功更新状态
if(item.fileId === file.id){
item.progress='100%';
item.status = "UPLOADED";
item.statusName = "已完成"
}
return item
})
//判断是不是都上传完,可以将该判断放在uploadComplete函数中,uploadSuccess只监听的到已成功的文件,uploadComplete函数无论成功失败都可以监听到
let isAllCompleted = updateFileList.every(item=>{
return item.status==="UPLOADED"||item.status==="ERROR"
})
that.setState({
fileQueuedList:res,
isAllCompleted:isAllCompleted
})
if(isAllCompleted){ //都上传成功之后
that.props.onClose&&that.props.onClose() //关闭弹窗
that.props.getFileList&&that.props.getFileList() //刷新文件table
}
});
uploader.on('error', function (type,file) {
// message.error("上传出错!请检查后重新上传!错误代码"+type);
// if(type=='F_DUPLICATE'){
// message.error(file.name+'文件重复')
// }
// if (type == "Q_TYPE_DENIED") {
// message.error("请上传视频格式文件");
// }else {
// message.error("上传出错!请检查后重新上传!错误代码"+type);
// }
});
}
//点击文件的"开始"Icon,obj为当前点击的文件对象,即currentItem in fileQueuedList
fileUpload(obj){
const {uploader,fileQueuedList} = this.state;
uploader.upload(obj.file)
let updateObj = fileQueuedList;
let idx = fileQueuedList.indexOf(obj);
updateObj[idx].status = "UPLOADING";
updateObj[idx].statusName = "上传中";
this.setState({fileQueuedList:updateObj})
}
//点击暂停Icon
fileStop(obj){
const {uploader,fileQueuedList} = this.state;
uploader.cancelFile(obj.file)
//此处为第一个坑,在API里暂停是调用stop方法,此处想要暂停指定文件,显然应该用stop(file)方法,
然而实践之后发现调用stop(file)方法会报错 “Cannot read property 'file' of undefined”,
之后再点击继续发现无法继续上传,没有发出请求。
后来经过各种尝试后采用了cancelFile方法,可以暂停并继续,但此方法会标记文件为已取消状态,可以再次手动选择添加进队列,从而不触发文件重复的error监听。
let idx = fileQueuedList.indexOf(obj);
let updateObj = fileQueuedList;
updateObj[idx].status = "PAUSE";
updateObj[idx].statusName = "已暂停";
this.setState({fileQueuedList:updateObj})
}
//文件暂停时点击继续开始Icon
fileContinue(obj){
const {uploader,fileQueuedList} = this.state;
uploader.retry(obj.file) //继续上传可以采用retry方法也可以使用upload方法
let idx = fileQueuedList.indexOf(obj);
let updateObj = fileQueuedList;
updateObj[idx].status = "UPLOADING";
updateObj[idx].statusName = "上传中";
this.setState({fileQueuedList:updateObj}) //更新文件状态
}
//点击文件删除Icon
clickDeleteIcon(obj){
let that = this;
const {uploader,fileQueuedList} = that.state;
let updateObj = fileQueuedList;
let idx = fileQueuedList.indexOf(obj);
updateObj.splice(idx,1)
uploader.cancelFile(obj.file);
that.setState({fileQueuedList:updateObj})
}
//点击开始上传按钮
startUpload(){
const{uploader,fileQueuedList} = this.state;
let PausedFile = fileQueuedList.filter(item=>{
return item.status==="PAUSE"
})
// console.log(PausedFile)
if(PausedFile&&PausedFile.length>0){ //如果有已暂停的文件则从已暂停的文件中第一个开始上传
uploader.upload(PausedFile[0].file)
}else{
uploader.upload()
}
}
//弹窗关闭
onClose(){
const {fileQueuedList,isAllCompleted,uploader} = this.state;
if(!isAllCompleted){
let res = fileQueuedList&&fileQueuedList.reduce((data,current)=>{ //把除了错误和上传完成的文件暂停
if(current.status!=='UPLOADED'||current.status!=='ERROR'){
current.status="PAUSE";
current.statusName="已暂停";
uploader.stop(true);
data.push(current)
}
return data
},[])
// console.log(res,'res')
this.props.saveFileStatus&&this.props.saveFileStatus(res) //把所有添加的文件状态保存下来传给父组件。再有父组件通过props传给子组件
}
this.props.onClose&&this.props.onClose()
this.props.getFileList()
}
componentDidMount(){
//挂载完成后获取父组件的props保存的文件状态
const {savedFileList} = that.props; //savedFileList保存了关闭弹窗后未上传完的任务列表
// console.log(savedFileList,'saved')
this.uploadOperate() //把WebUploader相关的代码统一写在了此函数中,挂载时调用,注册hook并生成WebUploader实例
if(savedFileList&&savedFileList.length>0){
this.setState({
fileQueuedList:savedFileList, //赋值,显示未完成的文件列表
},()=>{
const {uploader,fileQueuedList} = that.state;
let files = fileQueuedList.map(item=>{
return item.file
})
for(let i = 0; i < files.length;i++){
uploader.removeFile(files[i],true)
}
uploader.addFiles(files)
//遍历所有的未完成任务,移除任务后再重新添加,目的是这样会触发fileQueue事件,否则进来点继续上传只会触发uploadProgress函数,在这个函数里有setState方法,但是会报错“Warning: setState(...): Can only update a mounted or mounting component. This usually means you called setState() on an unmounted component.” 发现上传请求是正常进行的,但是页面进度条不渲染,这也是第二个坑点,博主当时也没有找到原因,因为componentDidMount函数已经触发了,uploader实例也生成了,为什么还是unmounted component呢?于是便各种尝试,最终衍生出了上述代码,解决了这个进度条不渲染的,需求到此也是都实现了。。。
})
}
}
}