用HTML5实现断点续传、上传进度条、图片预览
用HTML5实现断点续传、上传进度条、图片预览 · July 23, 2014 views: 458
再加上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属性,有这个属性之后,就可以同时选择多个文件了。