详解axios在node.js中的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或者下载到本地学习

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

(0)

相关推荐

  • 简单谈谈axios中的get,post方法

    学习vue和nodejs的过程当中,涉及到了axios,今天为了测试,写了get和post两个方法来跟node服务端交互,结果因为header和参数弄了好久,在此记录一下,同时分享: 由于刚接触axios,在测试方法中,写的都是很简单的东西,不过能够实现基础功能,大神看到的话..非常欢迎指导.. //GET方法 axios.get(url, { params: { 'key': 'value' } }).then(function (response) { alert(''.concat(res

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

    前言: 最近因为做的东西需要用到网络请求库,之前接触过的只有request,很强大好用.但是这个项目中需要用到Promise,我又不想重新封装,于是选择了另一款库axios. 在node中,axios的get请求加上原生支持的Promise语法使用起来很方便,很丝滑,但是后面碰到了一个需求,就是要向另一个服务器post数据,并且这个数据是以form-data的形式post过去的,这时,问题就出现了. 问题: 当我想在node中使用axios以post的方式发送一张图片给某个server时,最先我

  • 详解如何使用Node.js实现热重载页面

    前言 前不久我结合browser-sync+gulp+gulp-nodemon实现了一款生产环境热更新(我之前理解有点偏差,应该定义为热更新,不是热重载)的项目脚手架,那么,今天我们将使用Node.js实现一个热重载页面. 那么,我今天就总结一下吧,以防止大家也跟我一样. 热重载 所谓热重载就是页面每次改动,不需要手动去刷新,可自动刷新. 热更新 浏览器的无刷新更新,允许在运行时替换,添加,删除各种模块,而无需进行完全刷新重新加载整个页面. 目的:加快开发速度,所以只适用于开发环境下使用. 思路

  • 详解如何在Node.js的httpServer中接收前端发送的arraybuffer数据

    最近使用了protobuf进行数据交互,发送在node.js接收前端的二进制数据出现了数据错误等问题.后来发现思路上面的问题,在req.on('data',()=>{})事件中的处理不适当才引发数据错乱.借此发 我先直接贴正确接收二进制数据代码 const server = http.createServer((req, res) => { if(req.method==='OPTIONS'){ res.setHeader("Access-Control-Allow-Origin&q

  • 详解Wondows下Node.js使用MongoDB的环境配置

    为了保存网站的用户数据和业务数据,通常需要一个数据库.MongoDB和Node.js特别般配,因为MongoDB是基于文档的非关系型数据库,文档是按BSON(JSON的轻量化二进制格式)存储的,增删改查等管理数据库的命令和JavaScript语法很像.如果你在Node.js里访问MongoDB的数据,会有我们是一家人的感觉,特别亲切. 我也准备使用MongoDB来作为我的数据库. MongoDB使用集合(collection)和文档(document)来描述和存储数据,collection就相当

  • 详解两个Node.js进程是如何通信

    目录 前言 不同电脑上的两个 Node.js 进程间通信 使用 TCP 套接字 使用 HTTP 协议 同一台电脑上两个 Node.js 进程间通信 使用内置 IPC 通道 使用自定义管道 总结 前言 两个 Node.js 进程之间如何进行通信呢?这里要分两种场景: 不同电脑上的两个 Node.js 进程间通信 同一台电脑上两个 Node.js 进程间通信 对于第一种场景,通常使用 TCP 或 HTTP 进行通信,而对于第二种场景,又分为两种子场景: Node.js 进程和自己创建的 Node.j

  • 详解如何使用Node.js编写命令工具——以vue-cli为例

    vue-cli全局安装之后,提供了vue命令和vue init.vue list.vue build三个子命令,通过命令可以搭建基于vue.js的脚手架项目.本文简单介绍一下这些命令是如何实现的. vue-cli的项目目录 如下图,由之前文章介绍,npm安装过程中,可以利用package.json中bin字段的配置,将bin目录下的命令文件软连到全局命令.也就是说在/usr/local/bin下生成了四个软连命令:vue.vue-build.vue-list.vue-init. vue命令的源码

  • 详解如何使用node.js的开发框架express创建一个web应用

    搭建环境 1.安装Express 按键:Windows+R=>输入cmd,打开命令行,输入 npm install -g express@3 我们需要用全局模式安装Express,因为只有这样,我们才能在命令行中使用它 注意:这里我只选用Express 3.x版本,要是你对Express3x版本熟悉了的话,你只需看一下Migrating from 3.x to 4.x 的文档即可过渡到Express 4.x 2.新建一个工程 继续打开命令行,输入 express -e murenziwei 注意

  • 详解一些适用于Node.js的命名约定

    序言 There are only two hard things in Computer Science: cache invalidation and naming things. -- Phil Karlton 我乔鲁诺·乔巴拿有一个梦想,便是要成为程序员巨星.但如果你看过我写的代码,便知道我还远远够不上"巨星"二字.我的代码中有许多不一致的命名: 常量的命名时而是全大写的(如WAIT_CONFIRM),时而是全小写的: 某个项目大部分命名用的是camel case(如TaskC

  • 详解node.js中的npm和webpack配置方法

    概述 Node.js用c++语言编写而成的,是一个基于chrome V8引擎的javascript运行环境,让javaScript的运行脱离浏览器服务端,可以使用javaScript语言书写服务器端代码 1.使用node来实现一个http服务器 下面创建了一个端口为8787的服务器.他与php,java等不同,像php本地还要基于阿帕奇服务器,node.js能用代码快速搭建一个服务器. // 引入http模块 var http = require("http"); // 调用http的

  • node.js中grunt和gulp的区别详解

    node.js中grunt和gulp的区别详解 自nodeJS登上前端舞台,自动化构建变得越来越流行.目前最流行的当属grunt和gulp,这两个光看名字挺像,功能也差不多,不过gulp能在grunt这位大哥如日中天的境况下开辟出自己的一片天地,有着她独到的优点. 易用 Gulp相比Grunt更简洁,而且遵循代码优于配置策略,维护Gulp更像是写代码. 高效 Gulp相比Grunt更有设计感,核心设计基于Unix流的概念,通过管道连接,不需要写中间文件. 高质量 Gulp的每个插件只完成一个功能

随机推荐