歡迎大家關注騰訊雲技術社區-博客園官方主頁,我們將持續在博客園為大家推薦技術精品文章哦~
作者:譚偉華
導語
作為一枚初入鵝廠的鮮鵝,對這里的一切都充滿着求知欲。看到我們的KM平台如此生機勃勃,各種技術分享交流如火如荼,在努力的汲取着養分的同時也期待自己能為這個生態圈做出貢獻。正好新人導師讓我看看能否把產品目前使用的FileUploader從老的組件庫分離出來的,自己也查閱了相關的各種資料,對文件上傳的這些事有了更進一步的了解。把這些知識點總結一下,供自己日后回顧,也供有需要的同學參考,同時也歡迎各位大牛拍磚指點共同學習。
FileUpload 對象
在網頁上傳文件,最核心元素就是這個HTML DOM的FileUpload對象了。什么鬼?好像不太熟啊~別急,看到真人就熟了:
<input type="file">
就是他啊!其實在 HTML 文檔中該標簽每出現一次,一個 FileUpload 對象就會被創建。該標簽包含一個按鈕,用來打開文件選擇對話框,以及一段文字顯示選中的文件名或提示沒有文件被選中。
把這個標簽放在<form>
標簽內,設置form的action為服務器目標上傳地址,並點擊submit按鈕或通過JS調用form的submit()方法就可以實現最簡單的文件上傳了。
<form id="uploadForm" method="POST" action="upload" enctype="multipart/form-data"> <input type="file" id="myFile" name="file"></input> <input type="submit" value="提交"></input> </form>
這樣就完成功能啦?沒錯。但是你要是敢提交這樣的代碼,估計臉要被打腫
都什么年代了,我們要的是頁面無刷新上傳!
更優雅的上傳
現代網頁通過什么來實現用戶與服務器的無刷新交互?
——XMLHttpRequest
對,就是這個你很熟悉的家伙。如果你開發的產品支持的瀏覽器是現代瀏覽器,那么恭喜你,文件上傳就是這么easy!特別強調強調現代瀏覽器是因為我們接下來討論的XMLHttpRequest指的是XMLHttpRequest Level 2。
那什么是Level 1?為什么不行?因為它有如下限制:
-
僅支持文本數據傳輸, 無法傳輸二進制數據.
-
傳輸數據時, 沒有進度信息提示, 只能提示是否完成.
-
受瀏覽器 同源策略 限制, 只能請求同域資源.
-
沒有超時機制, 不方便掌控ajax請求節奏.
而XMLHttpRequest Level 2針對這些缺陷做出了改進:
-
支持二進制數據, 可以上傳文件, 可以使用FormData對象管理表單.
-
提供進度提示, 可通過 xhr.upload.onprogress 事件回調方法獲取傳輸進度.
-
依然受 同源策略 限制, 這個安全機制不會變. XHR2新提供 Access-Control-Allow-Origin 等headers, 設置為 * 時表示允許任何域名請求, 從而實現跨域CORS訪問(有關CORS詳細介紹請耐心往下讀).
-
可以設置timeout 及 ontimeout, 方便設置超時時長和超時后續處理.
關於XMLHttpRequest的細節就不在這里贅述了,有興趣可以移步這篇博客。目前, 主流瀏覽器基本上都支持XHR2, 除了IE系列需要IE10及更高版本. 因此IE10以下是不支持XHR2的.
上面提到的FormData就是我們最常用的一種方式。通過在腳本里新建FormData對象,把File對象設置到表單項中,然后利用XMLHttpRequest異步上傳到服務器:
<form id="uploadForm" method="POST" action="upload" enctype="multipart/form-data"> <input type="file" id="myFile" name="file"></input> <input type="submit" value="提交"></input> </form>
完成最基本的需求無法滿足我們對用戶體驗的追求,所以我們還想要支持上傳進度顯示和上傳圖片預覽。
上傳進度
因為是XMLHttpRequest Level 2, 所以很容易就可以支持對上傳進度的監聽。細心地小伙伴會發現在chrome的developer tools的console里new一個XHR對象,調用點運算符就可以看到智能提示出來一個onprogress事件監聽器,那是不是我們只要綁定XHR對象的progress事件就可以了呢?
很接近了,但是XHR對象的直屬progress事件並不是用來監聽上傳資源的進度的。XHR對象還有一個屬性upload, 它返回一個XMLHttpRequestUpload 對象,這個對象擁有下列下列方法:
-
onloadstart
-
onprogress
-
onabort
-
onerror
-
onload
-
ontimeout
-
onloadend
這些方法在XHR對象中都存在同名版本,區別是后者是用於加載資源時,而前者用於資源上傳時。其中onprogress 事件回調方法可用於跟蹤資源上傳的進度,它的event參數對象包含兩個重要的屬性loaded和total。分別代表當前已上傳的字節數(number of bytes)和文件的總字節數。比如我們可以這樣計算進度百分比:
xhr.upload.onprogress = function(event) { if (event.lengthComputable) { var percentComplete = (event.loaded / event.total) * 100; // 對進度進行處理 } }
其中事件的lengthComputable屬性代表文件總大小是否可知。如果 lengthComputable 屬性的值是 false,那么意味着總字節數是未知並且 total 的值為零。
如果是現代瀏覽器,可以直接配合HTML5提供的
<progress id="myProgress" value="50" max="100"> </progress>
其value屬性綁定上面代碼中的percentComplete的值即可。再進一步我們還可以對<progress>
的樣式統一調整,實現優雅降級方案,具體參見這篇文章。
再說說我在測試這個progress事件時遇到的一個問題。一開始我設在onprogress事件回調里的斷點總是只能走到一次,並且loaded值始終等於total。覺得有點詭異,改用console.log打印loaded值不見效,於是直接加大上傳文件的大小到50MB,終於看到了5個不同的百分比值。
因為xhr.upload.onprogress在上傳階段(即xhr.send()之后,xhr.readystate=2之前)觸發,每50ms觸發一次。所以文件太小網絡環境好的時候是直接到100%的。
圖片預覽
普通青年的圖片預覽方式是待文件上傳成功后,后台返回上傳文件的url,然后把預覽圖片的img元素的src指向該url。這其實達不到預覽的效果和目的。
屬於文藝青年的現代瀏覽器又登場了:“使用HTML5的FileReader API吧!” 讓我們直接上代碼,直奔主題:
function handleImageFile(file) { var previewArea = document.getElementById('previewArea'); var img = document.createElement('img'); var fileInput = document.getElementById("myFile"); var file = fileInput.files[0]; img.file = file; previewArea.appendChild(img); var reader = new FileReader(); reader.onload = (function(aImg) { return function(e) { aImg.src = e.target.result; } })(img); reader.readAsDataURL(file); }
這里我們使用FileReader來處理圖片的異步加載。在創建新的FileReader對象之后,我們建立了onload函數,然后調用readAsDataURL()開始在后台進行讀取操作。當圖像文件加載后,轉換成一個 data: URL,並傳遞到onload回調函數中設置給img的src。
另外我們還可以通過使用對象URL來實現預覽
var img = document.createElement("img"); img.src = window.URL.createObjectURL(file);; img.onload = function() { // 明確地通過調用釋放 window.URL.revokeObjectURL(this.src); } previewArea.appendChild(img);
多文件支持
什么?一個一個添加文件太煩?別急,打開一個開關就好了。別忘了我們文章一開頭就登場的FileUpload對象,它有一個multiple屬性。只要這樣
<input id="myFile" type="file" multiple>
我們就能在打開的文件選擇對話框中選中多個文件了。然后你在代碼里拿到的FileUpload對象的files屬性就是一個選中的多文件的數組了。
var fileInput = document.getElementById("myFile"); var files = fileInput.files; var formData = new FormData(); for(var i = 0; i < files.length; i++) { var file = files[i]; formData.append('files[]', file, file.name); }
FormData的append方法提供第三個可選參數用於指定文件名,這樣就可以使用同一個表單項名,然后用文件名區分上傳的多個文件。這樣也方便前后台的循環操作。
二進制上傳
有了FileReader,其實我們還有一種上傳的途徑,讀取文件內容后直接以二進制格式上傳。
var reader = new FileReader(); reader.onload = function(){ xhr.sendAsBinary(this.result); } // 把從input里讀取的文件內容,放到fileReader的result字段里 reader.readAsBinaryString(file);
不過chrome已經把XMLHttpRequest的sendAsBinary方法移除了。所以可能得自行實現一個
XMLHttpRequest.prototype.sendAsBinary = function(text){ var data = new ArrayBuffer(text.length); var ui8a = new Uint8Array(data, 0); for (var i = 0; i < text.length; i++){ ui8a[i] = (text.charCodeAt(i) & 0xff); } this.send(ui8a); }
這段代碼將字符串轉成8位無符號整型,然后存放到一個8位無符號整型數組里面,再把整個數組發送出去。
到這里,我們應該可以結合業務需求實現一個比較優雅的文件上傳組件了。等等,哪里優雅了?都不支持拖拽!
拖拽的支持
利用HTML5的drag & drop事件,我們可以很快實現對拖拽的支持。首先我們可能需要確定一個允許拖放的區域,然后綁定相應的事件進行處理。看代碼
var dropArea; dropArea = document.getElementById("dropArea"); dropArea.addEventListener("dragenter", handleDragenter, false); dropArea.addEventListener("dragover", handleDragover, false); dropArea.addEventListener("drop", handleDrop, false); // 阻止dragenter和dragover的默認行為,這樣才能使drop事件被觸發 function handleDragenter(e) { e.stopPropagation(); e.preventDefault(); } function handleDragover(e) { e.stopPropagation(); e.preventDefault(); } function handleDrop(e) { e.stopPropagation(); e.preventDefault(); var dt = e.dataTransfer; var files = dt.files; // handle files ... }
這里可以把通過事件對象的dataTransfer拿到的files數組和之前相同處理,以實現預覽上傳等功能。有了這些事件回調,我們也可以在不同的事件給我們UI元素添加不同的class來實現更好交互效果。
好了,一個比較優雅的上傳組件可以進入生產模式了。什么?還要支持IE9?好吧,讓我們來看看IE10以下的瀏覽器如何實現無刷新上傳。
借用iframe
之前說了要實現文件上傳使用FileUpload對象()即可。這在低版本的IE里也是適用的。那我們為什么還要用iframe呢?
因為在現代瀏覽器中我們可以用XMLHttpRequest Level 2來支持二進制數據,異步文件上傳,並且動態創建FormData。而低版本的IE里的XMLHttpRequest是Level 1。所以我們通過XHR異步向服務器發上傳請求的路走不通了。只能老老實實的用form的submit。
而form的submit會導致頁面的刷新。原因分析好了,那么答案就近在咫尺了。我們能不能讓form的submit不刷新整個頁面呢?答案就是利用iframe。把form的target指定到一個看不見的iframe,那么返回的數據就會被這個iframe接受,於是乎就只有這個iframe會刷新。而它又是看不見的,用戶自然就感知不到了。
window.__iframeCount = 0; var hiddenframe = document.createElement("iframe"); var frameName = "upload-iframe" + ++window.__iframeCount; hiddenframe.name = frameName; hiddenframe.id = frameName; hiddenframe.setAttribute("style", "width:0;height:0;display:none"); document.body.appendChild(hiddenframe); var form = document.getElementById("myForm"); form.target = frameName;
然后響應iframe的onload事件,獲取response
hiddenframe.onload = function(){ // 獲取iframe的內容,即服務返回的數據 var resData = this.contentDocument.body.textContent || this.contentWindow.document.body.textContent; // 處理數據 。。。 //刪除iframe setTimeout(function(){ var _frame = document.getElementById(frameName); _frame.parentNode.removeChild(_frame); }, 100); }
iframe的實現大致如此,但是如果文件上傳的地址與當前頁面不在同一個域下就會出現跨域問題。導致iframe的onload回調里的訪問服務返回的數據失敗。
這時我們再祭出JSONP這把利劍,來解決跨域問題。首先在上傳之前注冊一個全局的函數,把函數名發給服務器。服務器需要配合在response里讓瀏覽器直接調用這個函數。
// 生成全局函數名,避免沖突 var CALLBACK_NAME = 'CALLBACK_NAME'; var genCallbackName = (function () { var i = 0; return function () { return CALLBACK_NAME + ++i; }; })(); var curCallbackName = genCallbackName(); window[curCallbackName] = function(res) { // 處理response 。。。 // 刪除iframe var _frame = document.getElementById(frameName); _frame.parentNode.removeChild(_frame); // 刪除全局函數本身 window[curCallbackName] = undefined; } // 如果已有其他參數,這里需要判斷一下,改為拼接 &callback= form.action = form.action + '?callback=' + curCallbackName;
好了,實現一個文件上傳組件的基本知識點大致總結了一下。在這些基礎知識之上我們開始可以為我們的業務開發各種酷炫的File Uploader了。在之后的開發中會把相關的更細的知識點也總結進來,不足之處也歡迎大家指正。
【有獎討論】程序員,怎么應對三十歲? 點擊查看詳情
歡迎加入QQ群:374933367,與騰雲閣原創作者們一起交流,更有機會參與技術大咖的在線分享!
相關閱讀
Node Server零基礎——開發環境文件自動重載
Nginx + Lua搭建文件上傳下載服務
【小程序碼 - 設計篇】菊花綻放
此文已由作者授權騰訊雲技術社區發布,轉載請注明文章出處
原文鏈接:https://www.qcloud.com/community/article/985614
獲取更多騰訊海量技術實踐干貨,歡迎大家前往騰訊雲技術社區