willson-wang / Blog

随笔
https://blog.willson-wang.com/
MIT License
70 stars 10 forks source link

xhr的使用姿势 #40

Open willson-wang opened 6 years ago

willson-wang commented 6 years ago

之前在工作中的时候,使用axios,jquery等xhr工具的时候,总是有疑问;如jquery的post请求,当请求头的content-type为'application/x-www-form-urlencoded'时,data可以直接传对象,而不需要对对象进行JSON.stringify处理,当请求头的content-type为'application/json'时,data不可以直接传对象,而是需要JSON.stringify处理;使用axios的post方法时,content-type为'application/json'时,data可以直接传对象,而不需要qs.stringify等处理;当content-type为'application/x-www-form-urlencoded'时,data又需要qs.stringify等处理;于是抽时间来总结一下

首先jquery也好axios也好,其它的类库也好,原理都是使用xhr对象来进行ajax请求;

function request () {
      return new Promise((resolve) => {
        const xhr = new XMLHttpRequest();

        const params1 = "token=acc12356&url=post-form";

        const fileEle = document.getElementById('file');
        const params2 = new FormData();
        params2.append('token', 'acc12356');
        params2.append('url', 'post-form');
        params2.append('file', fileEle.files[0]);

        const params3 = JSON.stringify({token: 'acc12356', url: 'post-form'})

        xhr.open('POST', '/post-json', true);

        /*
          当设置请求头为'application/x-www-form-urlencoded'时,
            1. 当参数为params1,key value对的形式,也是表单默认的参数发送方式,后端能够正常拿到值
            2. 当参数为params2,form-data的形式,后端能够正常拿到值,但是content-type不会变成multipart/form-data;
            3. 当参数为params3,对象的形式,如果不对对象进行处理,则后端收到的是{ 'object Object': '' },如果对对象先进行JSON.stringify则后端接收到的参数是把整个对象做为了一个key的对象{ '{"token":"acc12356","url":"post-form"}': '' }
          所以如果content-type为'application/x-www-form-urlencoded',则我们最终传入send放到的参数会被转换成字符串,form-data除外,如果字符串是key value值对的形式则后端可以正常拿到,如果不是则会把整个字符串作为key,所以如果是表单的形式来提交,需要对参数转换成key value对
        **/ 

        /*
          当设置请求头为'application/json'时,
            1. 当参数为params1,key value对的形式,也是表单默认的参数发送方式,直接报错
            2. 当参数为params2,form-data的形式,直接报错
            3. 当参数为params3,对象的形式,如果不对对象进行处理,则后端收到的是{ 'object Object': '' },如果对对象先进行JSON.stringify则后端接收到的参数是正常的对象{ token: 'acc12356', url: 'post-form' }
          所以如果content-type为'application/json',则我们最终传入send放到的参数会被转换成字符串,form-data除外,如果字符串是json字符串的形式则后端可以正常拿到,如果不是则会直接报错
        **/ 
        xhr.setRequestHeader('content-type', 'application/json; charset=utf-8');

        xhr.onload = function(e) { 
          if(this.status == 200||this.status == 304){
            resolve(this.responseText);
          }
        };

        // 如果没有设置content-type,如传入的是一个字符串则content-type会被设为text/plain;charset=UTF-8
        // 如果没有设置content-type,如传入的是一个form-data则content-type会被设为multipart/form-data; boundary=----WebKitFormBoundarygbyZU9wiB2Uc300W
        // 如果没有设置content-type,如传入的是一个对象(会被转换为[object object]),如果传入的是一个JSON字符串则会被转换为JSON对象,但是content-type会被设置为text/plain;charset=UTF-8,但是后端拿不到参数
        xhr.send(params3);
      })
    }

这里需要注意的是,send方法可以传入的参数有几类;content-type与send传入的参数又有什么关系;

send传入的参数有ArrayBuffer,Blob,Document,DOMString,FormData,null共6类;我们只需要了解常用的DOMString,FormData类型;

content-type与send传入的参数关系是:当content-type为'application/x-www-form-urlencoded',这是send的参数需要是key value对这样后端才能接收到参数;当content-type为'application/json'时send的参数需要时json字符串对象,这样后端才能在req.body内获取到参数;

至于jquery的post方法,当content-type为'application/x-www-form-urlencoded'时(jquery post默认的content-type),data可以直接传对象,而不需要对对象进行JSON.stringify处理,反之的原因是,当content-type为'application/x-www-form-urlencoded'时,send方法需要传入的是key value值对,而通过jquery源码是能够看到内部是对传入的参数data有做处理的,如果传入的data是一个对象,则会转化成key,value值对,如果传入的直接是字符串是不做处理的,所以为什么jquery的post方法content-type不同时,处理data的方式不同;

1. jquery post content-type: 'application/x-www-form-urlencoded'

 const postFormJq = function (data) {
      return new Promise((resolve) => {
        // 默认表单请求, 这里data不需要JSON.stringify的原因与postJsonJq的是一样的
        $.post('/post-form', data, (res) => {
          resolve(res);
        });
      })
    }

2. jquery post content-type: 'application/json'

const postJsonJq = function (data) {
      return new Promise((resolve) => {
        // 单个传参数的形式,不支持修改content-type,需要传对象的形式才支持
        // $.post('/post-json', data, (res) => {
        //   resolve(res);
        // });

        $.post({
          url: '/post-json',
          data: JSON.stringify(data), // jquery直接传对象会报错的原因,是jquery内部有一段代码处理,如传入的data不是字符串,则会被转换为key value字符串对,而application/json;是不接受这种参数的,所以报错
          contentType: 'application/json; charset=utf-8',
          success: (res) => {
            resolve(res);
          }
        })
      })
    }

3. jquery get query内取参数

const getListJq = function (params) {
      return new Promise((resolve) => {
        $.get('/get-list', params, (data) => {
          resolve(data);
        });
      })
    }

4. jquery get params内取参数

const getListParamsJq = function (params) {
      return new Promise((resolve) => {
        $.get('/get-list-params/1234563/akcnal',{}, (data) => {
          resolve(data);
        });
      })
    }

至于axios的post方法,当content-type为'application/json'时(axios默认的content-type),data可以直接传对象,而不需要qs.stringfily处理的原因是,当content-type为'application/json'时,send方法需要传入的参数是json字符串,而通过axios源码可以发现,axios内部是对传入的data参数做了JSON.stringify处理的,所以当传入的是对象时,会进行JSON.stringify处理,如果直接传入的是字符串是不做处理的,所以为什么axios的post方法content-type不同时,处理data的方式不同;

1. axios post application/x-www-form-urlencoded

const postForm = function (data) {
      // 因为axios默认的content-type:application/json,所以如果我们需要使用application/x-www-form-urlencoded表单的方式来发送数据给后端的话,有以下几种方式
      // 方法一使用URLSearchParams来编码参数,当axios判断参数是,注意这种方式,通过req.body无法获取
      // const params = new URLSearchParams();
      // for (let prop in data) {
      //   params.append(prop, data[prop]);
      // }
      // console.log('params', params);
      // return axios.post('/post-form', params);

      // 第二种方法使用qs模块的stringify方法对参数进行处理
      // console.log(Qs.stringify(data));
      // return axios.post('/post-form', Qs.stringify(data));

      // 第三种方式,设置content-type,不加Qs.stringfy处理后端拿到的数据是这样的{ '{"token":"acc12356","url":"post-form"}': '' },加了Qs.stringfy处理{ token: 'acc12356', url: 'post-form' };使用我们在使用的时候,只需要对参数做处理了,就不需要主动去添加content-type头了

      // axios post方法内只对传入的data数据进行了JSON.stringify,所以当我们直接传入data进去的时候,会被转换成json字符串对象,而这个时候content-type如果是application/json则后端能够正常获取到参数,如果为application/x-www-form-urlencoded则后端不能正常获取到参数;这也是为什么如果是表单提交,需要对data处理转换成key value对
      return axios.post('/post-form', Qs.stringfy(data), {
        headers: { 'content-type': 'application/x-www-form-urlencoded' }
      });
    }

2. axios post 'application/json' 

const postJson = function (data) {
      return axios.post('/post-json', data);
   }

3. axios post multipart/form-data; boundary=[xxx]
这里需要注意的是axios内当检测到传入的参数是form-data时会删除默认的content-type: application/json,这也我们在浏览器内会看到content-type: multipart/form-data; boundary=[xxx];而jquery是没有做这一步处理的

const postFormData = function (data) {
      return axios.post('/post-form-data', data);
    }

4. axios get query内取参数

const getList = function (params) {
      return axios.get('/get-list', {params});
    }

5. axios get params内取参数

    const getListParams = function (params) {
      return axios.get('/get-list-params/1234563/akcnal',);
    }

app.js

const fs = require('fs');
const express = require('express');
const path = require('path');
const bodyParser = require('body-parser');
const multer = require('multer'); 

const upload = multer(); // for parsing multipart/form-data
const app = express();

app.use(bodyParser.json()); // for parsing application/json
app.use(bodyParser.urlencoded({ extended: true })); // for parsing application/x-www-form-urlencoded

app.use('/static', express.static('../front/static'));

app.get('/', (req, res) => {
  res.sendFile(path.resolve('../front/index.html'), {
    header: 'text/html'
  })
});

app.get('/get-list', (req, res) => {
  console.log(req.query);
  res.json({ url: 'post-form', code: 200, ...req.query })
})

app.get('/get-list-params/:id/:token', (req, res) => {
  console.log(req.params);
  res.json({ url: 'post-form', code: 200, ...req.params })
})

app.post('/post-form',upload.array(), (req, res) => {
  console.log(req.body);
  res.json({ url: 'post-form', code: 200 })
})

app.post('/post-form-data', upload.array(), (req, res) => {
  console.log(req, req.body);
  fs.rename(req.file.path, "upload/" + req.file.originalname, function(err) {
      if (err) {
          throw err;
      }
      console.log('上传成功!');
  })

  res.json({ url: 'post-form-data', code: 200 })
})

app.post('/post-json', (req, res) => {
  console.log(req.body);
  res.json({ url: 'post-json', code: 200 })
})

app.listen(8088, () => {
  console.log('启动成功');
});
<div>
    <button id="btn1">get请求</button>
    <button id="btn2">post请求application/x-www-form-urlencoded</button>
    <button id="btn3">post请求multipart/form-data</button>
    <button id="btn4">post请求application/application/json</button>
    <div>
      <input type="file" name="file" id="file">
    </div>
    <div>
      <button class="btn">get请求</button>
      <button class="btn">post请求application/x-www-form-urlencoded</button>
      <button class="btn">post请求multipart/form-data</button>
      <button class="btn">post请求application/application/json</button>  
      <button class="btn">post请求xhr</button> 
    </div>
  </div>
  <script src="/static/js/axios.js"></script>
  <script src="/static/js/qs.js"></script>
  <script src="/static/js/jquery.js"></script>
  <script>

    const buttons = document.getElementsByTagName('button');

    buttons[0].onclick = function () {
      getList({token: 'acc12356', url: 'get-list'}).then((res) => {
        console.log('buttons[0]', res);
      })
      getListParams({token: 'acc12356', url: 'get-list-params'}).then((res) => {
        console.log('buttons[0]', res);
      })
    }

    buttons[1].onclick = function () {
      postForm({token: 'acc12356', url: 'post-form'}).then((res) => {
        console.log('buttons[1]', res);
      })
    }

    buttons[2].onclick = function () {
      const fileEle = document.getElementById('file');
      console.log(fileEle, fileEle.files);
      const form = new FormData();
      form.append('file', 'file');
      form.append('file', fileEle.files[0]);
      form.append('token', 'acc12356');
      form.append('url', 'post-form-data');
      postFormData(form).then((res) => {
        console.log('buttons[2]', res);
      })
    }

    buttons[3].onclick = function () {
      postJson({token: 'acc12356', url: 'post-json'}).then((res) => {
        console.log('buttons[3]', res);
      })
    }

    const btns = document.getElementsByClassName('btn');

    btns[0].onclick = function () {
      getListJq({token: 'acc12356', url: 'get-list'}).then((res) => {
        console.log('btns[0]', res);
      })
      getListParamsJq({token: 'acc12356', url: 'get-list-params'}).then((res) => {
        console.log('btns[0]', res);
      })
    }

    btns[1].onclick = function () {
      postFormJq({token: 'acc12356', url: 'post-form'}).then((res) => {
        console.log('btns[1]', res);
      })
    }

    btns[2].onclick = function () {
      const fileEle = document.getElementById('file');
      console.log(fileEle, fileEle.files);
      const form = new FormData();
      form.append('file', 'file');
      form.append('file', fileEle.files[0]);
      form.append('token', 'acc12356');
      form.append('url', 'post-form-data');
      postFormDataJq(form).then((res) => {
        console.log('btns[2]', res);
      })
    }

    btns[3].onclick = function () {
      postJsonJq({token: 'acc12356', url: 'post-json'}).then((res) => {
        console.log('btns[3]', res);
      })
    }

    btns[4].onclick = function () {
      request().then((res) => {
        console.log('btns[4]', res);
      })
    }
  </script>

get请求request参考图

axios-get

post请求content-type: application/json request参考图

axios-post-applicationjson

post请求content-type: application/x-www-form-urlencoded request参考图

axios-post-application-x-www-form-urlencoded

post请求content-type: application/form-data request参考图

post-form-data

参考链接: https://segmentfault.com/a/1190000004322487 https://github.com/axios/axios/blob/master/lib/adapters/xhr.js