使用AJAX的方式发送FORM表单数据

详细介绍前端使用 AJAX 发送 FORM 表单数据的问题与解决方案,核心围绕中文乱码及 axios 设置请求头失效展开。

介绍 form 标签enctype属性及multipart/form-data的 MIME 规范,给出自定义 AJAX 方法:手动组织 FormData、设置带编码和分隔符的请求头、拼接请求体并以二进制发送。

问题缘由

在前端(vue,axios)提交表单数据到后端(springboot)时,表单字段如果存在中文,后端接收到数据会解析为乱码。这篇文章https://blog.csdn.net/weixin_42161320/article/details/129283121给出的原因是,后端接收到数据时默认的编码格式为iso_8859_1。

问题的缘由为前后端编码的问题所致,后端设置编码可以修正该问题。通过postman调用接口测试,前端指定参数编码也可以解决该问题。解决方式为在表单提交过程中,手动指定请求头Content-Type值为multipart/form-data; charset=UTF-8即可。

axios无法设置表单请求头

尝试修改axois请求,在发送请求前,手动设置请求头。代码如下,实际测试时发现,该方式并不会生效。

1
2
3
4
5
6
7
8
let formData = new FormData();
formData.append('id', '123') // id
formData.append('file', file, 'test.jpg') // 文件
axios.post(url, formData, {
headers: {
"Content-Type": "multipart/form-data; charset=UTF-8"
}
})

在axios0.xx.xx版本,0.27.20.26.0,设置的请求头会被忽略。在1.xx.xx版本,Content-Type会被修改为false。经过查询资料,在原生浏览器实现中,form表单提交时,由于form-data存在特殊的格式,当不设置Content-Type或者设置Content-Typefalse时,浏览器默认会设置这个请求头。在浏览器默认设置的请求头中,并不会设置chartset编码。

form标签规范

HTML的form标签用来提交表单数据,文件上传通常以表单的形式提交。

form属性中,enctype 属性用来设置表单的数据类型,当 method 属性值为 post 时,将表单的内容提交给服务器的 。可能的取值有:

当请求方式不为post时,该属性enctype无效。

MIME规范:multipart/form-data

作为重要的MIME类型,MDN对multipart/form-data编码规范进行了说明, 作为多部分文档格式,它由边界线(一个由'--'开始的字符串)划分出的不同部分组成。每一部分有自己的实体,以及自己的 HTTP 请求头。 Content-DispositionContent-Type 用于文件上传领域。

请求头以及提交的请求头格式,形如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 请求头
Content-Type: multipart/form-data; boundary=aBoundaryString
(该请求头和其他表单提交请求头,如Content-Length,组成一个整体)

# 请求体
--aBoundaryString
Content-Disposition: form-data; name="myFile"; filename="img.jpg"
Content-Type: image/jpeg

(data)
--aBoundaryString
Content-Disposition: form-data; name="myField"

(data)
--aBoundaryString
(其余更多的表单字段)
--aBoundaryString--

上述格式中给出了一个表单中,包含一个文件和一个其他字段的格式,每个字段之间通过--aBoundaryString进行分割,字段说明信息和字段值之间存在一个空行,文件字段比普通字段,多出filenameContent-Type信息。

如果有更多的字段,追加在下方即可。

所以,在上例ajax给出的表单请求,发送请求长这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 请求头
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7SWpm1kGf6oJFKnK

# 请求体
----WebKitFormBoundary7SWpm1kGf6oJFKnK
Content-Disposition: form-data; name="id"

123
----WebKitFormBoundary7SWpm1kGf6oJFKnK
Content-Disposition: form-data; name="file"; filename="test.jpg"
Content-Type: image/jpeg


----WebKitFormBoundary7SWpm1kGf6oJFKnK--

通过ajax的方式发送form-data

既然浏览器默认的方式无法指定请求头Content-Type值为multipart/form-data; charset=UTF-8,在了解浏览器表单提交数据的格式后,可以通过ajax的方式进行模拟。

在MDN文档使用 XMLHttpRequest中,给出了一个小框架,用于模拟表单提交的示例, 在使用 JavaScript 做表单提交的工作时,你需要完全模拟浏览器表单提交的行为。

在简化这个示例过程中,将表单提交的过程简化为一下几个步骤:

  1. 组织formData对象
  2. 手动设置请求头Content-Type值为multipart/form-data; charset=UTF-8; boundary=分隔符
  3. 手动拼接请求体格式
  4. 将请求体作为二级制数据,使用xhr发送请求
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
/**
* ajax形式模拟表单数据提交
* @param {String} url 请求URL
* @param {FormData} formData 表单数据
*/
let httpXhr = async function (url, formData) {
let xhr = new XMLHttpRequest();
xhr.open("POST", url, true);
// 固定分隔符
var sBoundary = "--WebKitFormBoundary7SWpm1kGf6oJFKnK";
// 设置请求头
xhr.setRequestHeader(
"Content-Type",
`multipart/form-data; charset=utf-8; boundary=${sBoundary}`
);
// 表单数据的数组形式
var reqData = [];

// 提交form-data格式
// https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Basics_of_HTTP/MIME_types#multipartform-data
// Content-Type: multipart/form-data; boundary=aBoundaryString
// (other headers associated with the multipart document as a whole)

// --aBoundaryString
// Content-Disposition: form-data; name="myFile"; filename="img.jpg"
// Content-Type: image/jpeg

// (data)
// --aBoundaryString
// Content-Disposition: form-data; name="myField"

// (data)
// --aBoundaryString
// (more subparts)
// --aBoundaryString--

for (var i of formData) {
// 如果是文件
if (i[1] instanceof File) {
// 异步转同步
await new Promise((resolve) => {
var reader = new FileReader();
// reader.readAsDataURL(i[1].slice());
reader.readAsBinaryString(i[1]);
reader.addEventListener("loadend", function (e) {
reqData.push(
`--${sBoundary}\r\nContent-Disposition: form-data; name="${i[0]}"; filename="${i[1].name}"\r\nContent-Type: ${i[1].type}\r\n\r\n${reader.result}\r\n`
);
resolve();
});
});
// 如果是普通字段
} else {
reqData.push(
`--${sBoundary}\r\nContent-Disposition: form-data; name="${i[0]}"\r\n\r\n${i[1]}\r\n`
);
}
}
// 增加一个结尾
reqData.push(`--${sBoundary}--`);

// 处理接口响应相关信息
xhr.onload = () => {
console.log("接口响应", xhr.response);
};

xhr.send(new Blob(reqData));
return xhr
};

使用该示例也较为简单

1
2
3
4
let formData = new FormData();
formData.append('id', '123') // id
formData.append('file', file, 'test.jpg') // 文件
httpXhr(url, formData)

限制

  • 该框架使用 FileReader API 进行文件的上传。这是一个较新的 API 并且还未在 IE9 及以下版本的浏览器中实现。因此,使用 AJAX 上传仍是一项实验性的技术。如果你不需要上传 二进制文件,该框架在大多数浏览器中运行良好。
  • 发送二进制内容的最佳途径是通过 ArrayBuffersBlobs 结合 send() 方法甚至 FileReader API 的 readAsArrayBuffer() 方法。但是,自从该脚本的目的变成处理 可字符串化 的原始数据以来,我们使用 sendAsBinary() 方法结合 FileReader API 的 readAsBinaryString() 方法。同样地,上述脚本仅当你处理小文件时行之有效。如果不打算上传二进制内容,就考虑使用 FormData API 来替代。
  • 非标准的 sendAsBinary 方法从 Gecko 31 开始将会废弃并且会很快被移除。标准方法 send(Blob data) 将会取而代之。

FormData 对象的使用

FormData 对象用以将数据编译成键值对,以便用XMLHttpRequest来发送数据。其主要用于发送表单数据,但亦可用于发送带键数据 (keyed data),而独立于表单使用。如果表单enctype属性设为 multipart/form-data,则会使用表单的submit()方法来发送数据,从而,发送数据具有同样形式。

你可以自己创建一个FormData对象,然后调用它的append()方法来添加字段,像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var formData = new FormData();

formData.append("username", "Groucho");
formData.append("accountnum", 123456); //数字 123456 会被立即转换成字符串 "123456"

// HTML 文件类型 input,由用户选择
formData.append("userfile", fileInputElement.files[0]);

// JavaScript file-like 对象
var content = '<a id="a"><b id="b">hey!</b></a>'; // 新文件的正文
var blob = new Blob([content], { type: "text/xml"});

formData.append("webmasterfile", blob);

var request = new XMLHttpRequest();
request.open("POST", "http://foo.com/submitform.php");
request.send(formData);

参考

使用AJAX的方式发送FORM表单数据

https://blog.plcent.com/use-ajax-send-form-data/

作者

Nolly

发布于

2023-05-11

更新于

2025-10-22

许可协议

评论