少妇无码太爽了在线播放_久久久综合香蕉尹人综合网_日日碰狠狠添天天爽五月婷_国产欧美精品一区二区三区四区

人(ren)參的(de)用量

小程序post傳參,axios請求參數格式,一文了解文件上傳全過程(1.8w字深度解析)「前端進階必備」



作者:藍色的秋風

轉發鏈接:
//mp.weixin.qq.com/s/cruL9JGZNZQFrMSrzJJWiQ

前言

平常(chang)(chang)在(zai)寫(xie)業務的(de)(de)(de)(de)時候常(chang)(chang)常(chang)(chang)會(hui)(hui)用(yong)的(de)(de)(de)(de)到的(de)(de)(de)(de)是(shi)(shi) GET, POST請(qing)求(qiu)(qiu)去請(qing)求(qiu)(qiu)接口,GET 相關的(de)(de)(de)(de)接口會(hui)(hui)比(bi)較容易基本不(bu)(bu)(bu)會(hui)(hui)出錯,而對于(yu) POST中常(chang)(chang)用(yong)的(de)(de)(de)(de) 表單提交,JSON提交也比(bi)較容易,但(dan)是(shi)(shi)對于(yu)文件上傳呢?大家可(ke)(ke)能對這個步驟會(hui)(hui)比(bi)較害(hai)怕(pa),因為(wei)(wei)可(ke)(ke)能大家對它(ta)并不(bu)(bu)(bu)是(shi)(shi)怎么(me)熟(shu)悉,而瀏覽器(qi)Network對它(ta)也沒有詳(xiang)細(xi)的(de)(de)(de)(de)進行記錄,因此它(ta)成為(wei)(wei)了我(wo)們(men)心中的(de)(de)(de)(de)一(yi)根刺,我(wo)們(men)老(lao)是(shi)(shi)無法確定,關于(yu)文件上傳到底(di)是(shi)(shi)我(wo)寫(xie)的(de)(de)(de)(de)有問(wen)題(ti)呢?還是(shi)(shi)后端有問(wen)題(ti),當然,我(wo)們(men)一(yi)般都比(bi)較謙(qian)虛, 總是(shi)(shi)會(hui)(hui)在(zai)自(zi)己身(shen)(shen)上找原因,可(ke)(ke)是(shi)(shi)往往實事(shi)呢?可(ke)(ke)能就出在(zai)后端身(shen)(shen)上,可(ke)(ke)能是(shi)(shi)他(ta)接受寫(xie)的(de)(de)(de)(de)有問(wen)題(ti),導致你換了各種請(qing)求(qiu)(qiu)庫去嘗(chang)試,axios,request,fetch 等(deng)等(deng)。那么(me)我(wo)們(men)如何避免這種情況呢?我(wo)們(men)自(zi)身(shen)(shen)要對這一(yi)塊夠(gou)熟(shu)悉,才能不(bu)(bu)(bu)以猜的(de)(de)(de)(de)方(fang)式去寫(xie)代(dai)碼。如果你覺得我(wo)以上說的(de)(de)(de)(de)你有同感,那么(me)你閱(yue)讀完這篇文章(zhang)你將收獲(huo)自(zi)信,你將不(bu)(bu)(bu)會(hui)(hui)質疑自(zi)己,不(bu)(bu)(bu)會(hui)(hui)以猜的(de)(de)(de)(de)方(fang)式去寫(xie)代(dai)碼。

本文(wen)(wen)比(bi)較長可能(neng)需要花(hua)點時間去看,需要有(you)耐心,我(wo)采用自頂向下(xia)的(de)(de)方(fang)式(shi),所(suo)有(you)示例會先(xian)展現(xian)出(chu)你(ni)熟悉(xi)的(de)(de)方(fang)式(shi),再一層層往(wang)下(xia), 先(xian)從請求端(duan)是(shi)怎(zen)么發送(song)文(wen)(wen)件(jian)的(de)(de),再到(dao)接(jie)收端(duan)是(shi)怎(zen)么解析文(wen)(wen)件(jian)的(de)(de)。

前置知識

什么是 multipart/form-data?

multipart/form-data 最初由 《RFC 1867: Form-based File Upload in HTML》[1]文檔提出。

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.

由于文(wen)(wen)件(jian)上傳功能將使許(xu)多應用程序受益,因此建(jian)議對HTML進行(xing)擴(kuo)展,以允許(xu)信(xin)息提供者統一表達文(wen)(wen)件(jian)上傳請求,并提供文(wen)(wen)件(jian)上傳響應的MIME兼(jian)容表示(shi)。

總結就是原先的規范不滿足了(le),我要擴充規范了(le)。

文件上傳為什么要用 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:因(yin)為此類型不(bu)適合(he)用于傳輸大型二進制(zhi)數(shu)據或者(zhe)包含非ASCII字符的數(shu)據。平(ping)常我們(men)使用這個(ge)類型都是把表(biao)單數(shu)據使用url編(bian)碼(ma)后傳送(song)給后端,二進制(zhi)文(wen)(wen)件當然沒辦法(fa)一(yi)起編(bian)碼(ma)進去了。所以(yi)multipart/form-data就(jiu)誕生了,專門用于有效的傳輸文(wen)(wen)件。

也許你有疑問?那可以用 application/json嗎?

其實我(wo)認為(wei),無論你用什么都可以(yi)傳(chuan),只不(bu)過會(hui)要(yao)綜合考慮一些(xie)因素(su)的(de)(de)(de)話,multipart/form-data更好。例如(ru)我(wo)們知道了文(wen)件是以(yi)二進(jin)制的(de)(de)(de)形(xing)式(shi)(shi)(shi)存(cun)在(zai)(zai),application/json 是以(yi)文(wen)本(ben)(ben)(ben)形(xing)式(shi)(shi)(shi)進(jin)行(xing)傳(chuan)輸,那么某種(zhong)意(yi)義上我(wo)們確(que)實可以(yi)將文(wen)件轉(zhuan)成例如(ru)文(wen)本(ben)(ben)(ben)形(xing)式(shi)(shi)(shi)的(de)(de)(de) Base64 形(xing)式(shi)(shi)(shi)。但是呢,你轉(zhuan)成這(zhe)樣的(de)(de)(de)形(xing)式(shi)(shi)(shi),后端也需要(yao)按(an)照你這(zhe)樣傳(chuan)輸的(de)(de)(de)形(xing)式(shi)(shi)(shi),做特殊的(de)(de)(de)解(jie)析。并且文(wen)本(ben)(ben)(ben)在(zai)(zai)傳(chuan)輸過程中(zhong)是相比二進(jin)制效率低的(de)(de)(de),那么對(dui)于我(wo)們動輒幾(ji)十M幾(ji)百M的(de)(de)(de)文(wen)件來說是速度是更慢的(de)(de)(de)。

以上為什么文件(jian)傳輸要用multipart/form-data 我(wo)還(huan)可以舉個例子(zi)(zi),例如(ru)(ru)你(ni)(ni)在中(zhong)(zhong)國,你(ni)(ni)想要去(qu)美(mei)(mei)洲(zhou),我(wo)們的multipart/form-data相(xiang)當(dang)于(yu)是選(xuan)擇飛(fei)(fei)機,而application/json相(xiang)當(dang)于(yu)高鐵,但(dan)是呢?中(zhong)(zhong)國和美(mei)(mei)洲(zhou)之間沒有(you)(you)高鐵啊(a),你(ni)(ni)執意要坐(zuo)(zuo)高鐵去(qu),你(ni)(ni)可以花昂貴的代(dai)價(后端額外解析你(ni)(ni)的文本)坐(zuo)(zuo)高鐵去(qu)美(mei)(mei)洲(zhou),但(dan)是你(ni)(ni)有(you)(you)更加廉價的方(fang)式坐(zuo)(zuo)飛(fei)(fei)機(使用multipart/form-data)去(qu)美(mei)(mei)洲(zhou)(去(qu)傳輸文件(jian))。你(ni)(ni)圖啥?(如(ru)(ru)果你(ni)(ni)有(you)(you)錢(qian)有(you)(you)時間,抱歉,打(da)擾了,老子(zi)(zi)給(gei)你(ni)(ni)道歉)

multipart/form-data規范是什么?

摘自 《RFC 1867: Form-based File Upload in HTML》[2] 6.Example

Content-type: multipart/form-data, boundary=AaB03x--AaB03xcontent-disposition: form-data; name="field1"Joe Blow--AaB03xcontent-disposition: form-data; name="pics"; filename="file1.txt"Content-Type: text/plain

... contents of file1.txt ...--AaB03x--

可以(yi)簡單解釋一(yi)些,首(shou)先是(shi)(shi)請求類(lei)型(xing),然后是(shi)(shi)一(yi)個(ge)(ge) boundary (分(fen)割符),這(zhe)(zhe)個(ge)(ge)東西是(shi)(shi)干啥(sha)的(de)(de)呢(ni)?其實看名(ming)字就知道(dao),分(fen)隔符,當時分(fen)割作用(yong),因為可能有(you)多文(wen)(wen)(wen)件(jian)(jian)多字段,每個(ge)(ge)字段文(wen)(wen)(wen)件(jian)(jian)之間,我(wo)們(men)無法(fa)準確地去判斷這(zhe)(zhe)個(ge)(ge)文(wen)(wen)(wen)件(jian)(jian)哪里到(dao)哪里為截止狀態。因此需要有(you)分(fen)隔符來(lai)進(jin)(jin)行劃分(fen)。然后再接下來(lai)就是(shi)(shi)聲明(ming)內容的(de)(de)描述(shu)是(shi)(shi) form-data 類(lei)型(xing),字段名(ming)字是(shi)(shi)啥(sha),如果(guo)是(shi)(shi)文(wen)(wen)(wen)件(jian)(jian)的(de)(de)話(hua),得(de)(de)知道(dao)文(wen)(wen)(wen)件(jian)(jian)名(ming)是(shi)(shi)啥(sha),還有(you)這(zhe)(zhe)個(ge)(ge)文(wen)(wen)(wen)件(jian)(jian)的(de)(de)類(lei)型(xing)是(shi)(shi)啥(sha),這(zhe)(zhe)個(ge)(ge)也很好理解,我(wo)上(shang)傳一(yi)個(ge)(ge)文(wen)(wen)(wen)件(jian)(jian),我(wo)總(zong)得(de)(de)告訴(su)(su)后端,我(wo)傳的(de)(de)是(shi)(shi)個(ge)(ge)啥(sha),是(shi)(shi)圖片?還是(shi)(shi)一(yi)個(ge)(ge)txt文(wen)(wen)(wen)本?這(zhe)(zhe)些信(xin)息(xi)肯(ken)定得(de)(de)告訴(su)(su)大家,別(bie)人才好去進(jin)(jin)行判斷,后面我(wo)們(men)也會講(jiang)到(dao)如果(guo)這(zhe)(zhe)些沒(mei)有(you)聲明(ming)的(de)(de)時候,會發生什么?

好了講完了這些前置知識,我們接下來要進入我們的主題了。面對File, formData,Blob,Base64,ArrayBuffer,到底怎么做?還有文件上傳不僅僅是前端的事。服務端也可以文件上傳(例如我們利用某云,把靜態資源上傳到 OSS 對象存儲)。服務端和客戶端也有各種類型,Buffer,Stream,Base64....頭禿,怎么搞?不急,就是因為上傳文件不單單是前端的事,所以我將以下上傳文件的一方稱為請求端,接受文件一方稱為接收方。我(wo)會以請求端(duan)各種上(shang)傳(chuan)方式(shi),接收端(duan)是(shi)怎么解(jie)(jie)析我(wo)們(men)的(de)文(wen)(wen)件以及我(wo)們(men)最終的(de)殺手锏(jian)調試(shi)工具-wireshark來進行講解(jie)(jie)。以下是(shi)講解(jie)(jie)的(de)大(da)綱,我(wo)們(men)先從瀏覽器端(duan)上(shang)傳(chuan)文(wen)(wen)件,再到服務端(duan)上(shang)傳(chuan)文(wen)(wen)件,然后我(wo)們(men)再來解(jie)(jie)析文(wen)(wen)件是(shi)如何(he)被(bei)解(jie)(jie)析的(de)。



請求端

瀏覽端

File

首先(xian)我們先(xian)寫(xie)下最(zui)簡單的一個表單提交方式。

<form action="//localhost:7787/files" method="POST">
	<input name="file" type="file" id="file">
	<input type="submit" value="提交">
</form>

我們選擇文件(jian)后上傳(chuan),發現后端(duan)返回了文件(jian)不存在。



不用著(zhu)急,熟悉的同學可能立馬知(zhi)道是啥原因了(le)。噓,知(zhi)道了(le)也聽我(wo)慢(man)(man)慢(man)(man)叨叨。

我們打開控制臺,由于(yu)表單(dan)提交會進行(xing)網(wang)頁跳轉(zhuan),因此我們勾選preserve log 來進行(xing)日(ri)志追蹤。





我(wo)們(men)可以發現其(qi)實 FormData 中 file 字段顯示的是文件(jian)名(ming),并沒有將真正的內容(rong)進行(xing)傳輸。再看請求頭。



發現是請求頭和預期不符,也印證了
application/x-www-form-urlencoded 無法進行文(wen)件上傳。

我們加上請求頭(tou),再(zai)次請求。

<form action="//localhost:7787/files" enctype="multipart/form-data" method="POST">
  <input name="file" type="file" id="file">
  <input type="submit" value="提交">
</form>



發現文件(jian)上傳(chuan)成功(gong),簡(jian)單的(de)表單上傳(chuan)就是像以上一樣簡(jian)單。但是你得熟記文件(jian)上傳(chuan)的(de)格式以及(ji)類(lei)型。

FormData

formData 的方式我隨便(bian)寫了(le)以下幾種(zhong)方式。

<input type="file" id="file"><button id="submit">上傳</button><script src="//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('//localhost:7787/files', form).then(res => {        console.log(res.data);
    })  	// type 2
  	fetch('//localhost:7787/files', {        method: 'POST',        body: form
    }).then(res => res.json()).tehn(res => {console.log(res)});  	// type3  	var xhr = new XMLHttpRequest();
    xhr.open('POST', '//localhost:7787/files', true);
    xhr.onload = function () {    	console.log(xhr.responseText);
    };
    xhr.send(form);
}</script>



以上(shang)(shang)幾種方式都(dou)是可以的(de)。但是呢,請求庫這么多,我隨便在 npm 上(shang)(shang)一搜就有幾百個請求相關的(de)庫。



因此,掌握請求庫的寫法并不是我們的目標,目標只有一個還是掌握文件上傳的請求頭和請求內容。



Blob

Blob 對象表示一個不可變、原始數據的類文件對象。Blob 表示的不一定是JavaScript原生格式的數據。`File`[3] 接口基于Blob,繼承了 blob 的功能并將其擴展使其支持用戶系(xi)統上的文件。

因此如果我們遇到(dao) Blob 方式的(de)文件上方式不用害(hai)怕(pa),可以(yi)用以(yi)下兩(liang)種方式:

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('//localhost:7787/files', form);

2.使用 File 對象,再(zai)進行一次包裝(File 兼(jian)容性可能會(hui)差一些(xie) //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('//localhost:7787/files', form)

ArrayBuffer

ArrayBuffer 對象用來(lai)表示(shi)通用的、固定長度的原(yuan)始二進制(zhi)數據緩沖區。

雖然(ran)它用(yong)的比較少(shao),但是他(ta)是最貼近文件流的方式(shi)了。

在(zai)(zai)瀏覽器(qi)中,他(ta)每個字節以(yi)十進制的方式存在(zai)(zai)。我提前準備了一張(zhang)圖(tu)片。

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('file', blob, '1.png');
axios.post('//localhost:7787/files', form)

這里(li)需要注意的是 new Blob([typedArray.buffer], {type: 'xxx'}),第一個參數(shu)是由一個數(shu)組(zu)包裹。里(li)面是 typedArray 類型的 buffer。

Base64

const base64 = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAABlBMVEUAAP+AgIBMbL/VAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAACklEQVQImWNgAAAAAgAB9HFkpgAAAABJRU5ErkJggg==';const byteCharacters = atob(base64);const byteNumbers = new Array(byteCharacters.length);for (let i = 0; i < byteCharacters.length; i++) {
	byteNumbers[i] = byteCharacters.charCodeAt(i);
}const array = Uint8Array.from(byteNumbers);const blob = new Blob([array], {type: 'image/png'});const form = new FormData();
form.append('file', blob, '1.png');
axios.post('//localhost:7787/files', form);

關于 base64 的轉化和原理可以看這兩篇 base64 原理[4]

原來瀏覽器原生支持JS Base64編碼解碼[5]

小結

對于瀏覽器(qi)端的文件(jian)上傳,可以(yi)(yi)歸結(jie)出一個套路(lu),所有(you)東西核(he)心思路(lu)就是構造出 File 對象。然后觀察請(qing)求 Content-Type,再看(kan)請(qing)求體(ti)是否(fou)有(you)信息(xi)缺失(shi)。而以(yi)(yi)上這(zhe)些二進(jin)制數據類型的轉化可以(yi)(yi)看(kan)以(yi)(yi)下表。



圖片來源 (
//shanyue.tech/post/binary-in-frontend/#
%E6%95%B0%E6%8D%AE%E8%BE%93%E5%85%A5[6]
)

服務端

講完(wan)了瀏覽(lan)器端,現在我(wo)們來講服務器端,和瀏覽(lan)器不同的(de)是,服務端上傳有兩(liang)個難點。

1.瀏覽(lan)器沒有原生 formData,也不會想(xiang)瀏覽(lan)器一樣幫我們轉成二進制形式。

2.服務端沒有可(ke)視(shi)化的 Network 調試器(qi)。

Buffer

Request

首先我們通過最簡單的示例來進行演示,然后一步一步深入。相信文檔可以查看
//github.com/request/request#
multipartform-data-multipart-form-uploads

// request-error.jsconst fs = require('fs');const path = require('path');const request = require('request');const stream = fs.readFileSync(path.join(__dirname, '../1.png'));
request.post({
    url: '//localhost:7787/files',
    formData: {
        file: stream,
    }
}, (err, res, body) => {    console.log(body);
})



發現(xian)報了(le)一個錯誤,正像(xiang)上面(mian)所(suo)說,瀏覽器(qi)端報錯,可以用(yong)NetWork。那么服務端怎么辦(ban)?這個時候我(wo)們(men)(men)拿出(chu)我(wo)們(men)(men)的利(li)器(qi) -- wireshark

我們打開 wireshark (如果沒有或者不會的可以查看教程
//blog.csdn.net/u013613428/article/details/53156957)

設置配置 tcp.port == 7787,這個是我們后端(duan)的端(duan)口。




運行上述文件(jian) node request-error.js



我(wo)(wo)們(men)來找到我(wo)(wo)們(men)發送的(de)這條http的(de)請求(qiu)報(bao)文。中間(jian)那堆(dui)亂七八糟的(de)就(jiu)是我(wo)(wo)們(men)的(de)文件內(nei)容。

POST /files HTTP/1.1host: localhost:7787content-type: multipart/form-data; boundary=--------------------------437240798074408070374415content-length: 305Connection: close----------------------------437240798074408070374415Content-Disposition: form-data; name="file"Content-Type: application/octet-stream

.PNG
.
...
IHDR.............%.V.....PLTE......Ll.....	pHYs..........+.....
IDAT..c`.......qd.....IEND.B`.----------------------------437240798074408070374415--

可以看到上述報文。發現我們的(de)內容請(qing)求頭 Content-Type: application/octet-stream有錯(cuo)誤,我們上傳的(de)是圖片請(qing)求頭應該是image/png,并(bing)且也少了(le) filename="1.png"。

我們來思考一下,我們剛(gang)才用的(de)是fs.readFileSync(path.join(__dirname, '../1.png')) 這個(ge)函數返(fan)回的(de)是 Buffer,Buffer是什么樣的(de)呢?就是下面的(de)形(xing)式,不會包含任何文件相關的(de)信息,只(zhi)有(you)二進(jin)制流。

<Buffer 01 02>

所以我想到的是,需要指(zhi)定文件(jian)名以及文件(jian)格(ge)式,幸好 request 也給(gei)我們提(ti)供了這個選項(xiang)。

key: {    value:  fs.createReadStream('/dev/urandom'),
    options: {
      filename: 'topsecret.jpg',
      contentType: 'image/jpeg'
    }
}

可以指定options,因此正確的(de)代碼(ma)(ma)應(ying)該如下(省略不重要的(de)代碼(ma)(ma))

...request.post({    url: '//localhost:7787/files',    formData: {        file: {            value: stream,            options: {                filename: '1.png'
            }
        },
    }
});

我們通過抓包可以進行分析到,文件上傳的要點還是規范,大部分的問題,都可以通過規范模板來進行排查,是否構造出了規范的樣子。

Form-data

我們(men)再(zai)深入一些(xie),來看看 request 的源碼(ma), 他(ta)是怎么實現Node端的數據傳輸的。

打開源碼我們很容易地就可以找到關于 formData 這塊相關的內容
//github.com/request/request/blob/3.0/request.js#L21



就是利(li)用form-data,我們先來(lai)看看 formData 的方式(shi)。

const path = require('path');const FormData = require('form-data');const fs = require('fs');const http = require('http');const form = new FormData();
form.append('file', fs.readFileSync(path.join(__dirname, '../1.png')), {    filename: '1.png',    contentType: 'image/jpeg',
});const request = http.request({    method: 'post',    host: 'localhost',    port: '7787',    path: '/files',    headers: form.getHeaders()
});
form.pipe(request);
request.on('response', function(res) {    console.log(res.statusCode);
});

原生 Node

看完formData,可(ke)能感(gan)覺這(zhe)個(ge)封裝還是(shi)(shi)太(tai)高(gao)層(ceng)了,于是(shi)(shi)我打算對照規范手動來(lai)構造multipart/form-data請求方式來(lai)進行講(jiang)解。我們再來(lai)回顧一下規范。

Content-type: multipart/form-data, boundary=AaB03x--AaB03xcontent-disposition: form-data; name="field1"Joe Blow--AaB03xcontent-disposition: form-data; name="pics"; filename="file1.txt"Content-Type: text/plain

... contents of file1.txt ...--AaB03x--

我模擬上方,我用原生 Node 寫(xie)出了一個(ge)multipart/form-data 請求的方式。

主要分為4個部分

  • 構造(zao)請求header

  • 構造內容header

  • 寫入內容

  • 寫入結束分隔符

const path = require('path');const fs = require('fs');const http = require('http');// 定義一個分隔符,要確保唯一性const boundaryKey = '-------------------------461591080941622511336662';const request = http.request({    method: 'post',    host: 'localhost',    port: '7787',    path: '/files',    headers: {        'Content-Type': 'multipart/form-data; boundary=' + boundaryKey, // 在請求頭上加上分隔符        'Connection': 'keep-alive'
    }
});// 寫入內容頭部request.write(    `--${boundaryKey}\r\nContent-Disposition: form-data; name="file"; filename="1.png"\r\nContent-Type: image/jpeg\r\n\r\n`);// 寫入內容const fileStream = fs.createReadStream(path.join(__dirname, '../1.png'));
fileStream.pipe(request, { end: false });
fileStream.on('end', function () {    // 寫入尾部
    request.end('\r\n--' + boundaryKey + '--' + '\r\n');
});
request.on('response', function(res) {    console.log(res.statusCode);
});

至此,已經實現(xian)服務端上傳文件(jian)的方式(shi)。

Stream、Base64

由于(yu)這兩塊就是和Buffer的(de)轉化(hua),比較簡單(dan),我就不(bu)再重復描述了。可以作為留(liu)給大家的(de)作業,感(gan)興趣的(de)可以給我這個(ge)示(shi)例(li)代碼倉(cang)庫貢獻這兩個(ge)示(shi)例(li)。

// base64 to bufferconst b64string = /* whatever */;const buf = Buffer.from(b64string, 'base64');
// stream to buffer
function streamToBuffer(stream) {  
  return new Promise((resolve, reject) => {
    const buffers = [];
    stream.on('error', reject);
    stream.on('data', (data) => buffers.push(data))
    stream.on('end', () => resolve(Buffer.concat(buffers))
  });
}

小結

由于服務(wu)端沒有(you)像瀏覽器那樣 formData 的原(yuan)生(sheng)對(dui)象,因(yin)此服務(wu)端核心思路(lu)為構造出(chu)文(wen)件(jian)上傳的格式(header,filename等),然后寫入 buffer 。然后千(qian)萬(wan)別忘了用 wireshark進行驗證。

接收端

這一(yi)部分是針對 Node 端進行講解,對于那些 koa-body 等(deng)用慣(guan)了(le)(le)的同學,可能(neng)一(yi)樣不太清楚(chu)整個過程發(fa)生了(le)(le)什么?可能(neng)唯(wei)一(yi)比較清楚(chu)的是 ctx.request.files ??? 如果ctx.request.files 不存在,就會懵逼了(le)(le),可能(neng)也不太清楚(chu)它到(dao)底做了(le)(le)什么,文件(jian)流又是怎么解析的。

我(wo)還是要說到規(gui)范...請(qing)求端是按(an)照規(gui)范來(lai)構造請(qing)求..那么我(wo)們接收端自然是按(an)照規(gui)范來(lai)解析請(qing)求了。

Koa-body

const koaBody = require('koa-body');

app.use(koaBody({ multipart: true }));

我們來看看最常用的 koa-body,它的使用方式非常簡單,短短幾行,就能讓我們享受到文件上傳的簡單與快樂(其他源碼庫一樣的思路去尋找問題的本源) 可以帶著一個問題去閱讀,為什么用了它就能解析出文件?

尋求問題的本源,我們當然要打開 koa-body的源碼,koa-body 源碼很少只有211行,
//github.com/dlau/koa-body/blob/v4.1.1/index.js#L125 很容易地發現(xian)它(ta)其實(shi)是用了一(yi)個(ge)叫做formidable的(de)庫來解(jie)析files 的(de)。并且把解(jie)析好的(de)files 對象(xiang)賦值(zhi)到了 ctx.req.files。(所(suo)以說(shuo)大家不要(yao)一(yi)味死記 ctx.request.files, 注意查看文(wen)檔(dang),因為今(jin)天(tian)用 koa-body是 ctx.request.files 明天(tian)換個(ge)庫可能就是 ctx.request.body 了)

因此看完koa-body我們得出的結論是,koa-body的核心方法是formidable

Formidable

那么(me)讓我(wo)們(men)(men)繼續深(shen)入(ru),來看(kan)看(kan)formidable做了什么(me),我(wo)們(men)(men)首(shou)先來看(kan)它的目錄(lu)結構(gou)。

.
├── lib│   ├── file.js│   ├── incoming_form.js│   ├── index.js│   ├── json_parser.js│   ├── multipart_parser.js│   ├── octet_parser.js│   └── querystring_parser.js

看到(dao)這(zhe)個目錄(lu),我們大致(zhi)可以(yi)梳(shu)理出這(zhe)樣的(de)關系。

index.js
|
incoming_form.js
|type?
|
1.json_parser
2.multipart_parser
3.octet_parser
4.querystring_parser

由于(yu)源碼分析比(bi)(bi)較枯燥。因(yin)此(ci)我(wo)只摘錄(lu)比(bi)(bi)較重要(yao)(yao)的片段。由于(yu)我(wo)們(men)是分析文件上(shang)傳,所以我(wo)們(men)只需要(yao)(yao)關心multipart_parser 這個(ge)文件。

//github.com/node-formidable/formidable/blob/v1.2.1/lib/multipart_parser.js#L72

...
MultipartParser.prototype.write = function(buffer) {console.log(buffer);  var self = this,
      i = 0,
      len = buffer.length,
      prevIndex = this.index,
      index = this.index,
      state = this.state,
...

我們將它(ta)的 buffer 打印看看.

<Buffer 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 34 36 31 35 39 31 30 38 30 39 34 31 36 32 32 35 31 31 33 33 36 36 36 ... >
144
<Buffer 89 50 4e 47 0d 0a 1a 0a 00 00 00 0d 49 48 44 52 00 00 00 01 00 00 00 01 01 03 00 00 00 25 db 56 ca 00 00 00 06 50 4c 54 45 00 00 ff 80 80 80 4c 6c bf ... >
106
<Buffer 0d 0a 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 34 36 31 35 39 31 30 38 30 39 34 31 36 32 32 35 31 31 33 33 36 ... >

我們來看wireshark 抓到的包



我用紅色進行(xing)了分割(ge)標記(ji),對應的就是(shi)formidable所(suo)分割(ge)的片段(duan) ,所(suo)以說這(zhe)個包主要是(shi)將大段(duan)的 buffer 進行(xing)分割(ge),然后(hou)循環處理。

這里我還(huan)可(ke)以(yi)補充一(yi)下(xia),可(ke)能你對以(yi)上表(biao)非(fei)常陌生。左側是(shi)(shi)二進制流,每(mei)1個(ge)代表(biao)1個(ge)字(zi)節(jie),1字(zi)節(jie)=8位,上面的(de)(de) 2d 其(qi)實就是(shi)(shi)16進制的(de)(de)表(biao)示形式,用二進制表(biao)示就是(shi)(shi) 0010 1101,右側是(shi)(shi)ascii 碼用來可(ke)視化,但是(shi)(shi) assii 分(fen)可(ke)顯和非(fei)可(ke)顯示。有(you)部分(fen)是(shi)(shi)無法(fa)可(ke)視的(de)(de)。比如你所看到文(wen)件中有(you)需要小點,就是(shi)(shi)不可(ke)見字(zi)符。

你可以對照,ascii表對照表[7]來看。

我來總(zong)結一下(xia)formidable對(dui)于文件的(de)處理(li)流(liu)程。



原生 Node

好了,我們已經(jing)知道了文件(jian)處理的流程,那么我們自己(ji)來寫一個吧。

const fs = require('fs');const http = require('http');const querystring = require('querystring');const server = http.createServer((req, res) => {  if (req.url === "/files" && req.method.toLowerCase() === "post") {
    parseFile(req, res)
  }
})function parseFile(req, res) {
  req.setEncoding("binary");  let body = "";  let fileName = "";  // 邊界字符  let boundary = req.headers['content-type']
    .split('; ')[1]
    .replace("boundary=", "")
  
  req.on("data", function(chunk) {
    body += chunk;
  });
  req.on("end", function() {    // 按照分解符切分    const list = body.split(boundary);    let contentType = '';    let fileName = '';    for (let i = 0; i < list.length; i++) {      if (list[i].includes('Content-Disposition')) {        const data = list[i].split('\r\n');        for (let j = 0; j < data.length; j++) {          // 從頭部拆分出名字和類型          if (data[j].includes('Content-Disposition')) {            const info = data[j].split(':')[1].split(';');
            fileName = info[info.length - 1].split('=')[1].replace(/"/g, '');            console.log(fileName);
          }          if (data[j].includes('Content-Type')) {
            contentType = data[j];            console.log(data[j].split(':')[1]);
          }
        }
      }
    }    // 去除前面的請求頭    const start = body.toString().indexOf(contentType) + contentType.length + 4; // 有多\r\n\r\n    const startBinary = body.toString().substring(start);    const end = startBinary.indexOf("--" + boundary + "--") - 2; // 前面有多\r\n     // 去除后面的分隔符    const binary = startBinary.substring(0, end);    const bufferData = Buffer.from(binary, "binary");
    fs.writeFile(fileName, bufferData, function(err) {
      res.end("sucess");
    });
    ;
  })
}

server.listen(7787)

總結

相信有了以上的介紹,你不(bu)再對文件(jian)上傳(chuan)(chuan)有所(suo)懼怕, 對文件(jian)上傳(chuan)(chuan)整個過程都會比較清晰了,還不(bu)懂。。。。找我。

再次回(hui)顧下我(wo)們的(de)重點(dian):

請(qing)求(qiu)端(duan)出問題,瀏覽器(qi)端(duan)打(da)開 network 查(cha)看格(ge)式是否正(zheng)確(請(qing)求(qiu)頭,請(qing)求(qiu)體), 如果數據不夠詳細,打(da)開wireshark,對(dui)照我(wo)們(men)的規范標準(zhun),看下格(ge)式(請(qing)求(qiu)頭,請(qing)求(qiu)體)。

接收端(duan)出問題(ti)(ti),情況一(yi)就是請求(qiu)(qiu)端(duan)缺少(shao)信息,參考上面請求(qiu)(qiu)端(duan)出問題(ti)(ti)的(de)情況,情況二請求(qiu)(qiu)體(ti)內容錯(cuo)(cuo)誤(wu)(wu),如果說請求(qiu)(qiu)體(ti)內容是請求(qiu)(qiu)端(duan)自己(ji)構(gou)造(zao)的(de),那么需要檢查請求(qiu)(qiu)體(ti)是否是正(zheng)確(que)的(de)二進制流(例如上面的(de)blob構(gou)造(zao)的(de)時(shi)候,我一(yi)開始少(shao)了一(yi)個[],導致內容主體(ti)錯(cuo)(cuo)誤(wu)(wu))。

推薦JavaScript經典實例學習資料文章

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

作者:藍色的秋風

轉發鏈接:
//mp.weixin.qq.com/s/cruL9JGZNZQFrMSrzJJWiQ

聯系我們

聯系我們

在線咨詢:

郵(you)件:@QQ.COM

工作(zuo)時間:周一至周五(wu),8:30-21:30,節假日不休

關注(zhu)微信
關注微信
返(fan)回(hui)頂部