基于resumable.js的切片上传大文件,且显示进度条(基于layui的进度条), 并结合后台使用springboot接收切片,合并切片的完整实例
1, 下载resumable.js 或使用 cdn引入js
可以到https://www.bootcdn.cn/resumable.js/ 下载
2, 前端代码 单个文件上传显示
<form class="layui-form" action="" lay-filter="addForm"> <div class="layui-form-item"> <label class="layui-form-label">文件上传</label> <div class="layui-input-block"> <div class="layui-btn" id="uploadBigFile"> <i class="layui-icon layui-icon-upload"></i> 选择文件 </div> <div class="layui-progress layui-hide" id="demo0" style="margin-top: 20px" lay-showPercent="true" lay-filter="file-upload-progress"> <div class="layui-progress-bar" lay-percent="0%"></div> </div> </div> </div> </form> <script src="layui.js"></script> <script src="resumable.js"></script> <script> layui.config({ base: '/vendor/layuiAdmin/res/' // 静态资源所在路径 }).use(function(){ let $ = layui.$ ,layer = layui.layer ,element = layui.element; //基于Resumable.js const r = new Resumable({ target: '/uploadFile', // 后端分片上传接口 chunkSize: 1 * 1024 * 1024, // 分片大小(默认 1MB) simultaneousUploads: 3, // 并发请求数 testChunks: false, // 不进行测试分块完整性检查,以提高性能 generateUniqueIdentifier: function(file) { // 提供自定义的文件唯一标识符生成方法 return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { var r = (Math.random() * 16) | 0, v = c == 'x' ? r : (r & 0x3) | 0x8; return v.toString(16); }); } }); r.assignBrowse(document.getElementById('uploadBigFile')); // 文件添加到队列时触发 r.on('fileAdded', function(file){ console.log('File added', file); $("#demo0").removeClass('layui-hide') r.upload() }); // 文件上传开始时触发 r.on('fileProgress', function(file){ // 渲染进度条组件 element.progress('file-upload-progress', Math.floor(file.progress() * 100) + '%'); // 设置 50% 的进度 //console.log('File progress', file.progress()); }); // 文件上传成功时触发 r.on('fileSuccess', function(file, message){ // console.log('File uploaded successfully', file, message); let obj = JSON.parse(message) console.log("新的文件名:" + obj.data) }); // 文件上传失败时触发 r.on('fileError', function(file, message){ console.log('File upload error', file, message); }); }); </script>
3, 后台springboot控制器代码
@RestController public class AdmUploadController { //配置的上传目录 @Value("${web.uploadPath}") private String uploadPath; //接收分片文件上传 @PostMapping("uploadFile") public R uploadFile( @RequestParam("resumableChunkNumber") int chunkNumber, @RequestParam("resumableTotalChunks") int totalChunks, @RequestParam("file") MultipartFile chunk, HttpServletRequest request) throws IOException { //表单body和url都会传递以下两个参数,参数接收会拼接 String fileName = request.getParameter("resumableFilename"); String identifier = request.getParameter("resumableIdentifier"); // 目标文件路径,identifier是唯一的标识符,用于存储文件分片 String targetPath = uploadPath + identifier; File uploads = new File(targetPath); //创建临时文件夹 if (!uploads.exists()) {uploads.mkdirs();} System.out.println(targetPath+"/"+fileName + "." + chunkNumber); Path chunkPath = Paths.get(targetPath,fileName + "." + chunkNumber); // 分片文件路径 Files.copy(chunk.getInputStream(), chunkPath, StandardCopyOption.REPLACE_EXISTING); // 保存分片到服务器 // 检查是否所有分片都已上传完毕,如果是,则合并分片文件到最终文件 if (chunkNumber == totalChunks) { String newName = mergeChunks(targetPath, identifier, totalChunks,fileName); // 合并分片的方法实现见下文 return R.builder().msg("All upload success").code(200).data(newName).build(); // 所有分片上传完毕,返回成功消息或进行其他处理 } else { return R.builder().msg("chunk upload success").code(200).build(); // 分片上传成功,返回成功消息或进行其他处理 } } private String mergeChunks(String targetPath, String identifier, int totalChunks,String fileName) throws IOException { String suffix = fileName.substring(fileName.lastIndexOf(".")); String newName = UUID.randomUUID().toString().replace('-','_') + suffix; File mergedFile = new File(uploadPath + newName); // 最终存储路径并生成新的文件名 try (FileOutputStream fos = new FileOutputStream(mergedFile, true)) { // 合并所有分片 for (int i = 1; i <= totalChunks; i++) { File chunk = new File(targetPath + "/" + fileName + "." + i); Files.copy(chunk.toPath(), fos); chunk.delete(); // 删除临时分片 } new File(targetPath).delete(); //删除临时存储切片的目录 System.out.println("合并成功"); } catch (IOException e) { System.out.println("合并失败"); } return newName; } //删除文件 @GetMapping("delFile") public R delFile(String fileName){ File file = new File(uploadPath + fileName); file.delete(); return R.builder().msg("del success").code(200).build(); } } @Data @Builder public class R { private String msg; private int code; private String data; }
实际效果:
3, 前端代码 多个文件上传显示
<form class="layui-form" action="" lay-filter="addForm"> <div class="layui-form-item"> <label class="layui-form-label">文件上传</label> <div class="layui-input-block"> <div class="layui-btn" id="uploadBigFile"> <i class="layui-icon layui-icon-upload"></i> 选择文件 </div> <table class="layui-table" id="file-list"> <thead><tr><th>文件名</th><th>文件大小</th><th>上传状态</th><th>操作</th></tr></thead> <tbody> </tbody> </table> <div class="layui-progress layui-hide" id="demo0" style="margin-top: 20px" lay-showPercent="true" lay-filter="file-upload-progress"> <div class="layui-progress-bar" lay-percent="0%"></div> </div> </div> </div> </form>
<script src="/vendor/layuiAdmin/res/layui/layui.js"></script> <script src="/vendor/resumable.js"></script> <script> layui.config({ base: '/vendor/layuiAdmin/res/' // 静态资源所在路径 }).use(function(){ let $ = layui.$ ,layer = layui.layer ,element = layui.element; //基于Resumable.js const r = new Resumable({ target: '/adm/uploadFile', // 后端分片上传接口 chunkSize: 2 * 1024 * 1024, // 分片大小(默认 1MB) simultaneousUploads: 3, // 并发请求数 testChunks: false, // 不进行测试分块完整性检查,以提高性能 generateUniqueIdentifier: function(file) { // 提供自定义的文件唯一标识符生成方法 return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { var r = (Math.random() * 16) | 0, v = c == 'x' ? r : (r & 0x3) | 0x8; return v.toString(16); }); } }); r.assignBrowse(document.getElementById('uploadBigFile')); // 文件添加到队列时触发 r.on('fileAdded', function(file){ console.log('File added', file); let strTr = `<tr> <td>${file.fileName}</td> <td>${getfilesize(file.file.size)}</td> <td> <div class="layui-progress" lay-showPercent="true" lay-filter="${file.file.uniqueIdentifier}"> <div class="layui-progress-bar" lay-percent="0%"></div> </div> </td> <td> <input type="hidden" name="files" data-original="${file.fileName}" data-size="${getfilesize(file.file.size)}" id="${file.file.uniqueIdentifier}"> <button type="button" class="layui-btn layui-bg-red layui-hide ${file.file.uniqueIdentifier} layui-btn-xs del_file">删除</button> </td> </tr>` $("#file-list > tbody").append(strTr) element.render('progress', file.file.uniqueIdentifier); r.upload() }); // 文件上传开始时触发 r.on('fileProgress', function(file){ // 渲染进度条组件 element.progress(file.file.uniqueIdentifier, Math.floor(file.progress() * 100) + '%'); element.render('progress', file.file.uniqueIdentifier); //console.log('File progress', file.progress()); }); // 文件上传成功时触发 r.on('fileSuccess', function(file, message){ // console.log('File uploaded successfully', file, message); let obj = JSON.parse(message) console.log("新的文件名:" + obj.data) $("."+file.file.uniqueIdentifier).removeClass('layui-hide').data('filename',obj.data) $("#"+file.file.uniqueIdentifier).val(obj.data) }); // 文件上传失败时触发 r.on('fileError', function(file, message){ console.log('File upload error', file, message); }); $(document).on("click",".del_file",function(){ console.log("ok") let _this = $(this) let fileName = $(this).data('filename') $.get("/adm/delFile",{fileName},function(res){ _this.parents('tr').remove(); }) return false; }) function getfilesize(size) { if (!size) return ""; var num = 1024.00; //byte if (size < num) return size + "B"; if (size < Math.pow(num, 2)) return (size / num).toFixed(2) + "K"; //kb if (size < Math.pow(num, 3)) return (size / Math.pow(num, 2)).toFixed(2) + "M"; //M if (size < Math.pow(num, 4)) return (size / Math.pow(num, 3)).toFixed(2) + "G"; //G return (size / Math.pow(num, 4)).toFixed(2) + "T"; //T } }); </script>