要實現文件上傳,要兼顧IE6份額還不小的市場情況,除了fashion的HTML5,SWFUpload還得用作降級方案。
Nicholas有一個系列關於利用HTML5上傳文件的博客“Working with files in JavaScript”,另外,這里還有一篇也很詳細的利用HTML5上傳文件的文章(中文版哦)http://www.html5rocks.com/zh/tutorials/file/dndfiles/#toc-slicing-files
一、利用HTML5進行文件上傳(來自Nicholas)
1、上傳文件,可以點擊按鈕選擇文件上傳;也可以拖拽上傳
插曲:
--------------------------------------------------------------------------------------------------------------------------------------
有一些小問題<input type="file" />在各種瀏覽器中的顯示都不一樣,
chrome:
鼠標放到按鈕“選擇文件”上,就會出現“未選擇文件”的tips,這是chrome的特色
ff:
opera:
所以需要統一下樣式,簡單的處理方式,就是opacity:0(隱藏掉),用單獨的a元素做個按鈕代替(只是樣式上的代替),然后再定位。
-----------------------------------------------------------------------------------------------------------------------------------------------------------------
(1)點擊按鈕選擇文件上傳
1 <input type="file" id="your-files" multiple>//multiple代表可以多文件上傳 2 <script> 3 var control = document.getElementById("your-files"); 4 control.addEventListener("change", function(event) { 5 6 var i = 0, 7 files = control.files, 8 len = files.length; 9 10 for (; i < len; i++) { 11 console.log("Filename: " + files[i].name);//每個文件名 12 console.log("Type: " + files[i].type);//每個文件類型 13 console.log("Size: " + files[i].size + " bytes");//每個文件大小 14 } 15 16 }, false); 17 </script>
(2)拖拽上傳
1 <div id="your-files">上傳文件拖拽到這里</div> 2 <script> 3 var target = document.getElementById("your-files"); 4 5 target.addEventListener("dragover", function(event) { 6 event.preventDefault(); 7 }, false); 8 9 target.addEventListener("drop", function(event) { 10 11 event.preventDefault();//取消默認的行為 12 13 var i = 0, 14 files = event.dataTransfer.files, 15 len = files.length; 16 17 for (; i < len; i++) { 18 console.log("Filename: " + files[i].name); 19 console.log("Type: " + files[i].type); 20 console.log("Size: " + files[i].size + " bytes"); 21 } 22 23 }, false); 24 </script>
2、上傳到瀏覽器的文件,可以利用ajax方式上傳到服務器
通過FormData對象(在XMLHttpRequest Level 2中定義)可以用 Ajax方式對文件上傳。該對象代替了HTML的form表單,允許你通過append()方法增加key-value的形式對上傳到服務器。
1 var form = new FormData(); 2 //增加的標識 3 form.append("name", "Nicholas"); 4 form.append("photo", control.files[0]); 5 6 // 通過XHR上傳 - 沒有設頭信息哦! 7 var xhr = new XMLHttpRequest(); 8 xhr.onload = function() { 9 console.log("Upload complete."); 10 }; 11 xhr.open("post", "/entrypoint", true); 12 xhr.send(form);
發post請求的效果,其中可以看到加的name與photo, 至於其中filename亂碼的問題,還不知道是編碼的原因呢?(頁面用的UTF-8)還是什么?
3、讀取文件
在客戶端我們就可以通過FileReader來讀取文件內容。
有四種讀文件的方式:
(1)readAsText()---用text的方式返回文件(txt文件格式,讀取沒問題,但是doc這類有自己格式的文件,出來就會是亂碼)
(2)readAsBinaryString()---以二進制編碼的方式返回文件(已過時了,現在用readAsArrayBuffer)
(3)readAsArrayBuffer()---用一個ArrayBuffer返回文件內容(對二進制文件很有用,比如圖片)
(4)readAsDataURL()---用data URL 返回文件內容
所有的這些方法啟動讀取文件,都類似於XHR對象的send方法啟動一個HTTP請求。所以需要在開始讀取文件之前監聽load事件。
1 var reader = new FileReader(); 2 reader.onload = function(event) { 3 var contents = event.target.result; 4 //輸出文件的內容 5 console.log("File contents: " + contents); 6 }; 7 8 reader.onerror = function(event) { 9 console.error("File could not be read! Code " + event.target.error.code); 10 }; 11 12 reader.readAsText(file);
下面用讀取URI的方式來讀取圖片,並利用canvas,設置它的src屬性,使得圖片在瀏覽器中呈現出來:
<canvas id="mycanvas">這里會顯示上傳的照片</canvas>
<script>
var reader = new FileReader(); reader.onload = function(event) { var dataUri = event.target.result, context = document.getElementById("mycanvas").getContext("2d"), img = new Image(); // 等到圖片被完全加載 img.onload = function() { context.drawImage(img, 100, 100); }; img.src = dataUri; }; reader.onerror = function(event) { console.error("File could not be read! Code " + event.target.error.code); }; reader.readAsDataURL(file);//這里的file可以利用上面提到的上傳文件的方式得到,比如前面的control.files[0](單個文件)
</script>
4、獲取上傳文件的進度
在FileReader過程中有5個事件:
(1)loadstart--表示加載數據開始。這個事件總是在最開始執行
(2)progress--當數據在加載過程中時觸發,使得能夠訪問中間的數據
(3)error--當加載失敗時觸發
(4)abort---當數據加載過程被取消時觸發(在XMLHttpRequest和FileReader中都能用)
(5)load---當所有的數據都成功讀取以后觸發
(6)loadend---當對象已經完成傳輸數據以后觸發,總是在error,abort和load之后觸發
error和load在 3、讀取文件的例子中已經運用了。
在文件上傳過程中,獲取文件的進度信息:
(1)lengthComputable---布爾值,指示瀏覽器能否決定數據完整的大小
(2)loaded---已經讀取的文件bytes值
(3)total---需要讀取的文件的bytes值
例子如下:
1 var reader = new FileReader(), 2 progressNode = document.getElementById("my-progress"); 3 4 reader.onprogress = function(event) { 5 if (event.lengthComputable) { 6 progressNode.max = event.total;//精度的最大值 7 progressNode.value = event.loaded;//進度值 8 } 9 }; 10 11 reader.onloadend = function(event) { 12 var contents = event.target.result, 13 error = event.target.error; 14 15 if (error != null) { 16 console.error("File could not be read! Code " + error.code); 17 } else { 18 progressNode.max = 1; 19 progressNode.value = 1; 20 console.log("Contents: " + contents); 21 } 22 }; 23 24 reader.readAsText(file);
根據loaded的進度值,就可以設置元素的背景色等,得到常見的進度條的模樣。
5、處理文件錯誤信息
就算是上傳本地文件,也會有出錯的情況,在File API的說明中,定義了四種類型的文件錯誤。當在文件讀取中發生錯誤時,FileReader對象錯誤屬性將會指示四種類型錯誤中的一種。在實際中,瀏覽器用 FileError對象實現這些文件錯誤屬性:
(1)FileError.NOT_FOUND_ERR 文件沒有找到
(2)FileError.SECURITY_ERR 安全問題
(3)FileError.NOT_READABLE_ERR 不可讀錯誤
(4)FileError.ENCODING_ERR 編碼錯誤
(5)FileError.ABORT_ERR 當在過程中沒有文件讀取時,調用abort()
1 var reader = new FileReader(); 2 3 reader.onloadend = function(event) { 4 var contents = event.target.result, 5 error = event.target.error; 6 7 if (error !== null) { 8 switch (error.code) { 9 case error.ENCODING_ERR: 10 console.error("Encoding error!"); 11 break; 12 13 case error.NOT_FOUND_ERR: 14 console.error("File not found!"); 15 break; 16 17 case error.NOT_READABLE_ERR: 18 console.error("File could not be read!"); 19 break; 20 21 case error.SECURITY_ERR: 22 console.error("Security issue with file!"); 23 break; 24 25 default: 26 console.error("I have no idea what's wrong!"); 27 } 28 } else { 29 progressNode.max = 1; 30 progressNode.value = 1; 31 console.log("Contents: " + contents); 32 } 33 }; 34 35 reader.readAsText(file);
6、利用Object URL實現從本地讀取文件到瀏覽器中顯示
Object URL是指向硬盤上文件的URL。假設,你需要在用戶自己的系統中展現圖片;而服務器根本不需要知道這個文件,所以就沒有必要上傳它。在之前的介紹中需要得到這個File對象的引用,讀取數據到data URI 中,然后在 <img>元素中展現出來。但是仔細想想,圖片已經在硬盤中存在了,為什么還要讀取圖片到另外一種形式然后來用它?如果創建一個object URL,你可以把它指派給<img>,直接從本地得到這個文件。
在File API中定義了一個全局的對象叫URL,有兩個方法。
createObjectURL(),接收一個File的引用,返回一個對象URL。這個函數讓瀏覽器創建和管理一個本地文件的URL。
reovkeObjectURL(),讓瀏覽器銷毀URL,有效的釋放內存。當然所有的對象URLs都會被釋放一旦網頁被銷毀的時候,但利用這個函數釋放內存是好的編碼方式,反正我們也不再需要這些對象URL。
現在瀏覽器對對象URL這部分的支持沒有File API的其他部分支持的好。 現在Internet Explorer 10+ 和 Firefox 9+ 支持,chrome支持自己的webkitURL,Safari和Opera都不支持。
1 var URL = window.URL || window.webkitURL, 2 imageUrl, 3 image; 4 5 if (URL) { 6 imageUrl = URL.createObjectURL(file); 7 image = document.createElement("img"); 8 9 image.onload = function() { 10 URL.revokeObjectURL(imageUrl); 11 }; 12 13 image.src = imageUrl; 14 document.body.appendChild(image); 15 }
乍一看,貌似很強大,URL本身還不是最大的安全問題,因為URL是動態綁定到瀏覽器上的,在其他機器上就沒用了。但是如果跨域呢?
File API不允許在不同的域中使用對象URL。當一個URL被創建,它就被綁定到執行這個JS的網頁的域中,所以在www.wrox.com中使用的對象URL在prp.wrox.com中不能使用(產生error)。然而,兩個都來自www.wrox.com的頁面,其中一個用iframe的方式嵌入到另外一個中,就可以使用同一個對象URL。
對象URL只在文檔創建它們時有效。當文檔卸載,它們就都釋放掉了。所以,不用擔心把URL存儲在本地的,以后使用;當頁面卸載以后,它們就無效了。
你可以在任何地方使用對象URL,瀏覽器會發出一個GET請求,包括images, scripts, web workers, style sheets, audio, and video。當瀏覽器執行POST請求時,比如form表單中設置為post方法時,將不能使用對象URL。
7、利用Blobs分割文件
在字符串和數組中,你可能對slice()很熟悉,Blobs的作用有點類似 slice()。Blobs接收三個參數:開始的字節位移,結束的字節位移,一個可選的MIME類型。如果沒有指定MIME類型,新的Blob的數據類型與之前的一樣。
每一個Blob都只是代表對數據的指示,而不是數據本身,所以可以快速的創建新的Blob指向其他的子部分。需要利用slice()方法來實現。
瀏覽器對slice()方法的支持不是很統一,FF中是通過mozSlice()來支持的,chrome通過webkitSlice()支持。其他瀏覽器暫時不支持。例子:
function sliceBlob(blob, start, end, type) { type = type || blob.type; if (blob.mozSlice) { return blob.mozSlice(start, end, type); } else if (blob.webkitSlice) { return blob.webkitSlice(start, end type); } else { throw new Error("This doesn't work!"); } }
比如,你可以使用這個函數來分割一個大文件並用塊的方式上傳。每一個新的Blob都是獨立的,即使它們之間的數據有交叉。
(1)用老方法創建 Blobs。
當文件對象在瀏覽器中出現,開發者意識到Blob對象非常有用,所以就想着能夠不通過用戶的交互來創建它。畢竟,任何數據都可以用Blob表達,而不用綁定到文件。瀏覽器很快就做出“響應”,創建BlobBuilder,它的目的就是為了在Blob對象中包裹數據。這不是一個標准的類型,在不同瀏覽器中都有不同的實現。ff(MozBlobBuilder),chrome(WebKitBlobBuilder),ie 10(MSBlobBuilder)。
BlobBuilder通過創建一個新對象,然后調用append方法,傳一個字符串或者ArrayBuffer或者Blob。一旦所有的數據被加載,調用getBlob()方法,傳入一個MIME類型。
例子:
1 var builder = new BlobBuilder(); 2 builder.append("Hello world!"); 3 var blob = builder.getBlob("text/plain");
例如,你可以用一個Blob創建一個網絡線程而不需要為這個線程代碼寫一份單獨的文件。這種技術已經存在於基本的網絡線程中。
1 // Prefixed in Webkit, Chrome 12, and FF6: window.WebKitBlobBuilder, window.MozBlobBuilder(在ff中用MozBlobBuilder,在chrome中用WebKitBlobBuilder 2 var bb = new BlobBuilder(); 3 bb.append("onmessage = function(e) { postMessage('msg from worker'); }"); 4 5 // Obtain a blob URL reference to our worker 'file'. 6 // 注意: window.webkitURL.createObjectURL() in Chrome 10+. 7 var blobURL = window.URL.createObjectURL(bb.getBlob()); 8 9 var worker = new Worker(blobURL); 10 worker.onmessage = function(e) {
console.log(e.data);//msg from worker 11 // e.data == 'msg from worker' 12 }; 13 worker.postMessage('',''); // Start the worker. 需要傳參數
上面的代碼創建了一個簡單的script腳本,然后創建了一個對象URL。對象URL被分配給一個網絡線程worker.
你可以調用append()任何次數來構建Blob的內容。
(2)用新方法創建Blob
用Blob構造器來創建。第一個參數就是 老方法中append()里面的東西,第二個參數是一個對象,包括新創建的Blob的屬性。目前有兩個屬性的定義,指向Blob的MIME類型和結尾(可以是transparent默認或者是native)
1 var blob = new Blob(["Hello world!"], { type: "text/plain" });
這種方法暫時只在chrome中支持。ff13將會支持。
結束:
使用這些技術,你可以在圖片上傳前,縮放圖片的大小(使用FileReader和canvas);你可以創建一個純瀏覽器端的文件編輯器;你可以分割大的文件一塊一塊的上傳。這些可能性都是無止境的,它們已經離我們越來越近了。