详解axios在node.js中的post使用
前言:
最近因为做的东西需要用到网络请求库,之前接触过的只有request,很强大好用。但是这个项目中需要用到Promise,我又不想重新封装,于是选择了另一款库axios。
在node中,axios的get请求加上原生支持的Promise语法使用起来很方便,很丝滑,但是后面碰到了一个需求,就是要向另一个服务器post数据,并且这个数据是以form-data的形式post过去的,这时,问题就出现了。
问题:
当我想在node中使用axios以post的方式发送一张图片给某个server时,最先我是尝试这样做:
方案一
letdata=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,下面这样的代码是完全没有问题的:
方案二
letdata=fs.createReadStream(__dirname+'/test.jpg') letform={ type:"image", media:data } request.post({url:url,formData:form},(err,res,body)=>{ if(err)console.log(err) console.log(body) })
探索:
于是,我陷入了思考,WTF!!
我打算简单的写一个服务器,用于打印HTTP请求,然后查看区别(别问我为什么不用抓包工具,任性!),代码呼之欲出:
importKoafrom'koa' constapp=newKoa() app.use(ctx=>{ console.log("===============================================") console.log(ctx.request) console.log("===============================================") ctx.body={foo:"bar"} }) app.listen(3000,()=>{ console.log("listeningon3000port") })
此时,将url设置为:http://127.0.0.1:3000/,再分别执行方案一和方案二这时打印出了这样的结果:
listeningon3000port =============================================== {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,于是乎:
letdata=fs.createReadStream(__dirname+'/test.jpg') letheader={ '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){ varformData=options.formData varrequestForm=self.form() varappendFormValue=function(key,value){ if(value&&value.hasOwnProperty('value')&&value.hasOwnProperty('options')){ requestForm.append(key,value.value,value.options) }else{ requestForm.append(key,value) } } for(varformKeyinformData){ if(formData.hasOwnProperty(formKey)){ varformValue=formData[formKey] if(formValueinstanceofArray){ for(varj=0;j这一段是request在初始化参数中的formData,其中调用了它自身的form()方法,追踪这个函数:
Request.prototype.form=function(form){ varself=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=(typeofform==='string') ?self._qs.rfc3986(form.toString('utf8')) :self._qs.stringify(form).toString('utf8') returnself } //createform-dataobject self._form=newFormData() self._form.on('error',function(err){ err.message='form-data:'+err.message self.emit('error',err) self.abort() }) returnself._form }发现了request调用了另一个库form-data,先通过self.form()创建出一个formData对象,再遍历options里的formData项,递归地将内容通过formData的append方法放进去,也就是说是formData实现了post文件,于是乎,我在axios中插入formData,形成了方案三:
方案三:
letdata=fs.createReadStream(__dirname+'/test.jpg') letform=newFormData() 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的
于是,基于方案三,我又添加了和改动了这两行形成了方案四:
方案四
letheader={ '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()的写法,于是方案五出现了:
方案五
letdata=fs.createReadStream(__dirname+'/test.jpg') letform=newFormData() 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里出现了
functionsetContentLength(){ if(isTypedArray(self.body)){ self.body=newBuffer(self.body) } if(!self.hasHeader('content-length')){ varlength if(typeofself.body==='string'){ length=Buffer.byteLength(self.body) } elseif(Array.isArray(self.body)){ length=self.body.reduce(function(a,b){returna+b.length},0) } else{ length=self.body.length } if(length){ self.setHeader('content-length',length) }else{ self.emit('error',newError('Argumenterror,options.body.')) } } }它采用Buffer来计算长度,然后添加到headers中去
然后看看在axios里是如何做的:axios/lib/adapters/http.js里出现了
if(data&&!utils.isStream(data)){ if(utils.isArrayBuffer(data)){ data=newBuffer(newUint8Array(data)); }elseif(utils.isString(data)){ data=newBuffer(data,'utf-8'); }else{ returnreject(createError( 'Dataaftertransformationmustbeastring,anArrayBuffer,oraStream', config )); } //AddContent-Lengthheaderifdataexists 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){ varknownLength=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化。最终方案的代码也就自然出现了:
方案六:
letdata=fs.createReadStream(__dirname+'/test.jpg') letform=newFormData() form.append('type','image') form.append('media',data,'test.jpg') form.getLength((err,length)=>{ if(err)console.log(err) letheaders=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一下,得到最终方案:
最终方案
letdata=fs.createReadStream(__dirname+'/test.jpg') letform=newFormData() form.append('type','image') form.append('media',data,'test.jpg') letgetHeaders=(form=>{ returnnewPromise((resolve,reject)=>{ form.getLength((err,length)=>{ if(err)reject(err) letheaders=Object.assign({'Content-Length':length},form.getHeaders()) resolve(headers) }) }) }) getHeaders(form) .then(headers=>{ returnaxios.post(url,form,{headers:headers}) }) .then((response)=>{ console.log(response.data) }) .catch(e=>{console.log(e)})总结
得到一个结论,多多看issue,多多看源码,多多了解基础知识(HTTP协议),对于问题的解决十分重要。最后这一套的实验代码放在github上了,需要研究研究的同学们可以看看:axios-request或者下载到本地学习
好了,以上就是这篇文章的全部内容了,希望本文的内容对大家学习或者工作能带来一定的帮助,如果有疑问大家可以留言交流,谢谢大家对毛票票的支持。