一般文件比较大时,可以采用第三方图片存储方案(如OSS),或者分块传输,本文将讲解使用php进行分块上传。
分块上传指的是将要上传的文件切成一片片小的区块进行上传,然后服务端接收这些前端上传过来的小区块,存储到
服务器存储文件的目录下,待上传完毕时将这些小区块合并成一个文件,并将已上传的这些区块删除。至此,上传过
程结束。
这里有几个细节需要注意,在上传时前端需要传给服务端一个“标志”,以表明这是一个分块上传的类型而不是文件
直接整个上传,用以区分这2种文件的上传类型。对于小文件可以直接上传,不需要分片。
(可选)其次前端需要再发送原文件的相关信息如文件名,便于服务端的存储。也可以不传,具体的存储逻辑开发者
可以自己实现。
技术栈:thinkphp、前端
以下是前端需要执行的一些代码逻辑
由于是个demo,因此开发者对于其它方面的需求可以自己实现
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>分块上传</title> <script class="lazyload" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAANSURBVBhXYzh8+PB/AAffA0nNPuCLAAAAAElFTkSuQmCC" data-original="/static/js/jquery.min.js"></script> <!-- md5相关的库文件 --> <script class="lazyload" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAANSURBVBhXYzh8+PB/AAffA0nNPuCLAAAAAElFTkSuQmCC" data-original="/static/js/core.js"></script> <script class="lazyload" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAANSURBVBhXYzh8+PB/AAffA0nNPuCLAAAAAElFTkSuQmCC" data-original="/static/js/md5.js"></script> </head> <body> <div> <!-- 文件上传的元素 --> <input type="file" id="file"> </div> <script> $(document).ready(function () { // 监听文件上传 $('#file').change(function (e) { // 定义每个区块的大小,以字节为单位 var chunk = 18*1024; var uploadFile = $(this)[0].files[0] var fileReader = new FileReader() // 初始化记录的进度 var loaded = 0 console.log(uploadFile,e) readPartFile(loaded,chunk) var fileId = CryptoJS.MD5(uploadFile).toString() // 监听加载完毕的区块 fileReader.onload = function (event) { // 更新进度 loaded += chunk var targetResult = event.target.result // 转换成Blob对象 var targetBlob = new Blob([targetResult]) console.log('event',event,'mime',targetBlob) var isEnd = loaded >= uploadFile.size ? 1 : 0 //每3秒上传一次,防止请求频率过快,时间可自定义 setTimeout(() => { pieceUpload(targetBlob,isEnd,fileId,uploadFile.name,function (e) { console.log(e) if (isEnd) { return alert('上传完成!') } //当前区块上传完毕后再上传下一个区块 readPartFile(loaded,chunk) }) },3000) } // 将文件分成指定大小的区块,start是起始位置 function readPartFile(start) { var piece = uploadFile.slice(start,start + chunk) fileReader.readAsArrayBuffer(piece) } // 分块上传 file:上传的区块 isEnd:是否已整体上传结束 fileId:源文件的唯一标识 filename:文件名 callback:上传之后的回调 function pieceUpload(file,isEnd,fileId,filename,callback) { try { var formData = new FormData() formData.append('file',file,'file') formData.append('is_end',isEnd) formData.append('file_id',fileId) formData.append('filename',filename) //向服务端表明该请求是一个分块上传 formData.append('type','chunk') console.log(formData) $.ajax({ //分块上传的接口url url: '/index/index/chunkUpload', type: 'POST', data: formData, processData: false, // dataType: 'json', // contentType: 'application/json', contentType: false, success: callback, error: function (err) { console.log(err) } }) } catch (e) { console.log(e) } } }) }) </script> </body> </html>
后端的代码逻辑
//执行分块上传的控制器方法 public function chunkUpload() { if ($this->request->isPost()) { //执行分块上传流程 $data = $this->request->post(); //判断是否是分块上传 if ($data['type'] === 'chunk') { $file = request()->file('file'); //获取对应的上传配置 $fs = Filesystem::disk('local'); $ext = $file->extension(); $chunkPath = $data['file_id'].'/'.$file->md5().($ext ? '.'.$ext : ''); //存储分片文件到指定路径 $savename = $fs->putFileAs( 'chunk', $file,$chunkPath); if (!$savename) { return json([ 'code' => 1, 'msg' => '上传失败', 'data' => [], ]); } if (!$data['is_end']) { $extra['url'] = ''; } else { //合并块文件 $fileUrl = ''; $chunkSaveDir = Filesystem::getDiskConfig('local'); $smallChunkDir = $chunkSaveDir['root'].'/chunk/'.$data['file_id']; //获取已存储的属于源文件的所有分块文件 进行合并 if ($handle = opendir($smallChunkDir)) { $chunkList = []; $modifyTime = []; while (false !== ($file = readdir($handle))) { if ($file != "." && $file != "..") { $temp['path'] = $smallChunkDir.'/'.$file; $temp['modify'] = filemtime($smallChunkDir.'/'.$file); $chunkList[] = $temp; $modifyTime[] = $temp['modify']; } } //对分块文件进行排序 array_multisort($modifyTime,SORT_ASC,$chunkList); $saveDir = Filesystem::getDiskConfig('public'); // $newPath = $saveDir['root'].'/'.date('Ymd').'/'.$data['file_id'].($newExt ? '.'.$newExt : ''); $newPath = $saveDir['root'].'/'.date('Ymd').'/'.$data['filename']; if (!file_exists($saveDir['root'].'/'.date('Ymd'))) { mkdir($saveDir['root'].'/'.date('Ymd'),0777,true); } $newFileHandle = fopen($newPath,'a+b'); foreach ($chunkList as $item) { fwrite($newFileHandle,file_get_contents($item['path'])); unlink($item['path']); } rmdir($smallChunkDir); //将合并后的文件存储到指定路径 $fileUrl = $saveDir['url'].'/'.date('Ymd').'/'.$data['filename']; fclose($newFileHandle); closedir($handle); } else { return json([ 'code' => 1, 'msg' => '目录:'.$chunkSaveDir['root'].'/chunk/'.$data['file_id'].'不存在', 'data' => [], ]); } $extra['url'] = $fileUrl; } //合并流程结束 return json([ 'code' => 0, 'msg' => '上传成功', 'data' => $extra, ]); } } return view(); }
引入的js依赖:
<script class="lazyload" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAANSURBVBhXYzh8+PB/AAffA0nNPuCLAAAAAElFTkSuQmCC" data-original="/static/js/core.js"></script> <script class="lazyload" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAANSURBVBhXYzh8+PB/AAffA0nNPuCLAAAAAElFTkSuQmCC" data-original="/static/js/md5.js"></script>
https://github.com/brix/crypto-js
<script class="lazyload" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAANSURBVBhXYzh8+PB/AAffA0nNPuCLAAAAAElFTkSuQmCC" data-original="/static/js/jquery.min.js"></script>
https://jquery.com/download/
授权许可:本文相关代码遵循MIT协议,用户可在遵循MIT协议所允许和禁止的范围下使用。若转载,请署名作者及来源。
MIT协议:https://baike.baidu.com/item/MIT/10772952?fr=aladdin
额外说明:若您在使用代码的过程中遇到了问题,欢迎在评论下方留言。另外本人平时兼职
开发,若您有合适的开发需求,欢迎联系我。