#前言
平常在寫業務的時候常常會用的到的是 GET
, POST
請求去請求接口,GET
相關的接口會比較容易基本不會出錯,而對於 POST
中常用的 表單提交,JSON
提交也比較容易,但是對於文件上傳呢?大家可能對這個步驟會比較害怕,因為可能大家對它並不是怎么熟悉,而瀏覽器Network
對它也沒有詳細的進行記錄,因此它成為了我們心中的一根刺,我們老是無法確定,關於文件上傳到底是我寫的有問題呢?還是后端有問題,當然,我們一般都比較謙虛, 總是會在自己身上找原因,可是往往實事呢?可能就出在后端身上,可能是他接受寫的有問題,導致你換了各種請求庫去嘗試,axios
,request
,fetch
等等。那么我們如何避免這種情況呢?我們自身要對這一塊夠熟悉,才能不以猜的方式去寫代碼。如果你覺得我以上說的你有同感,那么你閱讀完這篇文章你將收獲自信,你將不會質疑自己,不會以猜的方式去寫代碼。
本文比較長可能需要花點時間去看,需要有耐心,我采用自頂向下的方式,所有示例會先展現出你熟悉的方式,再一層層往下, 先從請求端是怎么發送文件的,再到接收端是怎么解析文件的。以下是講解的大綱,我們先從瀏覽器端上傳文件,再到服務端上傳文件,然后我們再來解析文件是如何被解析的。
#前置知識
#什么是 multipart/form-data?
multipart/form-data
最初由 《RFC 1867: Form-based File Upload in HTML》文檔提出。
Since file-upload is a feature that will benefit many applications, this proposes an extension to HTML to allow information providers to express file upload requests uniformly, and a MIME compatible representation for file upload responses.
由於文件上傳功能將使許多應用程序受益,因此建議對HTML進行擴展,以允許信息提供者統一表達文件上傳請求,並提供文件上傳響應的MIME兼容表示。
總結就是原先的規范不滿足啦,我要擴充規范了。
#文件上傳為什么要用 multipart/form-data?
The encoding type application/x-www-form-urlencoded is inefficient for sending large quantities of binary data or text containing non-ASCII characters. Thus, a new media type,multipart/form-data, is proposed as a way of efficiently sending the values associated with a filled-out form from client to server.
1867文檔中也寫了為什么要新增一個類型,而不使用舊有的application/x-www-form-urlencoded
:因為此類型不適合用於傳輸大型二進制數據或者包含非ASCII字符的數據。平常我們使用這個類型都是把表單數據使用url編碼后傳送給后端,二進制文件當然沒辦法一起編碼進去了。所以multipart/form-data
就誕生了,專門用於有效的傳輸文件。
也許你有疑問?那可以用 application/json
嗎?
其實我認為,無論你用什么都可以傳,只不過會要綜合考慮一些因素的話,multipart/form-data
更好。例如我們知道了文件是以二進制的形式存在,application/json
是以文本形式進行傳輸,那么某種意義上我們確實可以將文件轉成例如文本形式的 Base64
形式。但是呢,你轉成這樣的形式,后端也需要按照你這樣傳輸的形式,做特殊的解析。並且文本在傳輸過程中是相比二進制效率低的,那么對於我們動輒幾十M幾百M的文件來說是速度是更慢的。
以上為什么文件傳輸要用multipart/form-data
我還可以舉個例子,例如你在中國,你想要去美洲,我們的multipart/form-data
相當於是選擇飛機,而application/json
相當於高鐵,但是呢?中國和美洲之間沒有高鐵啊,你執意要坐高鐵去,你可以花昂貴的代價(后端額外解析你的文本)造高鐵去美洲,但是你有更加廉價的方式坐飛機(使用multipart/form-data
)去美洲(去傳輸文件)。你圖啥?(如果你有錢有時間,抱歉,打擾了,老子給你道歉)
#multipart/form-data規范是什么?
摘自 《RFC 1867: Form-based File Upload in HTML》 6.Example
Content-type: multipart/form-data, boundary=AaB03x
--AaB03x
content-disposition: form-data; name="field1"
Joe Blow
--AaB03x
content-disposition: form-data; name="pics"; filename="file1.txt"
Content-Type: text/plain
... contents of file1.txt ...
--AaB03x--
可以簡單解釋一些,首先是請求類型,然后是一個 boundary (分割符),這個東西是干啥的呢?其實看名字就知道,分隔符,當時分割作用,因為可能有多文件多字段,每個字段文件之間,我們無法准確地去判斷這個文件哪里到哪里為截止狀態。因此需要有分隔符來進行划分。然后再接下來就是聲明內容的描述是 form-data 類型,字段名字是啥,如果是文件的話,得知道文件名是啥,還有這個文件的類型是啥,這個也很好理解,我上傳一個文件,我總得告訴后端,我傳的是個啥,是圖片?還是一個txt文本?這些信息肯定得告訴人家,別人才好去進行判斷,后面我們也會講到如果這些沒有聲明的時候,會發生什么?
好了講完了這些前置知識,我們接下來要進入我們的主題了。面對File, formData,Blob,Base64,ArrayBuffer,到底怎么做?還有文件上傳不僅僅是前端的事。服務端也可以文件上傳(例如我們利用某雲,把靜態資源上傳到 OSS 對象存儲)。服務端和客戶端也有各種類型,Buffer,Stream,Base64....頭禿,怎么搞?不急,就是因為上傳文件不單單是前端的事,所以我將以下上傳文件的一方稱為請求端,接受文件一方稱為接收方。我會以請求端各種上傳方式,接收端是怎么解析我們的文件以及我們最終的殺手鐧調試工具-wireshark來進行講解。
#請求端
#瀏覽端
#File
首先我們先寫下最簡單的一個表單提交方式。
<form action="http://localhost:7787/files" method="POST"> <input name="file" type="file" id="file"> <input type="submit" value="提交"> </form>
我們選擇文件后上傳,發現后端返回了文件不存在。
不用着急,熟悉的同學可能立馬知道是啥原因了。噓,知道了也聽我慢慢叨叨。
我們打開控制台,由於表單提交會進行網頁跳轉,因此我們勾選preserve log
來進行日志追蹤。
我們可以發現其實 FormData
中 file
字段顯示的是文件名,並沒有將真正的內容進行傳輸。再看請求頭。
發現是請求頭和預期不符,也印證了 application/x-www-form-urlencoded
無法進行文件上傳。
我們加上請求頭,再次請求。
<form action="http://localhost:7787/files" enctype="multipart/form-data" method="POST"> <input name="file" type="file" id="file"> <input type="submit" value="提交"> </form>
發現文件上傳成功,簡單的表單上傳就是像以上一樣簡單。但是你得熟記文件上傳的格式以及類型。
#FormData
formData 的方式我隨便寫了以下幾種方式。
<input type="file" id="file"> <button id="submit">上傳</button> <script src="https://cdn.bootcss.com/axios/0.19.2/axios.min.js"></script> <script> submit.onclick = () => { const file = document.getElementById('file').files[0]; var form = new FormData(); form.append('file', file); // type 1 axios.post('http://localhost:7787/files', form).then(res => { console.log(res.data); }) // type 2 fetch('http://localhost:7787/files', { method: 'POST', body: form }).then(res => res.json()).tehn(res => {console.log(res)}); // type3 var xhr = new XMLHttpRequest(); xhr.open('POST', 'http://localhost:7787/files', true); xhr.onload = function () { console.log(xhr.responseText); }; xhr.send(form); } </script>
以上幾種方式都是可以的。但是呢,請求庫這么多,我隨便在 npm 上一搜就有幾百個請求相關的庫。
因此,掌握請求庫的寫法並不是我們的目標,目標只有一個還是掌握文件上傳的請求頭和請求內容。
#Blob
Blob
對象表示一個不可變、原始數據的類文件對象。Blob 表示的不一定是JavaScript原生格式的數據。File
接口基於Blob
,繼承了 blob 的功能並將其擴展使其支持用戶系統上的文件。
因此如果我們遇到 Blob 方式的文件上方式不用害怕,可以用以下兩種方式:
1.直接使用 blob 上傳
const json = { hello: "world" }; const blob = new Blob([JSON.stringify(json, null, 2)], { type: 'application/json' }); const form = new FormData(); form.append('file', blob, '1.json'); axios.post('http://localhost:7787/files', form);
2.使用 File 對象,再進行一次包裝(File 兼容性可能會差一些 https://caniuse.com/#search=File)
const json = { hello: "world" }; const blob = new Blob([JSON.stringify(json, null, 2)], { type: 'application/json' }); const file = new File([blob], '1.json'); form.append('file', file); axios.post('http://localhost:7787/files', form)
#ArrayBuffer
ArrayBuffer
對象用來表示通用的、固定長度的原始二進制數據緩沖區。
雖然它用的比較少,但是他是最貼近文件流的方式了。
在瀏覽器中,他每個字節以十進制的方式存在。我提前准備了一張圖片。
const bufferArrary = [137,80,78,71,13,10,26,10,0,0,0,13,73,72,68,82,0,0,0,1,0,0,0,1,1,3,0,0,0,37,219,86,202,0,0,0,6,80,76,84,69,0,0,255,128,128,128,76,108,191,213,0,0,0,9,112,72,89,115,0,0,14,196,0,0,14,196,1,149,43,14,27,0,0,0,10,73,68,65,84,8,153,99,96,0,0,0,2,0,1,244,113,100,166,0,0,0,0,73,69,78,68,174,66,96,130]; const array = Uint8Array.from(bufferArrary); const blob = new Blob([array], {type: 'image/png'}); const form = new FormData(); form.append