背景
在Ruoyi框架中,虽然也提供了基于fileinput的文件上传示例,加入企业在真实业务中有大文件的上传,比如上GB的文件,那使用fileinput的用户体验不怎么友好,因而在大容量文件上传处理时,就有必要进行切片,断点续传,重复文件判断等。因此本文将使用百度开源的WebUploader上传组件,对文件上传业务提供统一的封装和扩展,可以满足所有业务场景的覆盖。
本文将重点说明ruoyi使用的基础技术,简单介绍webuploader,webuploader如何在Ruoyi中进行集成。Ruoyi的示例例子采用的是Ruoyi的单体集成框架,不是前后端分离版,不过技术的思路是类似的,可以作为参考。
一、Ruoyi的实现
1、ruoyi的前端实现
ruoyi的前端是依赖于fileinput来实现的,其官方的文档手册地址可以参见:bootstrap-fileinput
实现的效果大致是这样的:
2、Ruoyi后端实现
Ruoyi使用了最简单的文件接收方式,没有文件切片,这样设计的目的,个人猜测是因为不考虑大文件的这种场景,当然在互联网里,确实遇到大文件的情况也不多,使用这样的方案也可以应对。Ruoyi的后台处理类代码如下:
packagecom.hngtghy.project.common; importjava.util.ArrayList; importjava.util.List; importjavax.servlet.http.HttpServletRequest; importjavax.servlet.http.HttpServletResponse; importorg.slf4j.Logger; importorg.slf4j.LoggerFactory; importorg.springframework.beans.factory.annotation.Autowired; importorg.springframework.http.MediaType; importorg.springframework.stereotype.Controller; importorg.springframework.web.bind.annotation.GetMapping; importorg.springframework.web.bind.annotation.PostMapping; importorg.springframework.web.bind.annotation.RequestMapping; importorg.springframework.web.bind.annotation.ResponseBody; importorg.springframework.web.multipart.MultipartFile; importcom.hngtghy.common.constant.Constants; importcom.hngtghy.common.utils.StringUtils; importcom.hngtghy.common.utils.file.FileUploadUtils; importcom.hngtghy.common.utils.file.FileUtils; importcom.hngtghy.framework.config.HngtghyConfig; importcom.hngtghy.framework.config.ServerConfig; importcom.hngtghy.framework.web.domain.AjaxResult; /*** 通用请求处理* * @author wuzuhu*/"/common") (publicclassCommonController{ privatestaticfinalLoggerlog=LoggerFactory.getLogger(CommonController.class); privateServerConfigserverConfig; privatestaticfinalStringFILE_DELIMETER=","; /*** 通用上传请求(单个)*/"/upload") (publicAjaxResultuploadFile(MultipartFilefile) throwsException { try { // 上传文件路径StringfilePath=HngtghyConfig.getUploadPath(); // 上传并返回新文件名称StringfileName=FileUploadUtils.upload(filePath, file); Stringurl=serverConfig.getUrl() +fileName; AjaxResultajax=AjaxResult.success(); ajax.put("url", url); ajax.put("fileName", fileName); ajax.put("newFileName", FileUtils.getName(fileName)); ajax.put("originalFilename", file.getOriginalFilename()); returnajax; } catch (Exceptione) { returnAjaxResult.error(e.getMessage()); } } /*** 通用上传请求(多个)*/"/uploads") (publicAjaxResultuploadFiles(List<MultipartFile>files) throwsException { try { // 上传文件路径StringfilePath=HngtghyConfig.getUploadPath(); List<String>urls=newArrayList<String>(); List<String>fileNames=newArrayList<String>(); List<String>newFileNames=newArrayList<String>(); List<String>originalFilenames=newArrayList<String>(); for (MultipartFilefile : files) { // 上传并返回新文件名称StringfileName=FileUploadUtils.upload(filePath, file); Stringurl=serverConfig.getUrl() +fileName; urls.add(url); fileNames.add(fileName); newFileNames.add(FileUtils.getName(fileName)); originalFilenames.add(file.getOriginalFilename()); } AjaxResultajax=AjaxResult.success(); ajax.put("urls", StringUtils.join(urls, FILE_DELIMETER)); ajax.put("fileNames", StringUtils.join(fileNames, FILE_DELIMETER)); ajax.put("newFileNames", StringUtils.join(newFileNames, FILE_DELIMETER)); ajax.put("originalFilenames", StringUtils.join(originalFilenames, FILE_DELIMETER)); returnajax; } catch (Exceptione) { returnAjaxResult.error(e.getMessage()); } } }
二、基于WebUploader的切片处理
1、关于webuploader
正是由于Ruoyi天生的大文件处理能力比较差的,经过对开源组件的比较,我们选定了百度开源的百度Webuploader,它具有以下的能力:
2、Webuploader集成
在官网上下载最新的webuploader资源包后,将相应的资源文件拷贝到Ruoyi的工程目录中,
3、在上传页面中引用webuploader.js
在这里,我们设计了统一的文件存储服务,因此,将文件的查询、上传、编辑、删除功能都封装在一个界面中,对外提供单个文件上传功能,也提供批量管理功能。所以,有必要对文件进行统一封装。下面是基于Thymeleaf的一个简单封装:
<htmllang="zh"xmlns:th="http://www.thymeleaf.org"><headth:include="include :: webupload"><bodyclass="no-skin"><divclass="main-container ace-save-state"id="main-container"><scripttype="text/javascript">try{ace.settings.loadState('main-container')}catch(e){} </script><divclass="main-content"><divclass="main-content-inner"><divclass="page-content"style="padding: 8px 10px 0px;"><divclass="widget-box"><div><formclass="form-search"><divclass="row"><divclass="col-xs-12 col-sm-12"><tableclass="table"style="margin-bottom: 0px;"><tbody><tr><tdstyle="border: 0px;vertical-align: middle;width:10%;"><pclass="text-right">文件名称</p></td><tdstyle="border: 0px;vertical-align: middle;width:50%;"><inputtype="text"name="name"class="form-control"placeholder="请输入文件名称"/></td><tdstyle="border: 0px;vertical-align: middle;width:30%;"><buttontype="button"class="btn btn-primary btn-xs"id="btn-search"><spanclass="fa fa-search "></span> 查询 </button><ahref="#"class="btn btn-success btn-xs filepicker_btn"th:id="'filePicker_'+${temp_b_id}"><iclass="ace-icon fa fa-upload"></i> 选择 </a><divth:include="include-upload-js :: header"></div></td></tr></tbody></table></div></div></form></div></div><divclass="table-responsive"><tableid="dataTable"lay-filter="dataTable"cellspacing="0"></table></div></div></div></div></div><scriptth:inline="javascript">varprefix= [[@{/uploadfile}]];varfileUploadIndex=0; $(document).ready(function() { $("#btn-search").on("click",doSearch); initTable(); }); functiondoSearch(){ table.reload('dataTable',{ where : { name :$("input[name='name']").val(), } }); } varuploadSuccessCallback=function(file,fileArray){ varallFinished=true; for(iinfileArray){ varobj=fileArray[i]; if(obj.status!='上传失败'&&obj.status!='上传成功'){ allFinished=false; break; } } if(allFinished){ doSearch(); parent.layer.close(fileUploadIndex); modal=null; } } functioninitTable(){ varbid= [[${bid}]]; vartemp_b_id= [[${temp_b_id}]]; varb_ids=bid==null?temp_b_id : bid; vartablename= [[${tablename}]]; varbizType= [[${bizType}]]; varmultipleMode= [[${multipleMode}]]; layui.use('table', function(){ table=layui.table; table.render({ elem: '#dataTable', height: "full", url: prefix+"/list?b_id="+b_ids, method : "post", page: true, //toolbar:"#toolbar",defaultToolbar:[], where:{orderByColumn:'createTime',isAsc:'desc',tablename:tablename,bizType:bizType}, done: function(res, curr, count){ if(multipleMode=="single"){//单选模式下需要进行设置 add by wuzuhu on 2022-07-18if(count>0){ $(".filepicker_btn").hide(); }else{ $(".filepicker_btn").show(); } } }, cols: [[//表头 {type: 'checkbox',fixed: 'left'}, {field: 'name', title: '文件名',sort: true, fixed: 'left'}, {field: 'createTime', title: '创建时间',sort: true,width:170,align: 'center'}, {field: 'size', title: '文件大小',sort: true,width:110,align: 'center',templet: function(data){ returnWebUploader.Base.formatSize(data.size); }}, {field:'title', title: '操作',width:120,templet: function(data){ varactions= []; actions.push('<a class="btn btn-purple btn-xs" href="#" onclick="downloadFile(\''+data.fid+'\')"><i class="fa fa-download">下载</i></a> '); actions.push('<a class="btn btn-danger btn-xs" href="#" onclick="deleteFile(\''+data.fid+'\')"><i class="fa fa-remove">删除</i></a> '); returnactions.join(''); } } ]] }); //监听工具条table.on('toolbar(dataTable)', function(obj){ varlayEvent=obj.event; if(layEvent=='create'){ } if(layEvent=="del"){ } }); //监听排序事件table.on('sort(dataTable)',function(obj){ table.reload('dataTable',{ initSort: obj, where:{orderByColumn:obj.field,isAsc:obj.type} }); }); }); } functiondeleteFile(fid){ $.ajax({ type:"POST", url:[[@{/uploadfile/deleteByFid}]], data:{ fid : fid, }, dataType:"json", success:function(response){ doSearch(); parent.layer.msg("操作成功",{time:1500,icon:6}); }, error:function(){ } }); } functiondownloadFile(fid){ window.location.href=[[@{/uploadfile/download}]]+"?fid="+fid; } </script></body></html>
4、Webuploader功能定制
这里我们放在百度网盘的样子对WebUpload的样式进行改造,同时需要将文件上传的列表展示出来,同时可以对文件进行上传、暂停、删除等操作,因此需要对webuploader进行定制化开发。相关代码如下:
functioninitUploader(){ bindFileListeners(); varfileNumLimit= [[${fileNumLimit}]];//文件数量限制varacceptType= [[${acceptType}]];//支持文件类型varauto= [[${autoUpload}]];//是否自动上传0否1是varmultipleMode= [[${multipleMode}]];//多选模式 add by wuzuhu on 2022-07-18uploader=WebUploader.create({ auto: auto==0?false : true, swf: [[@{/uploader/Uploader.swf}]], server: [[@{/uploadfile/bigUploader}]], pick: {id:'#filePicker_'+[[${temp_b_id}]],multiple: multipleMode=="single"?false : true}, dnd: '#filePicker_'+[[${temp_b_id}]], method:'POST', resize: false , chunked : true, chunkRetry:false, formData : { fid : '', name : '', size : 0, md5code : '', tablename : tablename, temp_b_id : temp_b_id, bizType : [[${bizType}]], bid : b_id }, compress : false, duplicate:true, prepareNextFile: true, disableGlobalDnd:true, }); uploader.on('beforeFileQueued', function(file) { if(file.size==0){ varerror="文件不能为空!"; parent.layer.msg(error); returnfalse; } if (fileNumLimit!=null&&fileNumLimit<=fileArray.length) { message.info("文件数量不能超过"+fileNumLimit); returnfalse; } varfile_name=file.name; varfile_type=file_name.substring(file_name.lastIndexOf(".") +1); if (acceptType!=null&&acceptType.length!==0) { if (acceptType.indexOf(file_type) ==-1) { message.info("文件类型只能是"+acceptType.toString()); returnfalse; } } returntrue; }); uploader.on('fileQueued', function(file) { /* for(i in fileArray){ var obj = fileArray[i]; if(obj.name == file.name){ if(obj.status == '上传失败'){ modal.removeFile(obj.f_id); } } } var uuid = WebUploader.Base.guid(''); var file_upload = new FileObj(file.id,uuid,file.name,file.size,'','等待上传','',''); fileArray.push(file_upload); openProcessModalFile(file); */ }); uploader.on('filesQueued', function(files) { for(jinfiles){ varfile=files[j]; for(iinfileArray){ varobj=fileArray[i]; if(obj.name==file.name){ if(obj.status=='上传失败'&&modal!=null){ modal.removeFile(obj.f_id); } } } varuuid=WebUploader.Base.guid(''); varfile_upload=newFileObj(file.id,uuid,file.name,file.size,'','等待上传','',''); fileArray.push(file_upload); } openProcessModalFiles(files); }); uploader.on( 'uploadProgress', function( file, percentage ) { varobj=getFileObjById(file.id); if(obj.status=='暂停'){ return; } if (obj.id===file.id) { if (percentage===1) { if (obj.status==='99.99%') { return; } if (file.size>block_size) { obj.status='99.99%'; if(modal){ modal.updateStatus(obj.f_id,'99.99%') } } } else { percentage= (percentage*100).toFixed(2); if (percentage+"%"===obj.status) { return; } obj.status=percentage+"%"; if(modal){ modal.updateStatus(obj.f_id,percentage+"%") } } } }); uploader.on( 'uploadBeforeSend', function( block,data,headers ) { varobj=getFileObjById(block.file.id); data.md5code=obj.md5code; data.fid=obj.f_id; data.name=obj.f_name; data.size=obj.f_size; data.chunk=block.chunk; data.chunkSize=block.end-block.start; }); } functionaddFiles(files){ for(iinfiles){ varfile=files[i]; addFile(file); } }
由于篇幅有限,这里不把所有的代码都列出来,仅将部分代码列出来。
三、WebUploader与Ruoyi集成总结
这里讲解了Webuploader与Ruoyi的简单集成,这是第一个部分,如果需要详细了解的,可以深入交流,这里有涉及数据分片的具体实现,还有后端的服务端支持等等,关于后端的设计和业务表的设计,打算在后续再进行说明。
Webuploader与Ruoyi的集成效果图如下图所示:
通过观察网络请求可以看到,前端往服务端提交数据时,数据是已经进行了分片: