分片上传,就是将所要上传的文件,按照一定的大小,将整个文件分隔成多个数据块(我们称之为Part)来进行分别上传,上传完之后再由服务端对所有上传的文件进行汇总整合成原始的文件。 分片上传的场景:大文件上传;网络环境环境不好,存在需要重传风险的场景

PHP

/**
 * 上传文件
 * @ApiMethod (POST)
 * @param File $file 文件流
 */
public function upload()
{
    Config::set('default_return_type', 'json');
    //必须设定cdnurl为空,否则cdnurl函数计算错误
    Config::set('upload.cdnurl', '');
    $chunkid = $this->request->post("chunkid");
    if ($chunkid) {
        if (!Config::get('upload.chunking')) {
            $this->error('分片上传未启用');
        }
        $action = $this->request->post("action");
        $chunkindex = $this->request->post("chunkindex/d");
        $chunkcount = $this->request->post("chunkcount/d");
        $filename = $this->request->post("filename");
        $method = $this->request->method(true);
        if ($action == 'merge') {
            $attachment = null;
            //合并分片文件
            try {
                $upload = new Upload();
                $attachment = $upload->merge($chunkid, $chunkcount, $filename);
            } catch (UploadException $e) {
                $this->error($e->getMessage());
            }
            $this->success('上传成功', ['url' => $attachment['url'], 'fullurl' => request()->domain().$attachment['url']]);
        } elseif ($method == 'clean') {
            //删除冗余的分片文件
            try {
                $upload = new Upload();
                $upload->clean($chunkid);
            } catch (UploadException $e) {
                $this->error($e->getMessage());
            }
            $this->success();
        } else {
            //上传分片文件
            //默认普通上传文件
            $file = $this->request->file('file');
            try {
                $upload = new Upload($file);
                $result=$upload->chunk($chunkid, $chunkindex, $chunkcount);
            } catch (UploadException $e) {
                $this->error($e->getMessage());
            }
            $this->success('上传成功',['chunkindex'=>$chunkindex,'chunkcount'=>$chunkcount]);
        }
    } else {
        $attachment = null;
        //默认普通上传文件
        $file = $this->request->file('file');
        try {
            $upload = new Upload($file);
            $attachment = $upload->upload();
        } catch (UploadException $e) {
            $this->error($e->getMessage());
        }

        $this->success('上传成功', ['url' => $attachment['url'], 'fullurl' => request()->domain().['url']]);
    }

}

HTML

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>大文件分片上传</title>
  <link href="static/layui/css/layui.css" rel="stylesheet">
  <script src="static/layui/layui.js"></script>
  <style>
    .layui-upload {
      width: 80%;
      margin: 2rem auto;
    }
  </style>
</head>
<body>
  <div class="layui-upload">
    <button type="button" class="layui-btn layui-btn-normal" id="uploads-files">选择文件</button>
    <div class="layui-upload-list">
      <table class="layui-table">
        <colgroup>
          <col style="min-width: 100px;">
          <col width="150">
          <col width="260">
          <col width="150">
        </colgroup>
        <thead>
          <th>文件名</th>
          <th>大小</th>
          <th>上传进度</th>
          <th>操作</th>
        </thead>
        <tbody id="uploads-files-list"></tbody>
      </table>
    </div>
    <input type="hidden" id="totalPage" value="0"/>
    <input type="hidden" id="page" value="1"/>
    <!-- <button type="button" class="layui-btn" id="uploads-files-action">开始上传</button> -->
  </div>
</body>
<script>
  function generateUUID() {
      var d = new Date().getTime();
      if (typeof performance !== 'undefined' && typeof performance.now === 'function') {
        d += performance.now(); // 使用性能相关数据提高唯一性
      }
      var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
        var r = (d + Math.random() * 16) % 16 | 0;
        d = Math.floor(d / 16);
        return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
      });
      return uuid;
    }

  layui.use(function () {
    var upload = layui.upload;
    var element = layui.element;
    var $ = layui.$;
    var curronProgress;
    // 制作多文件上传表格
    var uploadListIns = upload.render({
      elem: '#uploads-files',
      elemList: $('#uploads-files-list'), // 列表元素对象
      url: 'api/index/upload', // 此处用的是第三方的 http 请求演示,实际使用时改成您自己的上传接口即可。
      // data:{
      //   filesize:null,
      //   filename:null,
      //   chunkid:null,
      //   chunkindex:null,
      //   chunkcount:null,
      //   chunksize:null,
      //   chunkfilesize:null,
      //   width:null,
      //   height:null,
      //   type:null
      // },
      accept: 'file',
      acceptMime: 'application/zip',
      multiple: false,
      number: 1,
      auto: false,
      bindAction: '#uploads-files-action',
      choose: function (obj) {
        var that = this;
        var files = this.files = obj.pushFile(); // 将每次选择的文件追加到文件队列
        // 读取本地文件
        obj.preview(function (index, file, result) {
          curronProgress=index;
          that.data.filesize=file.size;
          that.data.filename=file.name;
          that.data.chunkid=generateUUID();
          that.data.chunkindex=0;
          that.data.chunkcount=Math.ceil(file.size/2097152);
          // console.log(that.data.chunkcount);
          that.data.chunksize=2097152;
          that.data.chunkfilesize = 2097152;
          that.data.width=file.width || 0;
          that.data.height=file.height || 0;
          that.data.type=file.type;
          var tr = $(['<tr id="upload-' + index + '">',
          '<td>' + file.name + '</td>',
          '<td>' + (file.size / 1024).toFixed(1) + 'kb</td>',
          '<td><div class="layui-progress" lay-filter="progress-files-' + index + '"><div class="layui-progress-bar" lay-percent=""></div></div></td>',
            '<td>',
            '<button class="layui-btn layui-btn-xs files-reload layui-hide">重传</button>',
            '<button class="layui-btn layui-btn-xs layui-btn-danger files-delete">删除</button>',
            '</td>',
            '</tr>'].join(''));
            $('#totalPage').val(that.data.chunkcount);
            $('#page').val(0);
            var progressTimer = setInterval(function () {
              var totalPage = parseInt($('#totalPage').val());
              var page = parseInt($('#page').val());
              // console.log(that.data.chunkcount + '_' + parseInt(page));
              if (that.data.chunkcount == parseInt(page)) {
                clearInterval(progressTimer);//上传成功或失败停止上传
              } else {
                obj.upload(that.data.chunkindex, file.slice(page * that.data.chunksize, (page+1) * that.data.chunksize));//从文件中截取进行分片上传
              }
            }, 1500);
          // 单个重传
          // tr.find('.files-reload').on('click', function () {
          //   obj.upload(index, file);
          // });
          // 删除
          tr.find('.files-delete').on('click', function () {
            delete files[index]; // 删除对应的文件
            tr.remove(); // 删除表格行
            // 清空 input file 值,以免删除后出现同名文件不可选
            uploadListIns.config.elem.next()[0].value = '';
          });
          that.elemList.append(tr);
          element.render('progress'); // 渲染新加的进度条组件
        });
      },
      done: function (res, index, upload) { // 成功的回调
        var that = this;
        if(res.code == 1){ // 上传成功
          var tr = that.elemList.find('tr#upload-' + index);
          var tds = tr.children();
          tds.eq(3).html(''); // 清空操作
          delete this.files[index]; // 删除文件队列已经上传成功的文件
          var totalPage = parseInt($('#totalPage').val());
          var page = parseInt($('#page').val());
          page = page + 1;//上传下一片
          $('#page').val(page);
          element.progress('progress-files-'+curronProgress,Math.ceil(page * 100 / totalPage)+'%');//更新进度条
          that.data.chunkindex=page;
          if(page==totalPage){
            $.ajax({
              type: "POST",
              url: "api/index/upload",
              data: { 
                action: 'merge', 
                filesize: that.data.filesize, 
                filename: that.data.filename, 
                chunkid: that.data.chunkid, 
                chunkcount: that.data.chunkcount 
              },
              dataType: "json",
              success: function (result) {
                if(result.data.fullurl){
                  layer.alert('<a href="'+result.data.fullurl+'">点击下载文件:'+result.data.fullurl+'</a>', {
                    shadeClose: true,
                    title: result.msg 
                  });
                }else{
                  layer.msg(result.msg);
                }
              }
            });
            return false;
          }
        }
        this.error(index, upload);
      },
      error: function (index, upload) { // 错误回调
        // console.log(upload)
        var that = this;
        var tr = that.elemList.find('tr#upload-' + index);
        var tds = tr.children();
        // 显示重传
        tds.eq(3).find('.files-reload').removeClass('layui-hide');
      },
      progress: function (n, elem, e, index) { // 注意:index 参数为 layui 2.6.6 新增
        element.progress('progress-files-' + index, n + '%'); // 执行进度条。n 即为返回的进度百分比
      }
    });
  });
</script>
</html>

附件下载://file.alonesky.com/file/splitupload.400539.com_20230809_165758.zip

参考链接:

https://blog.csdn.net/NET_class/article/details/105271137 https://blog.csdn.net/qq_36596869/article/details/119332005 https://layui.dev/docs/2.8/upload/#options.data