在工作中,熟悉常见的 Content-Type 类型对于处理 POST 请求至关重要,它可以帮助我们更好地使用和解决问题。接下来我们一起学习 POST 请求相关的知识要点。
介绍
POST 请求是一种常见的数据请求方式,相对于 GET 请求更安全、更灵活。一个标准的 POST 请求由以下三个部分组成:
- 请求行:包含了请求方法、URL 和 HTTP 协议版本。
- 请求头:包含了关于请求的附加信息,常见的请求头字段有
Content-Type
、Authorization
等。 - 请求主体:请求的数据存储在请求主体中,具体的数据格式和编码方式由
Content-Type
字段确定。服务端会根据 Content-Type 字段使用不同的方式对请求体进行解析。
常见的 Content-Type
application/x-www-form-urlencoded
这是 POST 请求中最常见的一种编码方式,适用于表单数据提交。它是原生表单 POST 提交(enctype)的默认值,大部分服务端语言都对这种方式有很好的支持。
当使用 application/x-www-form-urlencoded
提交数据时,需要对参数进行 urlencoded
编码和序列化。数据被编码成以 &
分隔的键值对,同时以 =
分隔键和值,非字母或数字的字符会被 percent-encoding(百分比编码)。
例如,表单提交参数为:
param1:website,
param2:https://www.google.com
经过 urlencoded
编码后:
param1:website
param2:https%3A%2F%2Fwww.google.com
再经过序列化,得到结果:
param1=website¶m2=https%3A%2F%2Fwww.google.com
这种格式发送到服务器的 HTTP 消息主体本质上是一个巨大的查询字符串,结构简单。然而,由于需要对数据进行编码,每个非字母数字字符都需要三个字节来表示。因此,对于大型二进制文件,请求体的大小会增加三倍,造成较低的效率。
此外,由于编码方式的限制,非 ASCII 字符可能会丢失,因此在传输文件等场景下,这种方式并不适用。
适用场景:数据量小的简单数据
multipart/form-data
这种编码方式主要用于文件上传,能够有效传输二进制数据(非字母数字字符),并支持携带其他信息。
在 multipart/form-data
格式中,数据被拆分为多个部分(part)。每个部分以一个标头(headers)开头,通过一组空行进行分隔,并且每个部分都包含一个唯一的 boundary
字符串,用于标识不同部分之间的边界。
- 边界:
--boundary
,用于分隔各个部分。 - 标头:包括元数据,如
Content-Type
、Content-Disposition
等。 - 空行:用于分隔标头和正文数据。
- 正文数据:包含实际的文件数据或表单字段的值。
最终,以 --boundary--
表示请求体的结束。
一个简单的例子如下:
POST http://www.example.com/upload HTTP/1.1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryABC123
------WebKitFormBoundaryABC123
Content-Disposition: form-data; name="username"
zhang san
------WebKitFormBoundaryABC123
Content-Disposition: form-data; name="avatar"; filename="avatar.png"
Content-Type: image/png
...二进制数据...
------WebKitFormBoundaryABC123--
在这个例子中,请求头中指定了 Content-Type: multipart/form-data
,并设置了一个唯一的 boundary
字符串作为分隔符(在此例中为 ----WebKitFormBoundaryABC123
)。
在请求体中,我们可以看到两个部分。
-
第一部分:以
--boundary
开始,并包含了一个标头Content-Disposition: form-data; name="username"
。这表示这个部分是一个表单字段,字段名为username
,字段值为zhang san
。 -
第二部分:以
--boundary
开始,并包含了两个标头。首先是Content-Disposition: form-data; name="avatar"; filename="avatar.png"
。然后是Content-Type: image/png
,指定了这个部分的数据类型为image/png
。接着是二进制文件数据主体。
最后以 --boundary--
表示结束。
multipart/form-data
也可以用于传递普通数据,但是由于每个字段都有自己的一组 MIME 标头和边界字符串,这会增加数据包的大小。相对于其他简单的编码方式(如 application/x-www-form-urlencoded
),它在处理普通数据时效率低,开销更大。
适用场景:文件上传、带有二进制数据的请求
application/json
使用 application/json
编码方式,可以将数据序列化成一个 JSON 字符串作为请求主体。相比其他只能传输键值对的方式,这种编码方式更加灵活和简单高效。它支持复杂的嵌套结构、数组、对象等,非常适合传输包含多层级数据关系的数据。
POST http://www.example.com HTTP/1.1
Content-Type: application/json;charset=utf-8
{"name":"test", "age": 24, "hobby":["a","b","c"]}
适用场景:复杂的结构化的数据
Axios 中 Content-Type 的处理
当使用 Axios 库发送 POST 请求时,它会自动处理请求头和请求参数,确保请求符合标准并且能够正确传输。
下面是Axios源码中在处理请求头中 Content-Type 时的代码逻辑(不同版本代码略有不同):
// https://github.com/axios/axios/blob/v2.x/lib/defaults/index.js
var DEFAULT_CONTENT_TYPE = {
'Content-Type': 'application/x-www-form-urlencoded'
};
// 使用该方法会判定果用户有没有设置 Content-Type
// 没有则会设置相应的类型值
function setContentTypeIfUnset(headers, value) {
if (!utils.isUndefined(headers) && utils.isUndefined(headers['Content-Type'])) {
headers['Content-Type'] = value;
}
}
function transformRequest(data, headers) {
var contentType = headers && headers['Content-Type'] || '';
var hasJSONContentType = contentType.indexOf('application/json') > -1;
var isObjectPayload = utils.isObject(data);
// 如果参数是个对象并且是 HTML Form 标签,会将参数转化为 FormData 对象
// 补充:如果 FormData 构造函数的参数是一个 DOM 的表单元素,构造函数会自动处理表单的键值对,
// 将 HTML 表单直接读取为 FormData
if (isObjectPayload && utils.isHTMLForm(data)) {
data = new FormData(data);
}
var isFormData = utils.isFormData(data);
if (isFormData) {
if (!hasJSONContentType) {
return data;
}
// 如果参数是 FormData 格式
// 并且用户设置了 Content-Type 是 application/json 格式,会将参数转化为 JSON 字符串
// 否则不处理
return hasJSONContentType ? JSON.stringify(formDataToJSON(data)) : data;
}
// 如果请求参数是 ArrayBuffer、Buffer、Stream、File、Blob 这几种格式的不做任何处理
if (
utils.isArrayBuffer(data) ||
utils.isBuffer(data) ||
utils.isStream(data) ||
utils.isFile(data) ||
utils.isBlob(data)
) {
return data;
}
// 如果请求参数是 ArrayBuffer 格式的则返回 data.buffer
if (utils.isArrayBufferView(data)) {
return data.buffer;
}
// 如果请求参数是一个 URLSearchParams 对象
// 会设置 Content-Type 为 application/x-www-form-urlencoded
// 并调用 URLSearchParams.toString() 方法
// 将参数转为序列化字符串 'foo=bar&hello=world'
if (utils.isURLSearchParams(data)) {
setContentTypeIfUnset(headers, 'application/x-www-form-urlencoded;charset=utf-8');
return data.toString();
}
var isFileList;
// 如果参数是对象类型
if (isObjectPayload) {
// 如果用户设置的 Content-Type 是 application/x-www-form-urlencoded 格式
// 则对参数进行URL编码并序列化 'foo=bar&hello=world'
if (contentType.indexOf('application/x-www-form-urlencoded') !== -1) {
return toURLEncodedForm(data, this.formSerializer).toString();
}
// 如果参数是 一个 FileList 对象
// 或者用户设置了 Content-Type 是 multipart/form-data
// 那么会将参数对象转为 FormData 对象
if ((isFileList = utils.isFileList(data)) || contentType.indexOf('multipart/form-data') > -1) {
var _FormData = this.env && this.env.FormData;
return toFormData(
isFileList ? {'files[]': data} : data,
_FormData && new _FormData(),
this.formSerializer
);
}
}
// 如果参数是对象类型
// 或者设置的 Content-Type 是 application/json
// 会设置 Content-Type 为 application/json
// 并将参数转化为 一个 JSON 字符串
if (isObjectPayload || hasJSONContentType ) {
setContentTypeIfUnset(headers, 'application/json');
return stringifySafely(data);
}
return data;
}
// 如果用户没有设置请求头,同时也没有命中 transformRequest 中对 Content-Type 的处理
// 则会使用默认值 DEFAULT_CONTENT_TYPE
utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
defaults.headers[method] = utils.merge(DEFAULT_CONTENT_TYPE);
});
如果请求参数是 FormData 类型,在最终发送请求前还会进行一次判断处理(在 dispatchRequest 里,在拦截器 request 之后)。
// axios/lib/adapters/xhr.js
module.exports = function xhrAdapter(config) {
// ...... 已省略部分代码 ......
return new Promise(function dispatchXhrRequest(resolve, reject) {
// 发送ajax之前,如果发现请求参数是 FormData 类型的
// 会删除请求头中 'Content-Type' 的设置,让浏览器自己选择
// multipart/form-data
if (utils.isFormData(requestData) && utils.isStandardBrowserEnv()) {
delete requestHeaders['Content-Type']; // Let the browser set it
}
})
}
当使用 Axios 发送 application/x-www-form-urlencoded
类型的请求时,如果请求参数不是 URLSearchParams
类型,Axios 并不会自动序列化对象或数组为查询字符串格式。这时,我们通常需要借助 qs.stringify
方法手动将参数序列化成适合的格式。这样,服务器才能正确地解析请求数据。
希望本文对你有帮助!
往期文章推荐: