关于axios在node中post的使用
发布于 3 个月前 作者 chux0519 1712 次浏览 来自 分享

关于axios在node中的post使用

起因:

  • 最近做的东西需要用到网络请求库,之前接触过的只有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

5 回复

思路不错,用习惯request的人一般会选择 https://github.com/request/request-promise

@welchwsy 目瞪口呆,马克了

我之前测试过市面上几乎所有的request类型的库,也为request库提过一个解决内存泄露的PR已经合并。开始以为axios很好用,后来发现里面坑很多,记得当时form-data只能以json形式提交,很多国内的站不支持。后来换成了superagent,出自node界传奇人物TJ手笔(现已叛变到Go阵营), 各种问题全部能够通过配置或者使用插件解决,有很多插件上手即用,非常方便,原生支持promise,兼容前后端,也比axios轻量且插件机制非常灵活,比got之流直观方便,功能强悍。request就不多说了,臃肿强大略微麻烦。推荐楼主使用superagent。或者想要简单点的直接用isomorphic-fetch也行

另外还可以支持测试环境,直接用supertest测试,也是原生promise,还是很舒服的,自此再也不用axios之流了。而且发现request,axios这类能做的superagent都能做,最后写出的代码是最简单直观的

@andyhu 粗略看了一下superagent的文档,链式写法很畅快,而且支持Promise,issue区也很活跃,看了下贡献者,主力是TJ,总之是值得一试的库。axios现在其实也很灵活,但确实有坑,有些需求要自己读了源码才搞得懂。

回到顶部