详解axios在node.js中的post使用


Posted in Javascript onApril 27, 2017

前言:

最近因为做的东西需要用到网络请求库,之前接触过的只有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或者下载到本地学习

好了,以上就是这篇文章的全部内容了,希望本文的内容对大家学习或者工作能带来一定的帮助,如果有疑问大家可以留言交流,谢谢大家对三水点靠木的支持。

Javascript 相关文章推荐
Extjs EditorGridPanel中ComboBox列的显示问题
Jul 04 Javascript
JavaScript中的公有、私有、特权和静态成员用法分析
Nov 20 Javascript
angularJS与bootstrap结合实现动态加载弹出提示内容
Oct 16 Javascript
不用一句js代码初始化组件
Jan 27 Javascript
jQuery实现表格隔行及滑动,点击时变色的方法【测试可用】
Aug 20 Javascript
微信小程序 实现列表项滑动显示删除按钮的功能
Apr 13 Javascript
Vue和Bootstrap的整合思路详解
Jun 30 Javascript
Angular2生命周期钩子函数的详细介绍
Jul 10 Javascript
js 获取html5的data属性实现方法
Jul 28 Javascript
Vue项目报错:Uncaught SyntaxError: Unexpected token
Nov 10 Javascript
解决layui的form里的元素进行动态生成,验证失效的问题
Sep 14 Javascript
JavaScript实现拖动对话框效果的实现代码
Oct 12 Javascript
使用jQuery,Angular实现登录界面验证码详解
Apr 27 #jQuery
js绑定事件和解绑事件
Apr 27 #Javascript
详解ECMAScript6入门--Class对象
Apr 27 #Javascript
利用Vue v-model实现一个自定义的表单组件
Apr 27 #Javascript
详解Vue 普通对象数据更新与 file 对象数据更新
Apr 26 #Javascript
微信小程序实现给循环列表添加点击样式实例
Apr 26 #Javascript
微信小程序 request接口的封装实例代码
Apr 26 #Javascript
You might like
单一index.php实现PHP任意层级文件夹遍历(Zjmainstay原创)
2012/07/31 PHP
七款最流行的PHP本地服务器分享
2013/02/19 PHP
PHP-FPM运行状态的实时查看及监控详解
2016/11/18 PHP
CakePHP框架Model函数定义方法示例
2017/08/04 PHP
php5.x禁用eval的操作方法
2018/10/19 PHP
Laravel数据库读写分离配置的方法
2019/10/13 PHP
PHP Swoole异步Redis客户端实现方法示例
2019/10/24 PHP
php实现文件上传基本验证
2020/03/04 PHP
javascript的事件描述
2006/09/08 Javascript
让iframe子窗体取父窗体地址栏参数(querystring)
2009/10/13 Javascript
JavaScript 事件冒泡简介及应用
2010/01/11 Javascript
关于eval 与new Function 到底该选哪个?
2013/04/17 Javascript
jquery 添加节点的几种方法介绍
2013/09/04 Javascript
jquery和ajax的关系详细介绍
2013/11/29 Javascript
跟我学习javascript的prototype原型和原型链
2015/11/18 Javascript
BootStrap使用file-input插件上传图片的方法
2016/09/05 Javascript
Vue.js表单控件实践
2016/10/27 Javascript
原生js仿浏览器滚动条效果
2017/03/02 Javascript
js仿网易表单及时验证功能
2017/03/07 Javascript
聊聊JavaScript如何实现继承及特点
2017/04/07 Javascript
深入理解Vue生命周期、手动挂载及挂载子组件
2017/09/27 Javascript
Vue 换肤的示例实践
2018/01/23 Javascript
JS实现可针对算术表达式求值的计算器功能示例
2018/09/04 Javascript
Python Tkinter基础控件用法
2014/09/03 Python
web.py在SAE中的Session问题解决方法(使用mysql存储)
2015/06/24 Python
python 连接各类主流数据库的实例代码
2018/01/30 Python
Windows下实现将Pascal VOC转化为TFRecords
2020/02/17 Python
如何通过python检查文件是否被占用
2020/12/18 Python
Expedia瑞典官网:预订度假屋、酒店、汽车租赁、机票等
2021/01/23 全球购物
护士进修自我鉴定
2014/02/07 职场文书
企业安全生产标语
2014/06/06 职场文书
员工2014年度工作总结
2014/12/09 职场文书
2015年党建工作目标责任书
2015/05/08 职场文书
三八妇女节主持词
2015/07/04 职场文书
心理学培训心得体会
2016/01/22 职场文书
Oracle中日期的使用方法实例
2022/07/07 Oracle