PHP搭建大文件切割分块上传功能示例
背景
在网站开发中,文件上传是很常见的一个功能。相信很多人都会遇到这种情况,想传一个文件上去,然后网页提示“该文件过大”。因为一般情况下,我们都需要对上传的文件大小做限制,防止出现意外的情况。
但是在有些业务场景中,大文件上传又是必须的,比如邮箱附件,或者内部OA等等。
问题
服务端为什么不能直接传大文件?跟php.ini里面的几个配置有关
upload_max_filesize=2M//PHP最大能接受的文件大小 post_max_size=8M//PHP能收到的最大POST值' memory_limit=128M//内存上限 max_execution_time=30//最大执行时间
当然不能简单粗暴的把上面几个值调大,否则服务器内存资源吃光是迟早的问题。
解决思路
好在HTML5开放了新的FILEAPI,也可以直接操作二进制对象,我们可以直接在浏览器端实现文件切割,按照以前的做法就得用Flash的方案,实现起来会麻烦很多。
JS思路
1.监听上传按钮的onchange事件
2.获取文件的FILE对象
3.把文件的FILE对象进行切割,并且附加到FORMDATA对象中
4.把FORMDATA对象通过AJAX发送到服务器
5.重复3、4步骤,直到文件发送完。
PHP思路
1.建立上传文件夹
2.把文件从上传临时目录移动到上传文件夹
3.所有的文件块上传完成后,进行文件合成
4.删除文件夹
5.返回上传后的文件路径
DEMO代码
前端部分代码
<!doctypehtml> <htmllang="en"> <head> <metacharset="UTF-8"> <metaname="viewport" content="width=device-width,user-scalable=no,initial-scale=1.0,maximum-scale=1.0,minimum-scale=1.0"> <metahttp-equiv="X-UA-Compatible"content="ie=edge"> <title>Document</title> <style> #progress{ width:300px; height:20px; background-color:#f7f7f7; box-shadow:inset01px2pxrgba(0,0,0,0.1); border-radius:4px; background-image:linear-gradient(tobottom,#f5f5f5,#f9f9f9); } #finish{ background-color:#149bdf; background-image:linear-gradient(45deg,rgba(255,255,255,0.15)25%,transparent25%,transparent50%,rgba(255,255,255,0.15)50%,rgba(255,255,255,0.15)75%,transparent75%,transparent); background-size:40px40px; height:100%; } form{ margin-top:50px; } </style> </head> <body> <divid="progress"> <divid="finish"style="width:0%;"progress="0"></div> </div> <formaction="./upload.php"> <inputtype="file"name="file"id="file"> <inputtype="button"value="停止"id="stop"> </form> <script> varfileForm=document.getElementById("file"); varstopBtn=document.getElementById('stop'); varupload=newUpload(); fileForm.onchange=function(){ upload.addFileAndSend(this); } stopBtn.onclick=function(){ this.value="停止中"; upload.stop(); this.value="已停止"; } functionUpload(){ varxhr=newXMLHttpRequest(); varform_data=newFormData(); constLENGTH=1024*1024; varstart=0; varend=start+LENGTH; varblob; varblob_num=1; varis_stop=0 //对外方法,传入文件对象 this.addFileAndSend=function(that){ varfile=that.files[0]; blob=cutFile(file); sendFile(blob,file); blob_num+=1; } //停止文件上传 this.stop=function(){ xhr.abort(); is_stop=1; } //切割文件 functioncutFile(file){ varfile_blob=file.slice(start,end); start=end; end=start+LENGTH; returnfile_blob; }; //发送文件 functionsendFile(blob,file){ vartotal_blob_num=Math.ceil(file.size/LENGTH); form_data.append('file',blob); form_data.append('blob_num',blob_num); form_data.append('total_blob_num',total_blob_num); form_data.append('file_name',file.name); xhr.open('POST','./upload.php',false); xhr.onreadystatechange=function(){ varprogress; varprogressObj=document.getElementById('finish'); if(total_blob_num==1){ progress='100%'; }else{ progress=Math.min(100,(blob_num/total_blob_num)*100)+'%'; } progressObj.style.width=progress; vart=setTimeout(function(){ if(start<file.size&&is_stop===0){ blob=cutFile(file); sendFile(blob,file); blob_num+=1; }else{ setTimeout(t); } },1000); } xhr.send(form_data); } } </script> </body> </html>
PHP部分代码
<?php classUpload{ private$filepath='./upload';//上传目录 private$tmpPath;//PHP文件临时目录 private$blobNum;//第几个文件块 private$totalBlobNum;//文件块总数 private$fileName;//文件名 publicfunction__construct($tmpPath,$blobNum,$totalBlobNum,$fileName){ $this->tmpPath=$tmpPath; $this->blobNum=$blobNum; $this->totalBlobNum=$totalBlobNum; $this->fileName=$fileName; $this->moveFile(); $this->fileMerge(); } //判断是否是最后一块,如果是则进行文件合成并且删除文件块 privatefunctionfileMerge(){ if($this->blobNum==$this->totalBlobNum){ $blob=''; for($i=1;$i<=$this->totalBlobNum;$i++){ $blob.=file_get_contents($this->filepath.'/'.$this->fileName.'__'.$i); } file_put_contents($this->filepath.'/'.$this->fileName,$blob); $this->deleteFileBlob(); } } //删除文件块 privatefunctiondeleteFileBlob(){ for($i=1;$i<=$this->totalBlobNum;$i++){ @unlink($this->filepath.'/'.$this->fileName.'__'.$i); } } //移动文件 privatefunctionmoveFile(){ $this->touchDir(); $filename=$this->filepath.'/'.$this->fileName.'__'.$this->blobNum; move_uploaded_file($this->tmpPath,$filename); } //API返回数据 publicfunctionapiReturn(){ if($this->blobNum==$this->totalBlobNum){ if(file_exists($this->filepath.'/'.$this->fileName)){ $data['code']=2; $data['msg']='success'; $data['file_path']='http://'.$_SERVER['HTTP_HOST'].dirname($_SERVER['DOCUMENT_URI']).str_replace('.','',$this->filepath).'/'.$this->fileName; } }else{ if(file_exists($this->filepath.'/'.$this->fileName.'__'.$this->blobNum)){ $data['code']=1; $data['msg']='waitingforall'; $data['file_path']=''; } } header('Content-type:application/json'); echojson_encode($data); } //建立上传文件夹 privatefunctiontouchDir(){ if(!file_exists($this->filepath)){ returnmkdir($this->filepath); } } } //实例化并获取系统变量传参 $upload=newUpload($_FILES['file']['tmp_name'],$_POST['blob_num'],$_POST['total_blob_num'],$_POST['file_name']); //调用方法,返回结果 $upload->apiReturn();
存在的问题
这只是一个简单的DEMO,有很多地方需要改进,比如上传的文件夹与临时文件放在一起,用户中途取消也没有发请求进行清理,容易造成文件冗余。JS采用的是同步模型,文件需要一块一块按顺序上传,会导致整个浏览器在上传的过程中出于堵塞的状态,按了按钮可能需要几秒钟才能反应过来,用户体验不好。真正需要产品化的时候就要综合考虑多种情况,当然作为一个示例,引导大家了解分块上传的思路还是不错的。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。