js大文件切片上传,断点续传实现demo

发布时间 2023-10-11 12:03:37作者: Xproer-松鼠

思路

  1. 把大文件切成每块10M(按照你自己的要求),然后依次上传(为了让你好理解,你可以理解成分页,一共89条数据,每页数10条,一共9页)。
  2. 上传完成后端把文件封装好,返回上传的url地址
  3. 加上进度条
  4. 中途上传中断,如果再次上传如何回到原来的位置继续上传,解决办法就是每次上传前给后端请求接口查验下,该文件上传到第几片了,和后端对接好chunk参数,比如-1代表已经上传完,0代表第0片,以此类推。
  5. 完善各种情况:比如发生错误,是否可以尝试重新上传,离开页面以后取消上传操作(离开页面以后,上传还在继续,这种情况除了上传还有计时器的问题,这里就不多说了,总之离开之前先清掉的思路)

PS:一定要上传完一个才能上传完下一个,是串行不是并行,另外,ajax一定要是异步的不管是原生还是jq的ajax,因为我开始弄成同步,progress事件根本出不来

我做这个的最大感悟就是一点点做起来的一个完善的东西,都是在开发中不断完善的,一开始不会考虑到那么多情况,或者你先不要考虑那么多,先把基础做了,慢慢叠加功能。我是先把1和2做了以后才慢慢加上3、4、5,这是一个很自然的过程,如果你一上来都要实现,对我而言亚历山大。

后台需要提供三个接口:

  1. 上传文件的接口
  2. 上传之前查验文件是否上传过,上传到第几片的接口
  3. 上传文件完成merge的接口

完整代码一:vue版本

(vue+js+ajax+md5+element-ui)

<input id="file" name="file" type="file"/>
<el-button size="small" type="primary" id="startBtn">上传视频</el-button>
<div>
  <el-progress style="width: 400px"  v-if="percentage==100 && !isError" :percentage="percentage" status="success"></el-progress>
  <el-progress style="width: 400px" :percentage="percentage" v-else-if="percentage > 0 && !isError && percentage < 100"></el-progress>
  <el-progress style="width: 400px" :percentage="percentage" status="exception" v-else-if="isError"></el-progress>
</div>

 

data() {
	return {
		percentage: 0, // 进度条
      	isError: false, // 是否发生错误
      	request: null  // ajax请求
	}
},
mounted() {
	var pecent;
    var start;
    
    var end;
	var file;
	var name;
	var size;
	var shardCount;
	var i = 0;
	var shardSize;
    var GUID;
		
        var status = 0;
        var _this = this;
		
        var page = {
            init: function(){
                $("#startBtn").click($.proxy(this.upload, this));
            },
            upload: function(){
                status = 0;
                
          
                file = $("#file")[0].files[0];  //文件对象

                if (!file) {
                  _this.$message.warning('请选择文件');
                  return;
                }
                name = file.name;        //文件名
                size = file.size;        //总大小
                GUID = this.guid(file.name, file.lastModified, file.size, file.type);
                shardSize = 10 * 1024 * 1024;   //以1MB为一个分片
                shardCount = Math.ceil(size / shardSize);  //总片数
                
                // 获取当前的片数
                let formData = new FormData();
                formData.append("md5", GUID);
                getCurrentFileChunk(formData).then(res => {
                  if (res.result.chunk < 0) {
                    _this.form.videoUrl = res.result.url;
                    _this.percentage = 100;
                  } else {
                    status = res.result.chunk;
                    start = res.result.chunk * shardSize;
                    end = Math.min(size, start + shardSize);
                    var partFile = file.slice(start,end);
                    

                    var pecent=100*(start * shardSize)/file.size;
                    _this.percentage = parseInt(pecent);

                    this.partUpload(GUID,partFile,name,shardCount,status);
                  }
                });
            },
            partUpload:function(GUID,partFile,name,chunks,chunk){
              // 重新上传的时候
              _this.isError = false;

              //构造一个表单,FormData是HTML5新增的
              var  now = this;
              var form = new FormData();
              form.append("md5", GUID);
              form.append("file", partFile);  //slice方法用于切出文件的一部分
              form.append("chunk", chunk);        //当前是第几片
              //form.append("chunks", chunks);  //总片数
              //Ajax提交
              _this.request = $.ajax({
                  url: process.env.API_F_URL + "/files/uploadVideo",
                  type: "POST",
                  data: form,
                  async: true,    //同步
                  processData: false,  //很重要,告诉jquery不要对form进行处理
                  contentType: false,  //很重要,指定为false才能形成正确的Content-Type
                  success: function(data){
                      status++;
                      if (status < chunks) {
                        start = status * shardSize,
                        end = Math.min(size, start + shardSize);
                        var partFile = file.slice(start,end);
                        now.partUpload(GUID,partFile,name,shardCount,status);
                      }
                      
                      // if(data.code == 200){
                      //   $("#output").html(status+ " / " + chunks);
                      // }
                      if(status==chunks){
                        now.mergeFile(GUID,name, chunks);
                      }
                  },
                  error: function(err) {
                    console.log('err', err);
                    if (err.statusText === 'abort') {
                       _this.$message.warning('已取消上传');
                    } else {
                      _this.isError = true;
                      _this.$message.error('上传失败,请重新上传');
                      
					  // 上传失败,再次上传
                      // start = status * shardSize,
                      // end = Math.min(size, start + shardSize);
                      // var partFile = file.slice(start,end);
                      // now.partUpload(GUID,partFile,name,shardCount,status);
                    }
                  },
                  xhr: function () {
                    //获取ajax中的ajaxSettings的xhr对象  为他的upload属性绑定progress事件的处理函数
                    var myXhr = $.ajaxSettings.xhr();
                    if (myXhr.upload) {
                      //检查其属性upload是否存在
                      myXhr.upload.addEventListener("progress", function(ev){
                        if(ev.lengthComputable){
                          pecent=100*(ev.loaded+start)/file.size;
                          if(pecent>99){
                            pecent=99;
                          }
                         
                          _this.percentage = parseInt(pecent);
                        }
                      }, false);
                    }
                    return myXhr;
                  },
                });
        },
        mergeFile:function(GUID,name,chunks){
            var formMerge = new FormData();
            formMerge.append("md5", GUID);
            formMerge.append("fileName", name);
			      formMerge.append("chunks", chunks);
            $.ajax({
                url: process.env.API_F_URL + "/files/mergeVideo",
                type: "POST",
                data: formMerge,
                processData: false,  //很重要,告诉jquery不要对form进行处理
                contentType: false,  //很重要,指定为false才能形成正确的Content-Type
                success: function(res){
                  if (res.status == 'success') {
                    _this.isError = false;
                    _this.form.videoUrl = res.result.url;
                    _this.percentage = 100;
                  } else {
                    _this.isError = true;
                    _this.$message.error('上传失败,请重新上传');
                  }
                },
                error: function(err) {
                  _this.isError = true;
                  _this.$message.error('上传失败,请重新上传');
                }
            });
        },
        guid:function(name, lastModified, size, type){
				  return md5(name+'#'+lastModified+'#'+size+'#'+type);
        }
    };

    $(function(){
        page.init();
    });
},
beforeDestroy() {
    this.request.abort();
}

 

不完整代码二:只实现了分片上传+进度条(html+jq)

自己看vue版本的也能把这版弄完整,但是我太懒了,不想弄了。大家自己动手吧

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <script src="http://libs.baidu.com/jquery/2.1.4/jquery.min.js"></script>
		<script src="http://www.gongjuji.net/Content/files/jquery.md5.js"></script>
        <meta content="text/html; charset=utf-8" http-equiv="Content-Type"/>
    </head>
    <body>
        <div id="uploader">
            <div class="btns">
                <input id="file" name="file" type="file"/>
                <br>
                    <br>
                        <button id="startBtn">
                            开始上传
                        </button>
						<div id="upimg">
							<div id="load"></div>
					   </div>
                    </br>
                </br>
            </div>
            <div id="output">
            </div>
        </div>
    </body>
    <script type="text/javascript">
		var des=document.getElementById('load');
		var num=document.getElementById('upimg');
		var pecent;
		var start;
		var file;
		var name;
		var size;
		var shardCount;
		var i = 0;
		var shardSize;
		var GUID;
		
        var status = 0;
		
        var page = {
        init: function(){
            $("#startBtn").click($.proxy(this.upload, this));
        },
        upload: function(){
            status = 0;
            
			
            file = $("#file")[0].files[0];  //文件对象
			name = file.name;        //文件名
			size = file.size;        //总大小
			GUID = this.guid(file.name, file.lastModified, file.size, file.type);
			shardSize = 10 * 1024 * 1024;   //以1MB为一个分片
			shardCount = Math.ceil(size / shardSize);  //总片数
			
			
			start = 0,
			end = Math.min(size, start + shardSize);
			var partFile = file.slice(start,end);
			this.partUpload(GUID,partFile,name,shardCount,0);
		
        },
        partUpload:function(GUID,partFile,name,chunks,chunk){
            //构造一个表单,FormData是HTML5新增的
            var  now = this;
            var form = new FormData();
            form.append("md5", GUID);
            form.append("file", partFile);  //slice方法用于切出文件的一部分
            form.append("chunk", chunk);        //当前是第几片
			//form.append("chunks", chunks);  //总片数
                //Ajax提交
                $.ajax({
                    url: "/api-f/files/uploadVideo",
                    type: "POST",
                    data: form,
                    async: true,    //同步
                    processData: false,  //很重要,告诉jquery不要对form进行处理
                    contentType: false,  //很重要,指定为false才能形成正确的Content-Type
                    success: function(data){
                        status++;
						if (status < chunks) {
							start = status * shardSize,
							end = Math.min(size, start + shardSize);
							var partFile = file.slice(start,end);
							now.partUpload(GUID,partFile,name,shardCount,status);
						}
						
						if(data.code == 200){
							$("#output").html(status+ " / " + chunks);
						}
						if(status==chunks){
							now.mergeFile(GUID,name, chunks);
						}
                    },
					xhr: function () {
						//获取ajax中的ajaxSettings的xhr对象  为他的upload属性绑定progress事件的处理函数
						var myXhr = $.ajaxSettings.xhr();
						if (myXhr.upload) {
							//检查其属性upload是否存在
							myXhr.upload.addEventListener("progress", function(ev){
								if(ev.lengthComputable){
									pecent=100*(ev.loaded+start)/file.size;
									if(pecent>100){
									  pecent=100;
									}
									des.style.width=pecent+'%';
									des.innerHTML = parseInt(pecent)+'%'
								}
							}, false);
						}
						return myXhr;
					},

					
                });
        },
		
        mergeFile:function(GUID,name,chunks){
            var formMerge = new FormData();
            formMerge.append("md5", GUID);
            formMerge.append("fileName", name);
			formMerge.append("chunks", chunks);
            $.ajax({
                url: "/api-f/files/mergeVideo",
                type: "POST",
                data: formMerge,
                processData: false,  //很重要,告诉jquery不要对form进行处理
                contentType: false,  //很重要,指定为false才能形成正确的Content-Type
                success: function(data){
                    if(data.code == 200){
                        alert('上传成功!');
                    }
                }
            });
        },
        guid:function(name, lastModified, size, type){
				return $.md5(name+'#'+lastModified+'#'+size+'#'+type);
                // var counter = 0;
                // var guid = (+new Date()).toString( 32 ),
                //     i = 0;
                // for ( ; i < 5; i++ ) {
                //    guid += Math.floor( Math.random() * 65535 ).toString( 32 );
                // }
                // return (prefix || 'wu_') + guid + (counter++).toString( 32 );
        }
    };
	
    $(function(){
        page.init();
    });
    </script>
</html>

参考文章:http://blog.ncmem.com/wordpress/2023/10/11/js%e5%a4%a7%e6%96%87%e4%bb%b6%e5%88%87%e7%89%87%e4%b8%8a%e4%bc%a0%ef%bc%8c%e6%96%ad%e7%82%b9%e7%bb%ad%e4%bc%a0%e5%ae%9e%e7%8e%b0demo/

欢迎入群一起讨论