最近因为做的东西需要用到网络请求库,之前接触过的只有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设置为::3000/,再分别执行方案一和方案二 这时打印出了这样的结果:
listening on 3000 port =============================================== { method: 'POST', url: 'https://www.jb51.net/', 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: 'https://www.jb51.net/', 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: 'https://www.jb51.net/', 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()方法,追踪这个函数: