HTML5 Drag&Drop + 純AJAX檔案上傳
Fri, 08 Oct 2010 14:50:16 +0800(有一些基本說明在前一篇文章:「一個簡單的HTML5 Drag & Drop試作」裡面提過了,這裡就不在重複了。)
除了在頁面之內使用拖拉,HTML5更強的地方,在與其他應用軟體間也能拖來拉去!這主要是靠DataTransfer.files屬性達成的。DataTransfer.files其實是一個FileList物件,而FileList則是包含了File物件的List...(廢話),這幾個東西,其實不是定義在HTML5裡面,而屬於File API。
當從其他應用軟體或作業系統拖曳檔案進來時,dataTransfer.types裡面會有一個值'Files',這時相對地在dataTransfer.files裡面就會有File物件,裡面有拖曳的檔案資料。要讀取File物件,則需透過FileReader物件,這些都是屬於File API裡面定義的物件。先來看看這些物件的定義及特性:
FileList是容納File物件的Sequence,只要利用跟Array一樣的方式([index])來操作就可以,不過他並沒有Array物件所擁有的方法及特性,所以不能搞混了:
typedef sequence<File> FileList;
File物件繼承自Blob物件:
interface Blob { readonly attribute unsigned long long size; //slice Blob into byte-ranged chunks Blob slice(in long long start, in long long length); // raises DOMException };
File物件自己的定義:
interface File : Blob { readonly attribute DOMString name; readonly attribute DOMString type; readonly attribute DOMString urn; };
透過File.size可以知道這個檔案長度是多少bytes,透過File.name可以取得檔名,透過File.type可以知道檔案的MIME type。File.urn理論上可以用來取得檔案的URN,不過我還不知道要怎麼使用比較好,先跳過他。要存取檔案的話,還需要透過FileReader物件:
[Constructor] interface FileReader { // async read methods void readAsBinaryString(in Blob fileBlob); void readAsText(in Blob fileBlob, [Optional] in DOMString encoding); void readAsDataURL(in File file); void abort(); // states const unsigned short EMPTY = 0; const unsigned short LOADING = 1; const unsigned short DONE = 2; readonly attribute unsigned short readyState; // file data readonly attribute DOMString result; readonly attribute FileError error; // event handler attributes attribute Function onloadstart; attribute Function onprogress; attribute Function onload; attribute Function onabort; attribute Function onerror; attribute Function onloadend; }; FileReader implements EventTarget;
FileReader繼承了EventTarget,這是DOM3 Event裡面定義的介面,有了他,才能提供addEventListener。FileReader提供了三個讀取資料的函數,readAsBinaryString可以將資料讀取為以byte組成的字串,readAsText可以依照提供的format資訊來將資料讀取為Text,readAsDataURL則可以將資料讀取為DataURL的格式。當在瀏覽器中使用時,這些操作方法都是非同步的,我們需要透過onprogress事件在他每讀取一段資料時透過FileReader.result來取得讀取的資料;或是透過onload事件在所有資料讀取完畢後,透過FileReader.result取得讀取的資料。
如果是圖片檔,透過DataURL,把DataURL指定為img的src屬性,就可以做出讀取後立刻預覽的效果。除了透過拖曳來取得檔案資料,其實在HTML5中,input type=file也有files屬性,可以透過他來做上傳檔案預覽,不過這不是這個測試的重點。接下來看一下測試程式,這部份包含一個php程式,用來把上傳的base64編碼的檔案解碼。至於上傳檔案為什麼是base64編碼...因為XMLHttpRequest的send(),會把資料轉成utf8...另外兩部份是網頁以及負責用multipart/form-data格式上傳的Javascript。
先來看網頁:
<html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <style> span#panel { display: inline-block; background: #336699; width: 140px; height: 180px; border: solid black 2px; border-radius: 10px; padding: 5px; text-align: center; color: white; vertical-align: middle; } span { display: inline-block; vertical-align: middle; } div.msg { display: block; background: #6699CC; width: 180px; height: 50px; border: solid black 2px; border-radius: 10px; padding: 5px; text-align: center; color: white; margin: 1px; vertical-align: middle; } div#container { vertical-align: baseline; border: solid 1px white; padding: 5px; text-align: center; } </style> <script src="fwajax3.js"></script> </head> <body> <div style="text-align:center"><h4>HTML5 Drag&Drop with pure AJAX file upload test.</h4></div> <div id="container"><span id="panel">Drop image here.<br></span> <span><div class="msg" id="msg1"></div><div class="msg" id="msg2"></div><div class="msg" id="msg3"></div></span></div> </body> </html> <script> document.ondragenter = function(e){e.preventDefault();} document.ondragover = function(e){e.preventDefault();} document.getElementById('panel').addEventListener('dragenter', function(e){ e.preventDefault(); if(window.console) console.log('dragenter'); e.effectAllowed = ['move']; },false); document.getElementById('panel').addEventListener('dragover', function(e){ e.preventDefault(); if(window.console) console.log('dragover'); e.dataTransfer.dropEffect = 'move'; },false); document.getElementById('panel').addEventListener('drop', function(e){ e.preventDefault(); e.dataTransfer.dropEffect = 'move'; if(window.console) console.log('drop'); if(window.console) console.log(e.dataTransfer); if(e.dataTransfer.types && e.dataTransfer.types.length>0) { for(var i=0; i<e.dataTransfer.types.length; i++) { if(window.console) console.log(e.dataTransfer.types[i]); if(e.dataTransfer.types[i]!=='Files') { if(window.console) console.log(e.dataTransfer.getData(e.dataTransfer.types[i])); } } } //can't get FileList in Chrome7... and Firefox4 didn't follow the FileAPI standard... if(e.dataTransfer.files.length>0) { for(var i=0; i<e.dataTransfer.files.length; i++) { switch(e.dataTransfer.files[i].type) { case 'image/jpeg': case 'image/gif': case 'image/png': case 'image/bmp': document.getElementById('msg1').innerHTML = 'File name: '+e.dataTransfer.files[i].name; document.getElementById('msg2').innerHTML = ''; document.getElementById('msg3').innerHTML = ''; setTimeout(function(blob){ return function() { var fileReader = new FileReader(); fileReader.onload = function() { if(window.console) console.log('got an image file.'); //if(window.console) console.log(this.result); var uploader = new fwH5AjaxUploader( 'save_ajax.php', function(_txt,_xml) { if(window.console) console.log(_txt); var msg = JSON.parse(_txt); if(msg.state == 'success') { document.getElementById('msg3').innerHTML = 'File uploaded: '+blob.name; var img = document.createElement('img'); img.src = msg.path; img.width = '110'; img.style = "text-align:center"; document.getElementById('panel').innerHTML = 'Drop Here.<p>'; document.getElementById('panel').appendChild(img); img = null; } } ); uploader.addFile( 'fileupload', {'name':blob.name,'type':blob.type,'data':this.result.slice(this.result.indexOf(",")+1)}, 'base64' ); document.getElementById('msg2').innerHTML = 'Start uploading: '+blob.name; uploader.send(); }; try { fileReader.readAsDataURL(blob); } catch(e) { alert(e); } }; }(e.dataTransfer.files[i]),100); break; default: alert('Only image file allowed.\nIncluding: jpeg, png, gif and bmp.'); break; } } } },false); </script>
再來是負責上傳的Javascript程式,就是網頁中可以看到的fwajax3.js:
function fwH5AjaxUploader(host, cb) { var req = function() { try{ return new ActiveXObject("Msxml2.XMLHTTP.6.0") }catch(e){} try{ return new ActiveXObject("Msxml2.XMLHTTP.3.0") }catch(e){} try{ return new ActiveXObject("Msxml2.XMLHTTP") }catch(e){} try{ return new ActiveXObject("Microsoft.XMLHTTP") }catch(e){} try{ return new XMLHttpRequest();} catch(e){} return null; }(), boundary = function () { var tmp = Math.random(); var thisDate = new Date(); tmp = Math.abs(tmp*thisDate.getTime()); tmp = "--------" + tmp + "--------"; return tmp; }(), //the format of _value argument should be {'name':'filename','type':'file mine type string', 'data':'file data in Data URL format'} createFile = function (_field, _value, _encoding) { var tmp = "--" + boundary + CRLF; tmp += "Content-Disposition: attachment; name=\"" + _field + "\"; filename=\"" + _value.name + "\"" + CRLF; tmp += "Content-Type: " + _value.type + CRLF; tmp += "Content-Transfer-Encoding: " + _encoding + CRLF + CRLF; tmp += _value.data + CRLF; tmp += "--" + boundary + "--" + CRLF + CRLF; return tmp; }, createField = function (_field, _value) { var tmp = "--" + boundary + CRLF; tmp += "Content-Disposition: form-data; name=\"" + _field + "\"" + CRLF; tmp += "Content-Transfer-Encoding: binary" + CRLF + CRLF; tmp += _value + CRLF tmp += "--" + boundary + "--" + CRLF + CRLF; return tmp; }, fields = [], files = [], CRLF = "\r\n"; req.onreadystatechange = function() { if(req.readyState == 4) { if(req.status == 200) { cb(req.responseText, req.responseXML); } } }; this.addFile = function(_field, _value, _encoding) { files.push([_field, _value, _encoding]); }; this.removeFile = function(_field) { for (var i=0; i<files.length; i++) { if (files[i][0] == _field) { files.splice(i,1); } } }; this.clearFile = function() { files = []; }; this.addField = function(_field, _value) { fields.push([_field, _value]); }; this.removeField = function(_field) { for (var i=0; i<fields.length; i++) { if (fields[i][0] == _field) { fields.splice(i,1); } } }; this.clearField = function () { fields = []; }; this.send = function() { req.open("POST", host, true); var msgBody = ""; for (var i=0; i<fields.length; i++){ msgBody += createField(fields[i][0], fields[i][1]); } for (var i=0; i<files.length; i++){ msgBody += createFile(files[i][0], files[i][1], files[i][2]); } req.setRequestHeader("Content-Type","multipart/form-data; boundary="+boundary); // req.setRequestHeader("Connection","Keep-Alive"); // req.setRequestHeader("Content-Length",msgBody.length); req.send(msgBody); }; }後面幾個setRequestHeader因為在Chrome7會出現警告訊息而無法執行,所以乾脆拿掉。不過看起來拿掉也可以正確上傳檔案就是了(這是用我很久以前寫的東西改的,當時這樣改header是允許的)。
最後就是接收上傳資料的php程式:
<?php $uploaddir = 'upload/'; $uploadfile = $uploaddir . $_FILES['fileupload']['name']; header('Content-type: text/json'); if (move_uploaded_file($_FILES['fileupload']['tmp_name'], $uploadfile)) { $str = file_get_contents($uploadfile); $str = base64_decode($str); file_put_contents($uploadfile, $str); echo "{\"state\":\"success\",\"path\":\"$uploadfile\"}"; } else { echo "{\"state\":\"fail\",\"path\":null}"; } ?>
目前測試過,在Chrome7跟Firefox4上面都可以正常執行,只是...Firefox4不支援border-radius,所以圓角沒出來,看起來會比較醜。接下來看一下在Chrome7上面執行的畫面:
剛打開網頁的畫面 |
拖曳圖檔上傳完畢後的畫面 |
圖檔上傳完畢後,會自動建立一個img,src指向上傳的檔案的URL。由於檔案上傳會比較花時間,如果等不及而另外拖拉檔案上去的話,畫面可能會不一致,例如右方顯是的是最後拖拉檔案的資訊,但是左側的圖片顯示的是最後上傳完畢的圖片。另外,這些程式都只是做概念驗證,所以沒有考慮到安全...要試用的話請自己注意安全。還有,上傳檔案時,檔案會先讀進記憶體,而Javascript GC不是那麼頻繁的話,所以瀏覽器會吃掉不少記憶體。