關於文件上傳下載我所知道的所有內容

文件上傳是一個很基礎的內容,有不少的應用場景,可是前端各類庫和框架實在是太便利了,根本不用瞭解到用原生的是怎麼實現的,一遇到問題就各類懵逼,最近恰好經歷了幾種文件上傳的需求,就以此來做爲開年的第一篇分享

1. 表單上傳

在AJAX還不流行的年代,表單上傳文件是基本操做。表單上傳文件很簡單,有兩個須要重點關注的屬性:html

1.1 enctype

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

  1. application/x-www-form-urlencoded 發送前編碼全部字符
  2. multipart/form-data 不對字符進行編碼
  3. text/plain 空格轉換爲+,可是不會對字符進行編碼

若是想要使用文件上傳,必須指定爲第二個屬性值:enctype=multipart/form-datahtml5

1.2 multiple

對於選擇文件的時候若是想對文件進行多選,那麼必需要設置<input type="file" multiple="multiple">git

一個比較完整代碼片斷github

<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上傳

若是要實現頁面不刷新的文件上傳,有兩種經常使用的方案:web

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

第一種方案在頁面中嵌套一個<iframe>,將表單放置於<iframe>中,此時完成表單提交不會發生全局頁面刷新。可是這個方案,隨着AJAX的逐漸完善以及先後端分離和單頁面應用的普及,輪爲了很不常規的替代方案。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);
複製代碼

若是表單中每一個文件想單獨發送請求(發送屢次請求),能夠獲取表單中文件信息並構建多個表單對象上傳canvas

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

相關文章
相關標籤/搜索