基于resumable.js和springboot上传大文件显示进度条实例

时间:2025-05-14 15:48:55 类型:JS/JQUERY
字号:    

基于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;
}


实际效果:

1.png2.png

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>

3.png

<