關于文件上傳下載我所知道的全部內容

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

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的:https://github.com/PatrickLh/file-upload

5. 參考

MDN XMLHttpRequest

MDN File and Directory Entries API

MDN HTML Drag and Drop API

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,048評論 6 542
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,414評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,169評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,722評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,465評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,823評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,813評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,000評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,554評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,295評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,513評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,035評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,722評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,125評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,430評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,237評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,482評論 2 379

推薦閱讀更多精彩內容

  • 本文詳細介紹了 XMLHttpRequest 相關知識,涉及內容: AJAX、XMLHTTP、XMLHttpReq...
    semlinker閱讀 13,707評論 2 18
  • Ajax和XMLHttpRequest 我們通常將Ajax等同于XMLHttpRequest,但細究起來它們兩個是...
    changxiaonan閱讀 2,256評論 0 2
  • 看到標題時,有些同學可能會想:“我已經用xhr成功地發過很多個Ajax請求了,對它的基本操作已經算挺熟練了。” 我...
    前端渣渣閱讀 5,779評論 1 12
  • 輕撥蘆葦蕩出笛聲悠悠 撩撥了未到的秋 我在船頭流連著那只白鷗 卻想你說過的自由 那年桃花映的臉龐 微醺 輕嘆若是歲...
    田老濕花癡愛寫詩閱讀 314評論 0 2
  • 做在地鐵上,座位對面是地鐵車窗,看著地鐵車窗里反射的我的形象,帽子大衣包包牛仔褲,今天穿的一身算是我比較滿意了。 ...
    nooooogod閱讀 279評論 1 0