4

前提

在前后端联调的时候总会牵扯到一个问题,就是参数的传递方式,GET请求就不说了,参数往url后面一拼,万事大吉。

然而一到POST请求的时候,花样就来了,后端童鞋跟你说,我这个接口在postman试过是没问题的,你content-type设置成我要的就行了,要是真这么简单今天咱也就啥也不写了。

今天就来聊聊设置不同content-type时,前端童鞋的参数设置方式,以及在http中是什么样的,以及后端是怎么接受的。

content-type的值非常非常多,有兴趣的去查一下 HTTP Content-type 对照表,我们今天只谈POST请求 request headers中的content-type

准备工作

前端: fetch; 字符集设置: charset=utf-8
抓包工具: charles; 浏览器: chrome
node服务: Koa; 后端: SpringBoot

简单封装post

export const post = (url: string, params: any, options: any = {}) => {
    return fetch(url, {
        method: 'post',
        headers: {
            ...options.headers,
        },
        body: params
    });
}

node服务

import router from './router/index'; // 路由配置,无关紧要
const Koa = require('koa');
const koaBody = require('koa-body');
const app = new Koa();
app.use(koaBody({
    multipart: true
}));
app.use(async (ctx, next) => {
    console.log(ctx.request.headers);
    console.log('body', { ...ctx.request.body }); // 主要看一下打印出的body
    ctx.params = {
        ...ctx.request.body,
        ...ctx.query
    };
    await next();
});
app.use(router.routes()).use(router.allowedMethods());
app.listen(3000)

text/plain

首先就是content-type什么都不设置时,大多数情况下(下面马上就有例外)都会被浏览器默认为 text/plain 将参数设置为纯文本的形式,浏览器也不会对其进行处理。实际中使用时基本也不会这样来用,可跳过

application/json

代码

设置一下Content-type: application/json

post('/api/post-application-json', JSON.stringify({
    name: 'application',
    content: {
        value: 20,
        names: ['zhang', 'li']
    }
}), {
    headers: {
        'Content-type': 'application/json; charset=utf-8'
    }
});

Chrome

image.png

charles

image.png

node后端

image.png

按照json格式就没啥问题

Jave后端

@Data
private class FormVO {
    private Integer id;
    private String name;
}

@RequestMapping(value = "/post-application-json", method = RequestMethod.POST)
public int createForm(@RequestBody FormVO form) {
    Integer id = form.getId();
    String name = form.getName();
    return 1;
}

需要为form写定制的class 即FormVO

application/x-www-form-urlencoded

表单上传:数据被编码为名称/值对
传统使用form表单上传时,都会创建一个form

表单构建

<form
    action="/api/post-form-urlencoded"
    method="post"
    encType="application/x-www-form-urlencoded"
>
    <input name="id" />
    <input name="name" />
    <button type="submit">提交</button>
</form>

chrome

image.png

charles

image.png

可以看出原始文本还是以键值对的形式

问题来了,当你不想写form时,但是后端又要求你用form表单上传时,该怎么构建参数呢
哈哈哈,就手动自己写键值对拼在一起!

手动构建form参数

post('/api/post-form-urlencoded', `id=2&name=wee`, {
    headers: {
        'Content-type': 'application/x-www-form-urlencoded; charset=utf-8'
    }
});

因为键值对的value要求为string,如果有复杂结构,那就需要对复杂value先JSON.stringify一下

post('/api/post-form-urlencoded', `id=2&content=${JSON.stringify({ name: 'aaa' })}`, {
    headers: {
        'Content-type': 'application/x-www-form-urlencoded; charset=utf-8'
    }
});

node后端

image.png

JSON.stringify过的字段都得再JSON.parse回来

Java后端

@RequestMapping(value = "/post-form-data", method = RequestMethod.POST)
public Object createForm(@RequestParam Map<String, Object> params) {
    Object id = params.get("id");
    Object name = params.get("name");
    return name;
}

简单愉快,扩展字段也不用手动去写class

multipart/form-data

然后就到了这货了
multipart/form-data: 数据被编码为一条消息,页上的每个控件对应消息中的一个部分

这个也是表单上传,跟application/x-www-form-urlencoded有啥不一样呢,主要是这个更厉害一丢丢,他可以用于上传文件

表单构建

<form
    action="/api/post-form-data"
    method="post"
    encType="multipart/form-data"
>
    <input name="id" />
    <input name="name" />
    <button type="submit">提交</button>
</form>

看起来和application/x-www-form-urlencoded 没啥不一样的

chrome

再看下chrome下,好像也没啥不一样的,除了Content-type
image.png

charles

然而再来看看charles下的原始信息
image.png

duang! duang! 差别在这呢,被编码成一个消息体了

手动构建form参数采坑

然后就到了踩坑的地方了,不写form表单怎么按这种方式上传呢
首先是参数设置,必须使用FormData来构建参数,别的都白扯,
然后设置Content-type吗,该踩的坑还是踩一下试试

const formData = new FormData();
formData.set('id', '2');
formData.set('name', 'zhang');
post('/api/post-form-data', formData, {
    headers: {
        'Content-type': 'multipart/form-data; charset=utf-8'
    }
});

结果呢,Chrome下
image.png
image.png

发现跟上面chrome下不太一样, chrome也不给你处理了,服务器也不干了
Error: bad content-type header, no multipart boundary

仔细看看,才会发现原生form, content-type后面还有个boundary,
这玩意干啥的呢

分隔符 上面说过了,form-data会被编码为一条消息,那我怎么分隔开这个消息体,解析得到我想要的键值对呢,就是通过boundary,就是那个特别长的------WebKitFormBoundaryK1IM1NwdabR3WdrA

手动构建form参数正解

所以怎么生成boundary呢,很简单,就不设置content-type,浏览器会因为你的参数是FormData类型,自动为你添加

const formData = new FormData();
formData.set('id', '2');
formData.set('name', 'zhang');
post('/api/post-form-data', formData);

不上截图了,跟form表单一样的东西

node后端

image.png

JSON.stringify过的字段都得再JSON.parse回来
form表单的后端处理看起来还是一样的

Java后端

和application/x-www-form-urlencoded 一样

总结

application/json 对于前端来说 真香

form表单对于后端来说,两种无差别,也不需要构造class

以后跟你的后端小伙伴好好商量吧(大哥,用json行不行)


晴天
7 声望3 粉丝