在前一篇文章《File雜談——拖拽上傳前傳》中我製作了一個靜態的拖拽上傳介面,拖拽檔案到顯示區域釋放,可以顯示拖入檔案的基本資訊。本文將在此基礎上進一步加工,打造一個完整的拖拽上傳示例。
示例說明
點選區域選擇檔案或直接將檔案拖入區域,觸發檔案上傳功能,檔案將非同步傳送到伺服器。待服務端處理完成後返回基本資訊,在頁面中顯示。由於伺服器容量問題,本示例未做檔案儲存處理,只是簡單的將檔案基本資訊返回,檔案上傳的後端具體處理邏輯需要自行補充。
新的小夥伴FormData
我們知道,傳統的檔案上傳如果要實現非同步的效果,我們會使用iframe去模擬,或使用flash上傳外掛。但是今天,我們又認識了一位新成員——FromData,它可以通過js建立表單物件,並可以向該物件中新增表單資料(字串、數字、檔案等)。再結合我們熟悉的XMLHttpRequest物件,將表單資料非同步提交到服務端,這樣我們的問題就解決了。
下面,我們來看下核心程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
function uploadFile(fs) { var len = fs.length; for (var i = 0; i < len; i++) { sendFile(fs[i]); } } function sendFile(file) { var xhr = new XMLHttpRequest(), fd = new FormData(); fd.append('file', file); xhr.onreadystatechange = function() { if (xhr.readyState == 4 && xhr.status == 200) { // 將服務端返回資訊輸出到日誌區(考慮多檔案情況) consoleDiv.innerHTML += '<br>' + xhr.responseText; } }; xhr.open('POST', './upload.php'); xhr.send(fd); } // 檔案控制元件發生變化時,呼叫uploadFile函式,觸發上傳功能 file.onchange = function() { uploadFile(this.files); }; // 在區域內釋放拖入檔案時,呼叫檔案上傳函式 area.ondrop = function(ev) { ev.preventDefault(); var dt = ev.dataTransfer; uploadFile(dt.files); }; |
程式碼很簡單,不再做過多闡述。但是這裡我想發表一點個人意見:根據示例我們不難發現有這麼一個問題——如果使用者都使用拖拽上傳功能,而不使用點選觸發File控制元件選擇檔案上傳,那麼File控制元件完全沒有存在的必要。聯絡上文中我提到的File控制元件的地位受到威脅這一觀點,我大膽的設想,假如未來的某一項標準中給每個HTMLElement暴露一個選擇檔案的功能介面,那麼拖拽和點選功能將可以集於一個元素之上,到那時File控制元件的地位恐怕不僅僅是受到威脅,很有可能退出歷史舞臺!出於File控制元件視覺效果和互動不統一的角度去考慮,我覺得以上推測還是有可能的,哈哈~~
雖然示例並未在後端做太多工作,我這裡還是以PHP為例,說明一下後端該如何工作。單從示例而言,我的程式碼是這樣的:
1 2 |
$file = $_FILES['file']; echo json_encode($file); |
可以說是非常簡單了。而我們在實際應用中往往還會涉及更多更復雜的處理邏輯。最起碼的我們應該要將tmp_name對應的臨時檔案移動到我們指定的上傳目錄吧。當然,這一過程我們就會對檔案型別進行判斷,大小限制,重新命名,資料儲存,等等。基本程式碼:
1 2 3 4 5 6 7 8 |
$file = $_FILES['file']; $path = './upload'; if ($file['size'] > 2000000) { echo '{"error": "1000", "message": "上傳檔案大小超限,不能超過xxM"}'; } $path .= '/file_' . time() . '.png'; // 這裡還可能會存在檔案資料儲存,新舊名稱關聯等邏輯 move_uploaded_file($file['tmp_name'], $path); |
一個神奇的方法sendAsBinary
前面我們聊到的使用FormData來實現檔案非同步上傳,在高階瀏覽器中都能正常運作,沒有太大問題。接下來我們另外一個在Firefox實現非同步上傳的方法。這個方法,我們又會交到一個新的朋友——FireReader。FileReader是HTML5新增的一個物件,它可以訪問使用者本地檔案,並且可以以不同格式讀取檔案內容。
FileReader基本使用
首先我們來看一下如何建立一個FileReader例項物件,以及它擁有哪些例項方法。在js中建立一個FileReader物件很簡單:
1 |
var reader = new FileReader(); |
我們可以通過reader物件訪問本地檔案,那麼reader物件擁有哪些我們常用的屬性、事件和方法呢?請看以下列表:
事件
- onload :檔案成功讀完時觸發
- onloadend :檔案讀完時觸發,無論成功與否
- onloadstart :開始讀取檔案時觸發
- onprogress :檔案讀取中,常用於獲取讀取進度
- onabort :檔案讀取操作中斷
- onerror :檔案讀取出錯
屬性
- result :讀取到的檔案內容,當讀取操作完成後生效
- readyState :FileReader物件的當前狀態
- error :出錯時的錯誤資訊
方法
- abort :中斷檔案讀取操作
- readAsBinaryString :將檔案內容讀取為二進位制格式
- readAsDataURL :將檔案內容讀取為DataURL格式,通常所說的base64格式
- readAsText :將檔案內容讀取為文字
以上就是FileReader物件最常用的內容,下面我們先看一個小例子:
1 2 3 4 5 |
var rd = new FileReader(); rd.onload = function(ev) { console.log(ev.target.result); }; rd.readAsText(file); |
以上程式碼中的file引數是一個file物件,可以使File控制元件的files屬性中FileList的一個,也可以是dataTransfer中files屬性中FileList的一個。
FileReader + sendAsBinary實現非同步上傳
認識了FireReader,下面我們來看一下在Firefox中如何使用FileReader和XMLHttpRequest的sendAsBinary方法實現檔案非同步上傳。核心程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
function sendByBinary(file) { var xhr = new XMLHttpRequest(), reader = new FileReader(); xhr.onreadystatechange = function() { if (xhr.readyState == 4 && xhr.status == 200) { consoleDiv.innerHTML += '<br>' + xhr.responseText; } }; xhr.overrideMimeType('text/plain; charset=x-user-defined-binary'); xhr.open('POST', './upload.php'); reader.onload = function(ev) { // 將二進位制內容傳送至服務端 xhr.sendAsBinary(ev.target.result); }; // 將檔案內容讀取為二進位制格式 reader.readAsBinaryString(file); } |
程式碼很簡單,跟FormData的方式差不多,我們接著看服務端將如何獲取POST過去的檔案內容(以PHP為例):
1 2 3 4 5 |
// 方法一,這個方法需要php.ini開啟支援 $content = $GLOBALS['HTTP_RAW_POST_DATA']; // 方法二,不需要php.ini設定,記憶體壓力小 $content = file_get_contents('php://input'); |
所以綜合起來比較保險的方法:
1 2 3 4 5 |
$content = $GLOBALS['HTTP_RAW_POST_DATA']; if (empty($content)) { $content = file_get_contents('php://input'); } echo $content; // 輸出檔案內容 |
我們暫且不說sendAsBinary這種方式目前只有Firefox支援,單從伺服器拿到檔案內容後該如何處理來說,這種方法顯然沒有使用FormData的方式有優勢。因為服務端僅僅拿到了檔案內容,並沒有檔案型別和大小等資訊,對一些限制邏輯和檔案儲存的實現很不友好。