详解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 相关文章推荐
php中给js数组赋值方法
Mar 10 Javascript
JavaScript 事件对象介绍
Apr 13 Javascript
JQuery中Bind()事件用法分析
May 05 Javascript
Jquery EasyUI实现treegrid上显示checkbox并取选定值的方法
Apr 29 Javascript
jquery过滤特殊字符',防sql注入的实现方法
Aug 17 Javascript
JS实现的自动打字效果示例
Mar 10 Javascript
文本溢出插件jquery.dotdotdot.js使用方法详解
Jun 22 jQuery
浅谈Node.js ORM框架Sequlize之表间关系
Jul 24 Javascript
vue-cli脚手架config目录下index.js配置文件的方法
Mar 13 Javascript
Vue渲染过程浅析
Mar 14 Javascript
微信小程序 函数防抖 解决重复点击消耗性能问题实现代码
Sep 12 Javascript
Vue中foreach数组与js中遍历数组的写法说明
Jun 05 Vue.js
使用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 全文搜索和替换的实现代码
2008/07/29 PHP
laravel 框架结合关联查询 when()用法分析
2019/11/22 PHP
Ucren Virtual Desktop V2.0
2006/11/07 Javascript
javaScript checkbox 全选/反选及批量删除
2010/04/28 Javascript
js分解url参数(面向对象-极简主义法应用)
2012/08/09 Javascript
JS实现切换标签页效果实例代码
2013/11/01 Javascript
javaScript如何生成xmlhttp
2013/12/16 Javascript
javascript数组操作总结和属性、方法介绍
2014/04/05 Javascript
浅谈JavaScript数据类型及转换
2015/02/28 Javascript
JavaScript中的acos()方法使用详解
2015/06/14 Javascript
jquery.cookie实现的客户端购物车操作实例
2015/12/24 Javascript
原生js编写autoComplete插件
2016/04/13 Javascript
在Node.js中使用Javascript Generators详解
2016/05/05 Javascript
jQuery+CSS实现一个侧滑导航菜单代码
2016/05/09 Javascript
Javascript基础_简单比较undefined和null 值
2016/06/14 Javascript
针对后台列表table拖拽比较实用的jquery拖动排序
2016/10/10 Javascript
基于pako.js实现gzip的压缩和解压功能示例
2017/06/13 Javascript
微信小程序实现录音功能
2019/11/22 Javascript
浅析JavaScript 函数柯里化
2020/09/08 Javascript
原生js实现分页效果
2020/09/23 Javascript
用Python制作在地图上模拟瘟疫扩散的Gif图
2015/03/31 Python
20招让你的Python飞起来!
2016/09/27 Python
python石头剪刀布小游戏(三局两胜制)
2021/01/20 Python
十行代码使用Python写一个USB病毒
2019/06/21 Python
Python整数对象实现原理详解
2019/07/01 Python
python GUI库图形界面开发之PyQt5表格控件QTableView详细使用方法与实例
2020/03/01 Python
Python实现一个简单的毕业生信息管理系统的示例代码
2020/06/08 Python
python产生模拟数据faker库的使用详解
2020/11/04 Python
告诉你怎样写创业计划书
2014/01/27 职场文书
人事专员工作职责
2014/02/22 职场文书
幼儿园儿童节主持词
2014/03/21 职场文书
八项规定自查自纠报告及整改措施
2014/10/26 职场文书
《角的度量》教学反思
2016/02/18 职场文书
Python+Selenium实现读取网易邮箱验证码
2022/03/13 Python
Win11软件图标固定到任务栏
2022/04/19 数码科技
js前端面试常见浏览器缓存强缓存及协商缓存实例
2022/06/21 Javascript