關於檔案上傳下載我所知道的全部內容

patrick_kibo發表於2019-01-01

關於檔案上傳下載我所知道的全部內容
檔案上傳是一個很基礎的內容,有很多的應用場景,但是前端各種庫和框架實在是太便利了,根本不用瞭解到用原生的是怎麼實現的,一遇到問題就各種懵逼,最近剛好經歷了幾種檔案上傳的需求,就以此來作為開年的第一篇分享

1. 表單上傳

在AJAX還不流行的年代,表單上傳檔案是基本操作。表單上傳檔案很簡單,有兩個需要重點關注的屬性:

1.1 enctype

屬性用於設定form表單提交的時候資料編碼方式,一共有三種引數選擇:

  1. application/x-www-form-urlencoded 傳送前編碼所有字元
  2. multipart/form-data 不對字元進行編碼
  3. text/plain 空格轉換為+,但是不會對字元進行編碼

如果想要使用檔案上傳,必須指定為第二個屬性值:enctype=multipart/form-data

1.2 multiple

對於選擇檔案的時候如果想對檔案進行多選,那麼必須要設定<input type="file" multiple="multiple">

一個比較完整程式碼片段

<form action="http://localhost:3000/upload" method="POST" enctype="multipart/form-data">
    <input type="file" name="file" multiple="multiple">
    <input type="submit" value="submit"/>
</form>
複製程式碼

2. AJAX上傳

如果要實現頁面不重新整理的檔案上傳,有兩種常用的方案:

  1. <iframe>表單提交方案
  2. AJAX方案

第一種方案在頁面中巢狀一個<iframe>,將表單放置於<iframe>中,此時完成表單提交不會發生全域性頁面重新整理。但是這個方案,隨著AJAX的逐漸完善以及前後端分離和單頁面應用的普及,輪為了很不常規的替代方案。

2.1 基本內容

實現AJAX上傳,首先需要對XHR有所瞭解(如有不瞭解的可以參照MDN的學習文件AJAX開始

XHR在傳送資料的時候可以接受一個html5的新物件FormData,可以通過將包含檔案的表單/活著將檔案放到FormData中傳遞到後端介面,

html:
<form id="fileForm">
    <input type="file" name="file" multiple="multiple" onchange="changeFileChoose(event)">
    <input type="button" onclick="upload();" value="submit"/>
</form>

js:
let formData = new FormData(document.getElementById('fileForm'));
let xhr = new XMLHttpRequest();
xhr.open('POST', 'http://localhost:3000/upload');
xhr.setRequestHeader('Content-Type', 'multipart/form-data');
xhr.send(formData);
複製程式碼

如果表單中每個檔案想單獨傳送請求(傳送多次請求),可以獲取表單中檔案資訊並構建多個表單物件上傳

formData.getAll('file').filter(file => {
    return file.name
}).forEach((file, index) => {
    let separateFormData = new FormData();
    separateFormData.set('file', file);
    xhr.send(separateFormData)
})
複製程式碼

PS:在傳遞到時候注意設定請求頭資訊Content-type: multiple/form-data來支援檔案上傳操作

2.2 上傳進度

將上傳過程的上傳進度告訴使用者是一個很好的使用者互動行為,一方面避免使用者多次重複上傳,另一方面也是對使用者操作對反饋,告訴使用者系統正在處理他的操作。

監聽檔案上傳進度,個人認為要麼前端輪詢獲取後端的檔案寫入情況,要麼前端有支援上傳進度獲取對事件,其實確實AJAX上傳過程中提供了相關物件,獲取到檔案的網路傳輸情況的,所以在對上傳結果要求並非十分嚴格的情況下,通過前端監聽反饋進度已經足夠了

上傳進度的監聽需要使用xhr.upload物件的事件,利用監聽xhr.upload.onprogress來實現上傳進度的監聽

xhr.upload.onprogress = ev => {
    console.log(`upload loaded: ${ev.loaded}, total: ${ev.total}`);
    progress = ev.loaded * 100 / ev.total;
}
複製程式碼

onprogress事件的event物件中包含前端已經傳輸的資料資訊ev.loaded以及檔案的總尺寸資訊ev.total,利用這些資訊就可以在頁面中顯示檔案上傳進度

2.3 取消上傳

AJAX自身提供了取消操作,通過利用xhr.abort()方法來取消掉整個xhr的請求,當然如果僅僅想取消檔案上傳而不是取消整個AJAX過程,也可以使用xhr.upload.abort()單獨的取消掉AJAX過程中的檔案上傳

2.4 選擇圖片並上傳預覽

<input type="file">onchange事件在選擇檔案發生變更的時候會觸發,利用事件中的event物件的event.target.files,可以獲取到當前選擇的檔案集合,遍歷該集合,根據file.type來判斷檔案型別,並利用window.URL.createObjectURL(file)可以拿到轉換過後的base64圖片地址,最後再給圖片img.src設定路徑從而實現選擇回顯(圖片可以使用createElement('img')body.appendChild(),也可以使用new Image()canvasdragImage()方法來實現繪製)

/**
 * 驗證圖片型別
 * @param {*} type 檔案型別
 */
function validateImage(type) {
    return ['image/jpeg', 'image/png', 'image/jpg'].includes(type);
}

if (validateImage(file.type)) {
    let image = document.createElement('img');
    // URL.createObjectURL可以接受File, Blob, MediaSource物件
    image.style.height = '100px';
    image.style.width = '100px';
    image.src = window.URL.createObjectURL(file);
    document.body.appendChild(image);
}
複製程式碼

PS:由於圖片載入對瀏覽器來說是非同步的過程,如果要對圖片進行相關操作,請在img.onload操作以後執行

3. 拖拽上傳

在瞭解AJAX上傳的基礎上,其實拖拽上傳只需要知道如何獲取到拖拽檔案物件,就可以使用相同的方法進行上傳了。 拖拽也是有一系列事件,具體拖拽相關事件,可以參見接下來的分享或者MDN Drag and Drop API

3.1 檔案拖拽

檔案拖拽上傳的關鍵在於,可以通過event.dataTransfer獲取到拖拽資訊。該物件存在的兩個物件屬性filesitems,如果拖拽的內容是檔案,那麼可以遍歷files物件,就可以獲得檔案資訊

html:
<div>
    <p>拖拽上傳</p>
    <div id="fileArea" class="file_area">拖拽到此區域上傳</div>
</div>

js:
let fileArea = document.querySelector('#fileArea')
fileArea.addEventListener('drop', ev => {
    let files = ev.dataTransfer.files
    for (let i = 0; i < files.length; i++) {
        // 呼叫ajax相關內容
        sendFile(files[i]);
    }
    // 防止瀏覽器直接開啟檔案
    ev.preventDefault();
})
複製程式碼

3.2 目錄拖拽

突然某一天出現了目錄拖拽的需求,以為和檔案上傳是同樣可以通過files來獲取,結果發現不行。這個時候需要使用另一個屬性物件items,並利用File and Directory Entries API來處理items

首先利用item.webkitGetAsEntry()/item.getEntry()獲取到FileEntry,之後使用entry.createReader()獲取到reader物件,之後reader.readEntries讀取資訊並遞迴分別處理檔案和資料夾,如果是檔案通過entry.file()的方式獲取檔案資訊

js:
fileArea.addEventListener('drop', ev => {
    for (let i = 0; i < ev.dataTransfer.items.length; i++) {
        // 獲取entry物件
        let entry = ev.dataTransfer.items[i].webkitGetAsEntry()
        if (entry) {
            scanFiles(entry, sendFile)
        }
    }
    // 防止瀏覽器直接開啟檔案
    ev.preventDefault();
})

function scanFiles (entry, callback) { // 瀏覽檔案結構
    // 如果是檔案目錄,那麼繼續迴圈獲取到目錄下的檔案
    if (entry.isDirectory) {
      let directoryReader = entry.createReader();
      directoryReader.readEntries(entries => {
        entries.forEach(entry => {
          scanFiles(entry, callback);
        })
      }, err => {
        console.log(err, err.message);
      })
    }
    // 如果是檔案,安麼新增到最後的檔案資料集中
    if (entry.isFile) {
        i++
        entry.file(file => {
            callback(file, i);
        }, err => {
            console.log(err, err.message);
        })
    }
}
複製程式碼

PS:

  1. 這裡尤其要注意entry.file()方法,想要獲取到檔案資訊只能在回撥函式中獲取
  2. 由於瀏覽器安全性問題,本地是不能直接訪問檔案系統的,所以,如果以上的例子不在服務端執行,會報錯DOMException(這個問題花費了我N個小時),可以全域性安裝一個http-server來執行上面的程式碼

4. 總結

程式設計真的是一件很好玩的事情,最近看演算法的基礎,覺得真的很有意思,前端程式設計也一樣,如果僅僅停留在使用元件上,真的很沒意思,有時間可以多多看看各種原生的事件和方法,深入研究一下框架相當有意思。超級感謝MDN啊,基本上可以獲取到所有想要的資訊

完整DEMO的:github.com/PatrickLh/f…

5. 參考

MDN XMLHttpRequest

MDN File and Directory Entries API

MDN HTML Drag and Drop API

相關文章