用js+php实现大文件分片上传

图片上传功能在各类网站和web应用中算是屡见不鲜了,对于图片上传常规的做法是一次性将整个文件一次性发送给服务端,这样做看似没啥问题,直到有一天,上帝(顾客)投诉上门了。

产品:喂,那谁,有客户说在我们网站图片上传不上去。

程序猿:那客户的图片有多大呢?

产品:客户说单张才200M

程序猿:...,才200M!!!一口老血喷溅

实际情况是200M的图片真的算大吗?在这个数码设备飞速发展的时代,一张几百M的图片并不是什么稀罕事,但是对于程序猿来说一次性上传几百M的图片到服务端,确实是一件稀罕事,因为平常测试的基本都是几M的图片,再大一点的也就是10来M,现在一次性上传几百M的图片到服务端对上传造成的压力可想而知,而且服务端对于上传文件的大小和请求时间都是有限制的,就算我们将可允许上传的文件大小和超时时间设置的无限大,只要服务器的负载稍微上来一点儿,这张几百M的图片上传很大可能会失败。

尽管现实很残酷,生活很无奈,我们还是得想办法活下去呀,为了让用户能够稳定的上传几百M甚至更大一些的图片,就不得不提到一个名词,分片上传。

那么,什么是分片上传呢?

所谓分片上传就是将一个大文件按照一定的规则划分成若干个小文件,然后我们再分别上传这若干个小文件,毕竟小文件的上传相对来说稳定很多,等到所有的小文件都上传完成之后再将这些小文件按顺序合并还原原来的大文件。

如何用js实现分片上传呢?

在着手实现之前我们不得不先提提js中的blob对象。

blob对象算是js中一个特殊的对象,对于这个对象很多人可能会说知之甚少甚至根本没听说过,但是实际我们经常会用到他, blob是Binary Large Object的缩写,直译为二进制大对象,表示一个不可变、原始数据的类文件对象。其原型链上有一个神奇的方法slice,该方法允许开发者获取blob对象中指定范围内的数据。而我们经常使用到的File对象实际也是继承自blob对象,所以我们也能使用那个神奇的slice方法,而这是实现分片上传的基础和关键。

blob.slice(start, end);

其实其实知道了这个关键点,剩下的工作就是码代码了,这里贴出我写的一个简单的实现:

(function() {
  var Uploader = (function() {
    var params = {
      sliceSize: 4 * 1024 * 1024, // 2MB
      sync: true, // 同步传输
      sliceUpload: true
    };

    function setOption(pms) {
      if (pms && typeof pms === 'object') {
        params = Object.assign({}, params, pms);
      }
    }

    function upload(file) {
      var name = file.name,
              totalSize = file.size,
              sliceSize = params.sliceUpload ? params.sliceSize : totalSize,
              sliceCount = Math.ceil(totalSize / sliceSize);
          var uploadedSize = 0, uploadedCount = 0;

          function onProgress(evt) {
            var loaded = evt.loaded;
            uploadedSize += loaded;
            var per = Math.floor(100 * uploadedSize / totalSize);
            if (per > 100) {
              per = 100;
            }
            params.onProgress && params.onProgress(per);
          }

          function onSuccess() {
            uploadedCount ++;
            if (uploadedCount >= sliceCount) {
              params.onAllUploaded && params.onAllUploaded({
                sliceCount: sliceCount,
                size: totalSize,
                name: name
              });
            }
          }

          function onFail(fileData, i) {
            // 2s后重传
            setTimeout(function() {
              doUpload(fileData, i)
            }, 2000);
          }

      function doUpload(fileData, i, isSync) {
        var form = new FormData();
          form.append('file', fileData);
            form.append('name', name);
            form.append('size', totalSize);
            if (sliceCount > 1) {
              form.append('chunks', sliceCount);
              form.append('index', i);
            }
            $.ajax({
              url: '/slice-upload/upload.php',
              type: 'POST',
              data: form,
              processData: false,
              contentType: false,
              xhr: function() {
                    var xhr = $.ajaxSettings.xhr();
                    if (xhr && xhr.upload) {
                        xhr.upload.addEventListener('progress', onProgress, false);
                        return xhr;
                    }
              },
                success: function(req) {
                    onSuccess();
                    if (isSync) {
                      var nextSliceIndex = i+1;
                      if (nextSliceIndex <= sliceCount) {
                        var start = i * sliceSize;
                        var end = start + sliceSize;
                        if (end > totalSize) {
                          end = totalSize
                        }
                        var nextFileData = file.slice(start, end);
                        doUpload(nextFileData, i+1, isSync);
                      }
                    }
                },
                error: function() {
                    onFail(fileData, i);
                }
            });
      }

      if (params.sync) {
        doUpload(file.slice(0, sliceSize), 1, true);
      } else {
        for (var i=0; i < sliceCount; i++) {
          (function(i) {
            var start = i * sliceSize;
            var end = start + sliceSize;
            if (end > totalSize) {
              end = totalSize
            }
            var fileData = file.slice(start, end);
            doUpload(fileData, i+1);
          })(i);
        }
      }
    }

    function init(pms) {
      if (pms &amp;&amp; typeof pms === 'object') {
        params = Object.assign({}, params, pms);
      }
    }

    return {
      init: init,
      upload: upload,
      setOption: setOption
    };
  })();

  window.Uploader = Uploader;
})();

后端代码这个就自由发挥了,你可以点击这里查看在线实例。

对于分片上传的一些思考:

1、分片上传速度一定比整个一次性上传速度快吗?

这个不一定,整个一次性上传只发起了一次请求,但是分片上传需要发起多个请求,而http协议是基于tcp协议的,每一次建立请求都需要三次握手,时间开销还是有的,再加上后端处理分片信息也需要时间消耗,但是可以确定的时对于大文件上传分片上传一定比整个一次性上传稳定,而且也完美解决了超大文件无法上传的问题。

2、分片异步上传和分片同步上传有啥区别?

分片同步上传是指顺序的上传分片,即上传完第一个分片再继续上传第二个分片,知道所有分片上传完毕,分片异步上传是指同时上传所有的分片,直到所有分片都上传完毕,理论上异步比同步可能应该会快,但是实际可能并不是这样,同时发起多个请求会不会存在网速分配问题,另外浏览器本身对并发也有一定的限制。

文末提供实例的源代码(包括PHP代码),有需要的请自行下载。

猛戳这里下载本文案例源码包
  • 支付宝二维码 支付宝
  • 微信二维码 微信
相关文章