您好,登錄后才能下訂單哦!
前言:
最近因為做的東西需要用到網絡請求庫,之前接觸過的只有request,很強大好用。但是這個項目中需要用到Promise,我又不想重新封裝,于是選擇了另一款庫axios。
在node中,axios的get請求加上原生支持的Promise語法使用起來很方便,很絲滑,但是后面碰到了一個需求,就是要向另一個服務器post數據,并且這個數據是以form-data的形式post過去的,這時,問題就出現了。
問題:
當我想在node中使用axios以post的方式發送一張圖片給某個server時,最先我是嘗試這樣做:
方案一
let data = fs.createReadStream(__dirname + '/test.jpg') axios.post(url,{media:data,type:"image"}) .then(function (response) { console.log(response.data); }) .catch(function (error) { console.log(error); })
事實證明,這樣做是完全沒有用的,我嘗試向另一個服務器poststream,返回的總是錯誤。然而,如果我使用request,下面這樣的代碼是完全沒有問題的:
方案二
let data = fs.createReadStream(__dirname + '/test.jpg') let form = { type:"image", media:data } request.post({url:url,formData:form},(err,res,body)=>{ if(err) console.log(err) console.log(body) })
探索:
于是,我陷入了思考,WTF!!
我打算簡單的寫一個服務器,用于打印HTTP請求,然后查看區別(別問我為什么不用抓包工具,任性!),代碼呼之欲出:
import Koa from 'koa' const app = new Koa() app.use(ctx=>{ console.log("===============================================") console.log(ctx.request) console.log("===============================================") ctx.body = {foo:"bar"} }) app.listen(3000,()=>{ console.log("listening on 3000 port") })
此時,將url設置為:http://127.0.0.1:3000/,再分別執行方案一和方案二 這時打印出了這樣的結果:
listening on 3000 port =============================================== { method: 'POST', url: '/', header: { accept: 'application/json, text/plain, */*', 'content-type': 'application/json;charset=utf-8', 'user-agent': 'axios/0.14.0', 'content-length': '587', host: '127.0.0.1:3000', connection: 'close' } } =============================================== =============================================== { method: 'POST', url: '/', header: { host: '127.0.0.1:3000', 'content-type': 'multipart/form-data; boundary=--------------------------949095406788084443059291', 'content-length': '186610', connection: 'close' } } ===============================================
- 上面的是方案一,下面的是方案二
這時可以看出,方案一和二的差別最明顯的是content-type,是的,這也是決定了方案一不可行的因素。 既然是content-type導致的,那么方案一PLUS就比較明了了,查閱axios的文檔后,我決定手動設置content-type,于是乎:
let data = fs.createReadStream(__dirname + '/test.jpg') let header = { 'content-type': 'multipart/form-data' } axios.post(url,{media:data,type:"image"},{headers:header}) .then(function (response) { console.log(response.data); }) .catch(function (error) { console.log(error); })
- 這時,請求是這樣的:
=============================== { method: 'POST', url: '/', header: { accept: 'application/json, text/plain, */*', 'content-type': 'multipart/form-data', 'user-agent': 'axios/0.14.0', 'content-length': '587', host: '127.0.0.1:3000', connection: 'close' } } ================================
貌似差別不大,但我先試著往服務器post數據時,仍然返回錯誤。實際上這時候沒有boundary,文件其實并沒有被綁定上去,所以現在仍然沒有解決問題。至于boundary,這里有個鏈接非常能說明問題。
到這里,我們就要耐下心來好好思考了,區別就在于,request中能夠設置正確的請求頭,那么它是怎么辦到的呢,于是我開始翻看request的源碼,發現了這一段:
if (options.formData) { var formData = options.formData var requestForm = self.form() var appendFormValue = function (key, value) { if (value && value.hasOwnProperty('value') && value.hasOwnProperty('options')) { requestForm.append(key, value.value, value.options) } else { requestForm.append(key, value) } } for (var formKey in formData) { if (formData.hasOwnProperty(formKey)) { var formValue = formData[formKey] if (formValue instanceof Array) { for (var j = 0; j < formValue.length; j++) { appendFormValue(formKey, formValue[j]) } } else { appendFormValue(formKey, formValue) } } } }
這一段是request在初始化參數中的formData,其中調用了它自身的form()
方法,追蹤這個函數:
Request.prototype.form = function (form) { var self = this if (form) { if (!/^application\/x-www-form-urlencoded\b/.test(self.getHeader('content-type'))) { self.setHeader('content-type', 'application/x-www-form-urlencoded') } self.body = (typeof form === 'string') ? self._qs.rfc3986(form.toString('utf8')) : self._qs.stringify(form).toString('utf8') return self } // create form-data object self._form = new FormData() self._form.on('error', function(err) { err.message = 'form-data: ' + err.message self.emit('error', err) self.abort() }) return self._form }
發現了request調用了另一個庫form-data,先通過self.form()
創建出一個formData對象,再遍歷options里的formData項,遞歸地將內容通過formData的append方法放進去,也就是說是formData實現了post文件,于是乎,我在axios中插入formData,形成了方案三:
方案三:
let data = fs.createReadStream(__dirname + '/test.jpg') let form = new FormData() form.append('type','image') form.append('media',data,'test.jpg') axios.post(url,form).then((response)=>{ console.log(response.data) }) .catch(e=>{console.log(e)})
但是,事實告訴我,我還是悲劇了,請求打印出來是這樣的:
=============================================== { method: 'POST', url: '/', header: { accept: 'application/json, text/plain, */*', 'content-type': 'application/x-www-form-urlencoded', 'user-agent': 'axios/0.14.0', host: '127.0.0.1:3000', connection: 'close', 'transfer-encoding': 'chunked' } } ===============================================
再次content-type還是不對,于是我再去翻axios的文檔和issue,發現,默認設置的content-type就是application/x-www-form-urlencoded,于是我判斷,一定還是要手動設置headers的
于是,基于方案三,我又添加了和改動了這兩行形成了方案四:
方案四
let header = { 'content-type': 'multipart/form-data' } axios.post(url,form,{headers:header}).then((response)=>{ console.log(response.data) })
但結果還是不理想,直接設置content-type是不行的,因為要將待發送文件綁定,就一定會有boundary出現,另外在方案三和方案四的請求中,出現了transfer-encoding這個值,關于這個chunked,可以參考MDN和這篇文章
一邊google一邊看文檔的我,發現formData的文檔中出現過form.getHeaders()
的寫法,于是方案五出現了:
方案五
let data = fs.createReadStream(__dirname + '/test.jpg') let form = new FormData() form.append('type','image') form.append('media',data,'test.jpg') axios.post(url,form,{headers:form.getHeaders()}).then((response)=>{ console.log(response.data) }) .catch(e=>{console.log(e)})
但是結果表明,這樣還是不行,現在的請求是這樣:
=============================================== { method: 'POST', url: '/', header: { accept: 'application/json, text/plain, */*', 'content-type': 'multipart/form-data; boundary=--------------------------171407872885673042671614', 'user-agent': 'axios/0.14.0', host: '127.0.0.1:3000', connection: 'close', 'transfer-encoding': 'chunked' } } ===============================================
但是我目前項目需求是,不使用chunked而采用content-length的方法來傳輸,這意味著,我要想辦法搞到form的長度
在成功案例中,使用requests,于是我翻看了部分源碼: 在request/request.js里出現了
function setContentLength () { if (isTypedArray(self.body)) { self.body = new Buffer(self.body) } if (!self.hasHeader('content-length')) { var length if (typeof self.body === 'string') { length = Buffer.byteLength(self.body) } else if (Array.isArray(self.body)) { length = self.body.reduce(function (a, b) {return a + b.length}, 0) } else { length = self.body.length } if (length) { self.setHeader('content-length', length) } else { self.emit('error', new Error('Argument error, options.body.')) } } }
它采用Buffer來計算長度,然后添加到headers中去
然后看看在axios里是如何做的: axios/lib/adapters/http.js里出現了
if (data && !utils.isStream(data)) { if (utils.isArrayBuffer(data)) { data = new Buffer(new Uint8Array(data)); } else if (utils.isString(data)) { data = new Buffer(data, 'utf-8'); } else { return reject(createError( 'Data after transformation must be a string, an ArrayBuffer, or a Stream', config )); } // Add Content-Length header if data exists headers['Content-Length'] = data.length; }
下文并沒有出現else,所以,當data是stream的時候,并沒有自動設置content-length
所以,我需要在formData.getHeaders()后,再添加一個content-length的key
想要計算長度,自然想到去看看源碼,于是在form-data/lib/form_data.js中出現了驚喜:
FormData.prototype.getLength = function(cb) { var knownLength = this._overheadLength + this._valueLength; if (this._streams.length) { knownLength += this._lastBoundary().length; } if (!this._valuesToMeasure.length) { process.nextTick(cb.bind(this, null, knownLength)); return; } asynckit.parallel(this._valuesToMeasure, this._lengthRetriever, function(err, values) { if (err) { cb(err); return; } values.forEach(function(length) { knownLength += length; }); cb(null, knownLength); }); };
formData已經封裝好了得到長度的方法,只不過它是異步的,不過沒關系,在實際項目中,可以將它手動Promise化。最終方案的代碼也就自然出現了:
方案六:
let data = fs.createReadStream(__dirname + '/test.jpg') let form = new FormData() form.append('type','image') form.append('media',data,'test.jpg') form.getLength((err,length)=>{ if(err) console.log(err) let headers = Object.assign({'Content-Length':length},form.getHeaders()) axios.post(url,form,{headers:headers}).then((response)=>{ console.log(response.data) }) .catch(e=>{console.log(e)}) })
這時的請求打印后是這樣的:
=============================================== { method: 'POST', url: '/', header: { accept: 'application/json, text/plain, */*', 'content-type': 'multipart/form-data; boundary=--------------------------424584867554529984619649', 'content-length': '186610', 'user-agent': 'axios/0.14.0', host: '127.0.0.1:3000', connection: 'close' } } ===============================================
事實證明它是可以工作的。
更進一步,我們把異步代碼Promise一下,得到最終方案:
最終方案
let data = fs.createReadStream(__dirname + '/test.jpg') let form = new FormData() form.append('type','image') form.append('media',data,'test.jpg') let getHeaders = (form=>{ return new Promise((resolve,reject)=>{ form.getLength((err,length)=>{ if(err) reject(err) let headers = Object.assign({'Content-Length':length},form.getHeaders()) resolve(headers) }) }) }) getHeaders(form) .then(headers=>{ return axios.post(url,form,{headers:headers}) }) .then((response)=>{ console.log(response.data) }) .catch(e=>{console.log(e)})
總結
得到一個結論,多多看issue,多多看源碼,多多了解基礎知識(HTTP協議),對于問題的解決十分重要。最后這一套的實驗代碼放在github上了,需要研究研究的同學們可以看看:axios-request或者下載到本地學習
好了,以上就是這篇文章的全部內容了,希望本文的內容對大家學習或者工作能帶來一定的幫助,如果有疑問大家可以留言交流,謝謝大家對億速云的支持。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。