詳解axios在node.js中的post使用
前言:
最近因?yàn)樽龅臇|西需要用到網(wǎng)絡(luò)請(qǐng)求庫(kù),之前接觸過(guò)的只有request,很強(qiáng)大好用。但是這個(gè)項(xiàng)目中需要用到Promise,我又不想重新封裝,于是選擇了另一款庫(kù)axios。
在node中,axios的get請(qǐng)求加上原生支持的Promise語(yǔ)法使用起來(lái)很方便,很絲滑,但是后面碰到了一個(gè)需求,就是要向另一個(gè)服務(wù)器post數(shù)據(jù),并且這個(gè)數(shù)據(jù)是以form-data的形式post過(guò)去的,這時(shí),問(wèn)題就出現(xiàn)了。
問(wèn)題:
當(dāng)我想在node中使用axios以post的方式發(fā)送一張圖片給某個(gè)server時(shí),最先我是嘗試這樣做:
方案一
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);
})
事實(shí)證明,這樣做是完全沒(méi)有用的,我嘗試向另一個(gè)服務(wù)器poststream,返回的總是錯(cuò)誤。然而,如果我使用request,下面這樣的代碼是完全沒(méi)有問(wèn)題的:
方案二
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??!
我打算簡(jiǎn)單的寫(xiě)一個(gè)服務(wù)器,用于打印HTTP請(qǐng)求,然后查看區(qū)別(別問(wèn)我為什么不用抓包工具,任性?。?,代碼呼之欲出:
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")
})
此時(shí),將url設(shè)置為:http://127.0.0.1:3000/,再分別執(zhí)行方案一和方案二 這時(shí)打印出了這樣的結(jié)果:
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' } }
===============================================
- 上面的是方案一,下面的是方案二
這時(shí)可以看出,方案一和二的差別最明顯的是content-type,是的,這也是決定了方案一不可行的因素。 既然是content-type導(dǎo)致的,那么方案一PLUS就比較明了了,查閱axios的文檔后,我決定手動(dòng)設(shè)置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);
})
- 這時(shí),請(qǐng)求是這樣的:
===============================
{ 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' } }
================================
貌似差別不大,但我先試著往服務(wù)器post數(shù)據(jù)時(shí),仍然返回錯(cuò)誤。實(shí)際上這時(shí)候沒(méi)有boundary,文件其實(shí)并沒(méi)有被綁定上去,所以現(xiàn)在仍然沒(méi)有解決問(wèn)題。至于boundary,這里有個(gè)鏈接非常能說(shuō)明問(wèn)題。
到這里,我們就要耐下心來(lái)好好思考了,區(qū)別就在于,request中能夠設(shè)置正確的請(qǐng)求頭,那么它是怎么辦到的呢,于是我開(kāi)始翻看request的源碼,發(fā)現(xiàn)了這一段:
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在初始化參數(shù)中的formData,其中調(diào)用了它自身的form()方法,追蹤這個(gè)函數(shù):
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
}
發(fā)現(xiàn)了request調(diào)用了另一個(gè)庫(kù)form-data,先通過(guò)self.form()創(chuàng)建出一個(gè)formData對(duì)象,再遍歷options里的formData項(xiàng),遞歸地將內(nèi)容通過(guò)formData的append方法放進(jìn)去,也就是說(shuō)是formData實(shí)現(xiàn)了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)})
但是,事實(shí)告訴我,我還是悲劇了,請(qǐng)求打印出來(lái)是這樣的:
===============================================
{ 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還是不對(duì),于是我再去翻axios的文檔和issue,發(fā)現(xiàn),默認(rèn)設(shè)置的content-type就是application/x-www-form-urlencoded,于是我判斷,一定還是要手動(dòng)設(shè)置headers的
于是,基于方案三,我又添加了和改動(dòng)了這兩行形成了方案四:
方案四
let header = {
'content-type': 'multipart/form-data'
}
axios.post(url,form,{headers:header}).then((response)=>{
console.log(response.data)
})
但結(jié)果還是不理想,直接設(shè)置content-type是不行的,因?yàn)橐獙⒋l(fā)送文件綁定,就一定會(huì)有boundary出現(xiàn),另外在方案三和方案四的請(qǐng)求中,出現(xiàn)了transfer-encoding這個(gè)值,關(guān)于這個(gè)chunked,可以參考MDN和這篇文章
一邊google一邊看文檔的我,發(fā)現(xiàn)formData的文檔中出現(xiàn)過(guò)form.getHeaders()的寫(xiě)法,于是方案五出現(xiàn)了:
方案五
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)})
但是結(jié)果表明,這樣還是不行,現(xiàn)在的請(qǐng)求是這樣:
===============================================
{ 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' } }
===============================================
但是我目前項(xiàng)目需求是,不使用chunked而采用content-length的方法來(lái)傳輸,這意味著,我要想辦法搞到form的長(zhǎng)度
在成功案例中,使用requests,于是我翻看了部分源碼: 在request/request.js里出現(xiàn)了
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來(lái)計(jì)算長(zhǎng)度,然后添加到headers中去
然后看看在axios里是如何做的: axios/lib/adapters/http.js里出現(xiàn)了
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;
}
下文并沒(méi)有出現(xiàn)else,所以,當(dāng)data是stream的時(shí)候,并沒(méi)有自動(dòng)設(shè)置content-length
所以,我需要在formData.getHeaders()后,再添加一個(gè)content-length的key
想要計(jì)算長(zhǎng)度,自然想到去看看源碼,于是在form-data/lib/form_data.js中出現(xiàn)了驚喜:
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已經(jīng)封裝好了得到長(zhǎng)度的方法,只不過(guò)它是異步的,不過(guò)沒(méi)關(guān)系,在實(shí)際項(xiàng)目中,可以將它手動(dòng)Promise化。最終方案的代碼也就自然出現(xiàn)了:
方案六:
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)})
})
這時(shí)的請(qǐng)求打印后是這樣的:
===============================================
{ 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' } }
===============================================
事實(shí)證明它是可以工作的。
更進(jìn)一步,我們把異步代碼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)})
總結(jié)
得到一個(gè)結(jié)論,多多看issue,多多看源碼,多多了解基礎(chǔ)知識(shí)(HTTP協(xié)議),對(duì)于問(wèn)題的解決十分重要。最后這一套的實(shí)驗(yàn)代碼放在github上了,需要研究研究的同學(xué)們可以看看:axios-request或者下載到本地學(xué)習(xí)
好了,以上就是這篇文章的全部?jī)?nèi)容了,希望本文的內(nèi)容對(duì)大家學(xué)習(xí)或者工作能帶來(lái)一定的幫助,如果有疑問(wèn)大家可以留言交流,謝謝大家對(duì)腳本之家的支持。
- 如何使用axios庫(kù)在Node.js中進(jìn)行代理請(qǐng)求(實(shí)踐案例)
- Node.js中Express框架使用axios同步請(qǐng)求(async+await)實(shí)現(xiàn)方法
- Node.js+Express+Vue+MySQL+axios的項(xiàng)目搭建全過(guò)程
- Node.js 使用axios讀寫(xiě)influxDB的方法示例
- node.js通過(guò)axios實(shí)現(xiàn)網(wǎng)絡(luò)請(qǐng)求的方法
- node.js中axios使用心得總結(jié)
- 如何在 Node.js 中使用 axios 配置代理并實(shí)現(xiàn)圖片并發(fā)下載
相關(guān)文章
node.js中優(yōu)雅的使用Socket.IO模塊的方法
Socket.IO是一個(gè)WebSocket庫(kù),包括了客戶端的js和服務(wù)器端的node.js,它的目標(biāo)是構(gòu)建可以在不同瀏覽器和移動(dòng)設(shè)備上使用的實(shí)時(shí)應(yīng)用,這篇文章主要介紹了node.js中優(yōu)雅的使用Socket.IO模塊,需要的朋友可以參考下2022-12-12
Node.js實(shí)現(xiàn)http請(qǐng)求服務(wù)與Mysql數(shù)據(jù)庫(kù)操作方法詳解
這篇文章主要介紹了Node.js實(shí)現(xiàn)http請(qǐng)求服務(wù)與Mysql數(shù)據(jù)庫(kù)操作方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)吧2022-10-10
利用nodeJs anywhere搭建本地服務(wù)器環(huán)境的方法
今天小編就為大家分享一篇利用nodeJs anywhere搭建本地服務(wù)器環(huán)境的方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-05-05
nodejs入門(mén)教程六:express模塊用法示例
這篇文章主要介紹了nodejs入門(mén)教程之express模塊用法,結(jié)合實(shí)例形式分析了express模塊的功能、創(chuàng)建、路由相關(guān)使用技巧,需要的朋友可以參考下2017-04-04
Nodejs從有門(mén)道無(wú)門(mén)菜鳥(niǎo)起飛必看教程
下面小編就為大家?guī)?lái)一篇Nodejs從有門(mén)道無(wú)門(mén)菜鳥(niǎo)起飛必看教程。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2016-07-07
Node.js安裝詳細(xì)步驟教程(Windows版)詳解
這篇文章主要介紹了Node.js安裝詳細(xì)步驟教程(Windows版),本文圖文并茂給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-09-09
小結(jié)Node.js中非阻塞IO和事件循環(huán)
本文針對(duì)在Node.js關(guān)鍵的兩個(gè)概念:非阻塞IO和事件循環(huán)進(jìn)行了適當(dāng)?shù)目偨Y(jié),需要的朋友可以參考下2014-09-09

