详解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 相关文章推荐
Javascript &amp; DHTML 实例编程(教程)基础知识
Jun 02 Javascript
一些常用的JS功能函数(2009-06-04更新)
Jun 04 Javascript
关于火狐(firefox)及ie下event获取的两种方法
Dec 27 Javascript
js判断undefined类型,undefined,null, 的区别详细解析
Dec 16 Javascript
屏蔽相应键盘按钮操作
Mar 10 Javascript
jQuery构造函数init参数分析
May 13 Javascript
Jsonp post 跨域方案
Jul 06 Javascript
javascript封装简单实现方法
Aug 11 Javascript
js判断主流浏览器类型和版本号的简单实现代码
May 26 Javascript
js获取腾讯视频ID的方法
Oct 03 Javascript
本地Bootstrap文件字体图标引入却无法显示问题的解决方法
Apr 18 Javascript
layer.confirm点击第一个按钮关闭弹出框的方法
Sep 09 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
PHP数组排序函数合集 以及它们之间的联系分析
2013/06/27 PHP
PHP URL路由类实例
2013/11/12 PHP
完整删除ecshop中获取店铺信息的API
2014/12/24 PHP
PHP实现的曲线统计图表示例
2016/11/10 PHP
laravel中Redis队列监听中断的分析
2020/09/14 PHP
jquery tools之tabs 选项卡/页签
2009/07/25 Javascript
javascript中强制执行toString()具体实现
2013/04/27 Javascript
JQuery为页面Dom元素绑定事件及解除绑定方法
2014/04/23 Javascript
Node.js文件操作详解
2014/08/16 Javascript
jquery插件jquery.confirm弹出确认消息
2015/12/22 Javascript
jQuery模拟下拉框选择对应菜单的内容
2017/03/07 Javascript
node.js+express+mySQL+ejs+bootstrop实现网站登录注册功能
2018/01/12 Javascript
vue better-scroll插件使用详解
2018/01/25 Javascript
js取0-9随机取4个数不重复的数字代码实例
2019/03/27 Javascript
详解iframe跨域的几种常用方法(小结)
2019/04/29 Javascript
你知道JavaScript Symbol类型怎么用吗
2020/01/08 Javascript
跟老齐学Python之dict()的操作方法
2014/09/24 Python
在Linux上安装Python的Flask框架和创建第一个app实例的教程
2015/03/30 Python
Python tkinter的grid布局及Text动态显示方法
2018/10/11 Python
对python pandas 画移动平均线的方法详解
2018/11/28 Python
Python TestCase中的断言方法介绍
2019/05/02 Python
如何不用安装python就能在.NET里调用Python库
2019/07/12 Python
Python多继承以及MRO顺序的使用
2019/11/11 Python
python实现俄罗斯方块小游戏
2020/04/24 Python
基于python纯函数实现井字棋游戏
2020/05/27 Python
python爬虫智能翻页批量下载文件的实例详解
2021/02/02 Python
英国家喻户晓的折扣商场:TK Maxx
2017/05/26 全球购物
Woolworth官网:澳洲第一大超市
2017/06/25 全球购物
行政主管岗位职责
2013/11/18 职场文书
升国旗仪式主持词
2014/03/19 职场文书
测控技术自荐信
2014/06/05 职场文书
真诚的求职信
2014/07/04 职场文书
租房协议书范文
2014/08/20 职场文书
2014迎国庆标语大全
2014/09/19 职场文书
2015新教师教学工作总结
2015/07/22 职场文书
 分享一个Python 遇到数据库超好用的模块
2022/04/06 Python