用HTML5实现断点续传、上传进度条、图片预览
用HTML5实现断点续传、上传进度条、图片预览 · Jul 23, 2014 clicks
再加上File对象和FormData对象就可实现断点续传,
要实现即时预览文件,则需要FileReader对象读取文件绝对地址或内容。
XMLHttpRequest Level 2中的XMLHttpRequest对象有一个属性upload,类型是XMLHttpRequestUpload,监听它的progress事件可以获取上传进度。
同时,send方法的参数可以是ArrayBuffer,
Blob,
的条件就满足了。Document,
FormData,和 string,所以我们可以把文件分片(利用File对象slice方法),把文件以Blob格式上传, 这样断点续传
upload.html代码
<!doctype html> <html> <head> <title>Upload File via XMLHttpRequest 2</title> <script> var fr_supported = typeof FileReader != 'undefined'; var fd_supported = typeof FormData != 'undefined'; if ( !fr_supported ){ alert('you browser doesnt support FileReader!'); } if ( !fd_supported ){ alert('you browser doesnt support FormData!'); } function get(id){ return document.getElementById(id); } var upload_file = null; function fileSelected(){ var file = get('fileToUpload').files[0]; if ( file ){ upload_file = file; var fileSize = 0; if (file.size > 1024*1024){ fileSize = (Math.round(file.size * 100 / (1024 * 1024)) / 100).toString() + 'MB'; }else{ fileSize = (Math.round(file.size * 100 / 1024) / 100).toString() + 'KB'; } get('fileName').innerHTML = file.name; get('fileSize').innerHTML = fileSize; get('fileType').innerHTML = file.type; } } function uploadFile(){ var xhr = new XMLHttpRequest(); var fd = new FormData(); fd.append('uploadfile', upload_file ); xhr.upload.addEventListener("progress", uploadProgress,false); xhr.addEventListener("load", uploadComplete,false); xhr.addEventListener("error", uploadFailed,false); xhr.addEventListener("abort", uploadCanceled, false); xhr.open("POST", "upload.php"); xhr.send(fd); } function uploadProgress(evt){ var percent = 'unable to compute progress'; if (evt.lengthComputable){ var percent = Math.round( (evt.loaded+pos)*100/evt.totalCount ) + '%'; } get('progressNumber').innerHTML = percent; } function uploadComplete(evt){ alert('finish uploading'); } function uploadFailed(evt){ alert('Failed to upload'); } function uploadCanceled(evt){ alert('the upload has been canceled'); } </script> <body> <form action="upload.php" id="form1" method="post" enctype="multipart/form-data"> <div class="row"> <label for="fileToUpload">Select a File to Upload</label> <br /> <input type="file" name="fileToUpload" id="fileToUpload" onchange="fileSelected()" /> </div> <p>FileName: <span id="fileName"></span></p> <p>FileSize: <span id="fileSize"></span></p> <p>FileType: <span id="fileType"></span></p> <p id="preview"></p> <div class="row"> <input type="button" onclick="uploadFile()" value="Upload" /> </div> <p>Progress: <span id="progressNumber"></span></p> </form>
上面的代码只是简单地实现了上传,要想断点续传,首先要获取上次下载到什么地方,所以在上传之前要查询一下这个文件是否上传过,上传了多少。
我们修改一下uploadFile函数,把原来的uploadFile函数改名成do_upload_file
var pos = 0; var slice_size = 1024 * 1024; var upload_data=null; var upload_finished = false; var upload_file_size = 0; var upload_file = null; function uploadFile() { var file = upload_file; upload_file_size = file.size; var xhr = new XMLHttpRequest(); var fd = new FormData(); fd.append('act','query'); fd.append('qry_fname', file.name ); xhr.addEventListener('load',function(stat){ pos = parseInt(this.responseText); if ( pos < upload_file_size){ upload_data = upload_file.slice(pos, pos+slice_size); upload_finished = (pos + slice_size) >= upload_file_size; get('progressNumber').innerHTML = Math.round(pos*100/upload_file_size)+'%'; do_upload_file(); }else{ upload_finished = true; get('progressNumber').innerHTML = '100%'; alert('file already upladed'); } },false); xhr.open("POST",'upload.php'); xhr.send(fd); }
再把do_upload_file函数修改一下:
function do_upload_file(){ if ( upload_data==null ){ alert( 'nothing to upload' ); return; } var xhr = new XMLHttpRequest(); var fd = new FormData(); fd.append('act','upload'); fd.append('uploadfile', upload_data ); fd.append('filename', upload_file.name); fd.append('finished', upload_finished?'1':'0' ); fd.append('position', pos); xhr.upload.addEventListener("progress", uploadProgress,false); xhr.addEventListener("load", uploadComplete,false); xhr.addEventListener("error", uploadFailed,false); xhr.addEventListener("abort", uploadCanceled, false); xhr.open("POST", "upload.php"); xhr.send(fd); }
同时要修改load事件监听函数,上传完一个片段之后自动上传下一个
function uploadComplete(evt){ if ( !upload_finished ){ pos += slice_size; upload_data = upload_file.slice(pos, pos+slice_size); upload_finished = (pos + slice_size) >= upload_file_size; do_upload_file(); } }
这在每次上传开始之前,先用文件名去服务器端查询,服务器返回已上传的字节数pos,然后从文件的pos位置开始,截取slice_size个字节(本例中是1MB)的数据用来上传。
似乎忘了预览文件了? 预览很简单,稍微修改下fileSelected函数,判断下文件类型,如果是图片,就创建一个image对象
function fileSelected(){ var file = get('fileToUpload').files[0]; upload_file = file; pos = 0; upload_data = null ; upload_finished = false; upload_file_hash = ''; if ( file ){ upload_file_size = file.size; var fileSize = 0; if (file.size > 1024*1024){ fileSize = (Math.round(file.size * 100 / (1024 * 1024)) / 100).toString() + 'MB'; }else{ fileSize = (Math.round(file.size * 100 / 1024) / 100).toString() + 'KB'; } get('fileName').innerHTML = file.name; get('fileSize').innerHTML = fileSize; get('fileType').innerHTML = file.type; //preview image if ( file.type.split('/')[0] == 'image' ){ var rd = new FileReader(); rd.addEventListener('load',function(evt){ var im = new Image(); im.src = evt.target.result; get('preview').innerHTML = ''; get('preview').appendChild(im); },false); rd.readAsDataURL(file); } } }
似乎已经大功告成了, 但是要想更严密点,就还得加点东西了: 仅仅用文件名来判断文件是否上传远远不够,文件同名的情况是很常见的,即使再加上文件大小也不行,同一个文件替换掉相同数目的字符,大小是不变的!
那要怎么办呢? 对了, hash校验。
计算hash值的方法,浏览器端的js没有内置,所以在网上找了一个:Spark MD5
现在完整的代码如下:
<!doctype html> <html> <head> <title>Upload File via XMLHttpRequest 2</title> <script src="spark_md5.min.js"></script> <script> var fr_supported = typeof FileReader != 'undefined'; var fd_supported = typeof FormData != 'undefined'; if ( !fr_supported ){ alert('you browser doesnt support FileReader!'); } if ( !fd_supported ){ alert('you browser doesnt support FormData!'); } function get(id){ return document.getElementById(id); } var pos = 0; var slice_size = 1024 * 1024; var upload_data=null; var upload_finished = false; var upload_file_size = 0; var upload_file = null; var upload_file_hash= ''; function fileSelected(){ var file = get('fileToUpload').files[0]; upload_file = file; pos = 0; upload_data = null ; upload_finished = false; upload_file_hash = ''; if ( file ){ upload_file_size = file.size; var fileSize = 0; if (file.size > 1024*1024){ fileSize = (Math.round(file.size * 100 / (1024 * 1024)) / 100).toString() + 'MB'; }else{ fileSize = (Math.round(file.size * 100 / 1024) / 100).toString() + 'KB'; } get('fileName').innerHTML = file.name; get('fileSize').innerHTML = fileSize; get('fileType').innerHTML = file.type; //preview image if ( file.type.split('/')[0] == 'image' ){ var rd = new FileReader(); rd.addEventListener('load',function(evt){ var im = new Image(); im.src = evt.target.result; get('preview').innerHTML = ''; get('preview').appendChild(im); },false); rd.readAsDataURL(file); } } } function uploadFile() { var file = upload_file; //get md5 hash var reader = new FileReader(); reader.addEventListener('load',function(evt){ upload_file_hash = SparkMD5.hash(evt.target.result); console.log(['md5', upload_file_hash ]); //get size of uploaded data var xhr = new XMLHttpRequest(); var fd = new FormData(); fd.append('act','query'); fd.append('qry_fname', file.name ); fd.append('file_hash', upload_file_hash); xhr.addEventListener('load',function(stat){ pos = parseInt(this.responseText); console.log([upload_file,pos,upload_file_size]); if ( pos < upload_file_size){ upload_data = upload_file.slice(pos, pos+slice_size); upload_finished = (pos + slice_size) >= upload_file_size; get('progressNumber').innerHTML = Math.round(pos*100/upload_file_size)+'%'; //upload file do_upload_file(); }else{ upload_finished = true; get('progressNumber').innerHTML = '100%'; alert('file already upladed'); } },false); xhr.open("POST",'upload.php'); xhr.send(fd); }); reader.readAsBinaryString(file); } function do_upload_file(){ if ( upload_data==null ){ alert( 'nothing to upload' ); return; } var xhr = new XMLHttpRequest(); var fd = new FormData(); fd.append('act','upload'); fd.append('uploadfile', upload_data ); fd.append('filename', upload_file.name); fd.append('finished', upload_finished?'1':'0' ); fd.append('position', pos); fd.append('file_hash', upload_file_hash); xhr.upload.addEventListener("progress", uploadProgress,false); xhr.addEventListener("load", uploadComplete,false); xhr.addEventListener("error", uploadFailed,false); xhr.addEventListener("abort", uploadCanceled, false); xhr.open("POST", "upload.php"); xhr.send(fd); } function uploadProgress(evt){ var percent = 'unable to compute progress'; if (evt.lengthComputable){ var percent = Math.round( (evt.loaded+pos)*100/upload_file_size ) + '%'; } get('progressNumber').innerHTML = percent; } function uploadComplete(evt){ //alert( evt.target.responseText ); console.log([evt.target.responseText]); if ( !upload_finished ){ pos += slice_size; upload_data = upload_file.slice(pos, pos+slice_size); upload_finished = (pos + slice_size) >= upload_file_size; do_upload_file(); } } function uploadFailed(evt){ alert('Failed to upload'); } function uploadCanceled(evt){ alert('the upload has been canceled'); } </script> <body> <form action="upload.php" id="form1" method="post" enctype="multipart/form-data"> <div class="row"> <label for="fileToUpload">Select a File to Upload</label> <br /> <input type="file" name="fileToUpload" id="fileToUpload" onchange="fileSelected()" /> </div> <p>FileName: <span id="fileName"></span></p> <p>FileSize: <span id="fileSize"></span></p> <p>FileType: <span id="fileType"></span></p> <p id="preview"></p> <div class="row"> <input type="button" onclick="uploadFile()" value="Upload" /> </div> <p>Progress: <span id="progressNumber"></span></p> </form>
后台程序中以hash加上文件名作为保存文件的文件名字,在后面加".part"表示未完成的文件 upload.php的代码:
<?php if ( $_SERVER['REQUEST_METHOD'] =='POST' ){ $post = $_POST; $hash = $post['file_hash']; if ( !preg_match("/^[a-z0-9]{32}$/i",$hash) ){ exit('invalid hash'); } if( $post['act']=='query' && isset($post['qry_fname']) ){ $dest = 'uploads/'.$hash.'_'. $post['qry_fname']; $pdest = $dest.'.part'; if ( file_exists( $pdest) ){ echo filesize($pdest); }elseif( file_exists($dest)){ echo filesize($dest); }else{ echo 0; } }elseif( $post['act']=='upload'){ $file = $_FILES['uploadfile']; $file_name = $post['filename']; $pos = intval($post['position']); $dest = 'uploads/'.$hash.'_'.$file_name; $pdest = $dest.'.part'; $finished = $post['finished']; if ( file_exists($pdest) ){ if ( filesize($pdest) != $pos ){ exit('Position Not matches filesize'); } } if ( !$finished ){ file_put_contents( $pdest, file_get_contents($file['tmp_name']),FILE_APPEND ); echo 'upload '. round($file['size']/1024).'KB'; } if ( $finished ){ if ( file_exists($pdest) ){ rename($pdest, $dest); }else{ if (! move_uploaded_file($file['tmp_name'], $dest ) ){ echo 'Failed to move file ',$file['tmp_name'] . ' >> ' .$dest ; exit; } } echo 'upload finished.'; } } }
把文件分块上传还有一个好处就是,无需理会服务器的配置了,以前要上传大文件必须修改web服务的配置,默认的配置小得可怜。
值得一说的是,上传时必须把文件名也提交上去,因为这种情况下,$_FILES['upload_file']['name']获取的全部都是BLOB。。。。。
要说遗憾,就是每次计算文件的hash值花时间太长,给人的感觉是点了upload之后会卡顿一会,只能用一个loading提示框来掩盖了,毕竟这个计算量是不可避免的。
最后再提示一点,html5下文件域可以添加一个multiple属性,有这个属性之后,就可以同时选择多个文件了。