前端同时上传文件和json字符串

Q1:使用了Blob的方式

你提到的问题是正确的。当使用 Blob 包装 JSON 数据并将其添加到 FormData 中时,即使 Blob 的类型被设置为 application/json,前端在开发者工具中仍然会显示为二进制文件,而不是可读的 JSON 字符串。这是因为 FormDataBlob 视为文件处理,因此无法在开发者工具中直接显示其内容。

原因分析

FormData 是为上传文件设计的,它能处理文本字段和文件类型,但对于通过 Blob 包装的 JSON 数据,它会视作二进制数据,因此在开发者工具中显示为“文件”类型。

可能的解决方案与权衡

目前,没有一种能够在浏览器开发者工具中显示 JSON 字符串,同时在后端直接解析为对象的完美解决方案。这是因为 FormData 的设计决定了它如何处理数据。

方案 1: 直接传递 JSON 字符串

  • 优点: 前端开发者工具中能看到 JSON 数据的实际内容。
  • 缺点: 后端需要将接收到的字符串手动转换为对象。
1
formData.append("metadata", JSON.stringify(myObject)); // 字符串形式传递

**方案 2: 使用 Blob 和 **application/json

  • 优点: 后端能够直接将 JSON 数据解析为对象。
  • 缺点: 前端开发者工具中显示为二进制文件,不易查看内容。
1
formData.append("metadata", new Blob([JSON.stringify(myObject)], { type: 'application/json' }));

总结

如果一定要选择一种方式,只能在这两种方案中取舍:

  1. 如果你希望前端能清楚看到 JSON 数据内容,且愿意在后端手动解析 JSON 字符串,可以选择方案 1。
  2. 如果你希望后端自动解析 JSON 数据为对象,且能接受在前端看到的是二进制文件,可以选择方案 2。

这种取舍源于 FormData 的底层设计限制,不能同时满足两个需求。

Q2:具体讲一讲FormData的底层设计

FormData 是一种用于构建包含键值对的数据集的接口,特别适用于 multipart/form-data 格式的数据,这种格式常用于文件上传。FormData 的设计使得它能够灵活地处理各种类型的数据,包括文本、文件、以及二进制数据。

FormData 的底层设计与工作原理

  1. 键值对的结构
    • FormData 的核心是一个键值对的数据结构。每一个键对应一个值,这个值可以是普通的字符串、二进制数据、文件、甚至是 Blob 对象。
    • 每个键可以在 FormData 中对应多个值,例如多个文件上传。
  2. 数据类型的处理
    • 字符串:当你将字符串添加到 FormData 时,它会作为纯文本处理,并且在请求中以 Content-Disposition: form-data; name="key" 的形式被发送。
    • 文件和 Blob:当你添加 FileBlob 对象时,它们会被视为二进制数据。在 HTTP 请求中,这些数据将被发送为 Content-Disposition: form-data; name="key"; filename="file" 的形式,并且带有相应的 Content-Type(如 application/jsonimage/png 等)。
  3. 多部分数据的构建
    • FormData 通过 multipart/form-data 格式来发送数据。这种格式将每个字段的内容封装在一个“部分”(part)中,每个部分由唯一的边界(boundary)分隔。
    • 每个部分包含字段的元数据(如 Content-DispositionContent-Type)以及实际的数据内容。
  4. Blob** 对象的处理**
    • 当你使用 Blob 对象并指定 Content-Type(如 application/json),FormData 会将其视为一个二进制数据块。虽然它内部可能是可读的 JSON 字符串,但浏览器开发者工具在显示时只会将其当作文件处理,显示为二进制内容。
  5. 发送数据
    • 在将 FormData 对象与 XMLHttpRequestfetch API 一起使用时,数据会被序列化为 multipart/form-data 格式,并附加到 HTTP 请求的主体中发送。服务器端将根据 Content-Type 头部的 multipart/form-data 来解析这些部分。

底层限制的原因

  • FormData 的设计主要是为了支持文件上传和多部分表单提交,而不是为了在前端方便查看或调试数据。因此,对于二进制数据(如 Blob),它不会显示内容的实际值,而是将其处理为文件数据。
  • 浏览器开发者工具只显示 FormData 内部数据的简化视图,对于 Blob 数据,它不会解码或显示其中的内容,这就是为什么 JSON 数据被 Blob 包装后在开发者工具中显示为二进制文件。

设计的初衷

FormData 的设计初衷是为了处理各种形式的数据(尤其是文件)上传,保证在数据传输过程中数据的完整性和类型的准确性。因此,它对数据的处理方式偏向于灵活与通用,而不是在调试时的可读性。

Q3:对于FormData数据,后端除了使用RequestPart注解接收,还有其他接收和解析方式吗

除了使用 @RequestPart 注解接收和解析 FormData 数据外,Spring 后端还可以通过以下几种方式来处理 FormData 数据。这些方法在特定情况下可能会更合适,具体取决于你传递的数据类型和需求:

1. 使用 HttpServletRequest 直接解析

  • 直接访问请求体:你可以使用 HttpServletRequest 来手动解析 FormData 数据。通过 request.getParts() 可以获取表单中的所有部分,并手动处理每个部分的数据。
  • 适用场景:当你需要更低层次的控制或需要处理非常复杂的 multipart 请求时,可以使用这种方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
@PostMapping("/upload")
public ResponseEntity<String> handleFileUpload(HttpServletRequest request) throws IOException, ServletException {
Part filePart = request.getPart("file");
Part metadataPart = request.getPart("metadata");

InputStream fileInputStream = filePart.getInputStream();
InputStream metadataInputStream = metadataPart.getInputStream();

// 你可以手动读取和处理这些输入流的数据
// 例如,将metadataInputStream解析为JSON对象

return ResponseEntity.ok("File and metadata processed successfully");
}

2. **使用 **MultipartHttpServletRequest

  • **专门处理 **multipart/form-data:Spring 提供了 MultipartHttpServletRequest,它继承自 HttpServletRequest,并提供了更方便的方法来处理 multipart/form-data 请求。
  • 适用场景:当你想要简化 multipart 数据处理,同时保留对每个部分的细粒度控制时,这种方法非常有用。
1
2
3
4
5
6
7
8
9
10
@PostMapping("/upload")
public ResponseEntity<String> handleFileUpload(MultipartHttpServletRequest request) {
MultipartFile file = request.getFile("file");
String metadata = request.getParameter("metadata"); // 可以直接获得字符串形式的metadata

// 你可以手动解析metadata为Java对象
// 或处理文件内容

return ResponseEntity.ok("File and metadata processed successfully");
}

3. **使用 @ModelAttribute 与 **@RequestParam

  • 结合使用:你可以使用 @ModelAttribute 来绑定简单表单数据,同时使用 @RequestParam 来处理文件上传。这种方式更适合没有复杂 JSON 数据结构的情况。
  • 适用场景:当你处理的 FormData 主要是简单的键值对数据和文件时,这种方式非常适合。
1
2
3
4
5
6
7
8
9
@PostMapping("/upload")
public ResponseEntity<String> handleFileUpload(
@RequestParam("file") MultipartFile file,
@ModelAttribute MyFormData myFormData) {
// 这里的 MyFormData 可以包含简单的表单字段(例如字符串、数字)
// 文件通过 @RequestParam 处理

return ResponseEntity.ok("File and form data processed successfully");
}

4. 使用 @RequestBody 配合自定义解析器

  • 处理 JSON 数据:虽然 @RequestBody 主要用于处理整个请求体作为 JSON 格式的情况,但你可以通过自定义消息转换器(MessageConverter)来处理 multipart 请求,并将部分内容作为 JSON 对象解析。
  • 适用场景:当你需要高度自定义的 JSON 解析时,这种方法可能是有效的,但它比 @RequestPart 更复杂且不常用。
1
2
3
4
5
6
7
8
9
@PostMapping("/upload")
public ResponseEntity<String> handleFileUpload(
@RequestBody MultipartFile file,
@RequestBody MyObject myObject) {
// 注意:通常情况下,@RequestBody 不直接处理 Multipart 数据,
// 这种方式需要自定义配置

return ResponseEntity.ok("File and JSON object processed successfully");
}

总结

  • @RequestPart 是最常见和简便的处理 FormDatamultipart 数据的方法,尤其是在同时处理文件和 JSON 数据时。
  • HttpServletRequest** 与 **MultipartHttpServletRequest 提供了更低层次的控制,适用于需要直接操作请求部分的情况。
  • @ModelAttribute** 和 **@RequestParam 适用于表单字段简单、文件上传场景,而不包含复杂的 JSON 结构。

根据你项目的需求,可以选择最适合的方式来处理 FormData 数据。