AngularJS FormData 多部分文件上传
又一篇关于使用 AngularJS 进行文件上传的教程,使用 FormData 进行多部分文件上传
引言
前段时间,我写了一篇关于如何使用 AngularJS 的 ngResource
上传文件的教程。 在这里。 但最近我发现这是一个很棒的想法,但不是一个好主意。问题是,使用 Microsoft IIS,请求 JSON 的大小是有限制的。如果你使用 ASP.NET MVC(不是 .NET Core 的那个),要增加 JSON 请求的大小非常困难。因此,我的项目不得不使用 HTML 表单来上传文件。正如我所发现的,使用 HTML 表单上传文件并不难。本教程将向您展示如何做到。示例应用程序将上传文件,而基于 Spring REST 的后端 API 控制器将处理接收上传的文件。
除了文件上传,我还将讨论如何将 JSON 请求添加到同一个表单请求中,以便与上传的文件一起处理。这是可行的,因为使用 HTML 表单,文件是多部分表单请求的一部分。任何你可以添加的东西,都可以成为请求的一部分。这种多部分表单数据就像一个大的键值集合。所以一个人可以把文件放进去,任何基于字符串的值也可以放进去。总之,继续阅读。
示例应用程序架构
示例应用程序有两层,前端和后端。前端是一个简单的 AngularJS 应用程序,它演示了如何上传文件,以及如何向上传请求添加一些额外数据。后端将演示如何处理保存文件以及如何处理上传文件的额外数据。
前端分为两个 JavaScript 文件。第一个(app.js)是前端的应用程序。另一个(uploadService.js)将处理上传的服务。这很简单。后端也一样,只有一个 Rest 控制器(SampleFileUploadController.java)带有一个方法,该方法将上传的文件保存到磁盘。它还将请求中的 string
反序列化为一个对象,然后处理该对象。在这种情况下,我只是将对象内容写入控制台。
让我们深入代码。
使用 AngularJS 进行多部分文件上传
为了使用 FormData
和 AngularJS 上传文件,我必须使用 $http
,而不是 ngResource
。并且我必须使用 HTTP 方法 POST
。以下是完成此操作的步骤
- 声明一个
FormData
对象,并将一个文件对象附加到它。 - 如果需要,向此
FormData
对象添加另一个基于string
的对象。 - 使用
$http
将FormData
发送到后端 Rest 服务。
声明一个 FormData
对象,并将一个文件对象附加到它。如果需要,我会向 FormData
对象添加更多数据
var fd = new FormData();
fd.append('fileMetadata', JSON.stringify(imgMetadata));
fd.append('file', fileToUpload);
上面代码示例的第二行是在 FormData
对象中添加一个额外的对象。第三行是添加要上传的文件。正如我之前提到的,FormData
是一个键值集合。这两行是
- 添加一个代表 JSON 格式对象的
string
,标识此对象的键称为“fileMetadata
”。 - 附加一个文件(待上传)并且代表此文件的键称为“
file
”。
请注意,这些键非常重要。它们将在 Java 端使用,以便可以从 FormData
集合中检索对象。在 Java 端,FormData
集合由 org.springframework.web.multipart.MultipartFile
表示。稍后我们将讨论这一点。
接下来,我将使用 $http
进行上传。或将请求数据发送到服务器端。方法如下
return $http.post(uploadUrl, fd, {
transformRequest: angular.identity,
headers: {
"Content-Type": undefined
}
});
这里有几个想法需要解释
- 我调用
$http.post()
。此方法需要三个参数。第一个是我发送请求的 URL,第二个是FormData
对象。第三个是一个会传递额外请求数据的对象,例如头信息和 cookie 信息。 - 代码
transformRequest: angular.identity
用于转换用户身份数据并添加到请求中。这会将用户身份验证数据复制到请求中,以便安全地传递到后端服务器。 - 最后一部分,
headers
对象只有一个名为“Content-Type
”的属性,并将其设置为 undefined。$http.post()
将填充正确的头数据。
调用 $http.post()
后,返回的 promise 会发送回调用者,以便调用者可以处理从服务器发回的响应。如您所见,这只是前端 UI 代码的前半部分。
在我继续之前,我将向您展示文件上传服务的完整代码。这是
(function () {
"use strict";
var mod = angular.module("testSampleUploadModule", [ ]);
mod.factory("testSampleUploadService", [ "$http",
function ($http) {
var svc = {
uploadFile: uploadFile
};
function uploadFile(uploadUrl, fileToUpload) {
var imgMetadata = {
author: "Han Solo",
title: "Sample Upload Image",
description: "This is a simple upload with additional metadata attached.",
keywords: "keyword1,keyword2,keyword3,keyword4"
};
var fd = new FormData();
fd.append('fileMetadata', JSON.stringify(imgMetadata));
fd.append('file', fileToUpload);
return $http.post(uploadUrl, fd, {
transformRequest: angular.identity,
headers: {
"Content-Type": undefined
}
});
}
return svc;
}
]);
})();
很简单,不是吗?
调用文件上传服务
在上一节中,我们已经看到了可以使用 FormData
上传文件的 AngularJS 服务,它利用了多部分文件上传功能,可以将额外的元数据与文件上传一起发送。本节将演示如何使用此服务。
让我们回顾一下前端如何进行文件上传。在 HTML 端,我仍然使用了隐藏的文件输入字段以及一个 bootstrap 输入组(一个文本字段和一个按钮拼接在一起的控件)。点击按钮,它会点击文件输入字段,这将弹出“打开文件”对话框,允许用户选择文件。一旦用户选择了文件,我就不必将其数据提取为 BASE64
编码并添加到 JSON 请求中。这是文件输入控件的 HTML 标记
<div class="input-group">
<input type=file id="chosenFile" style="display: none;">
<input type="text" class="form-control"
ng-model="vm.fileName" ng-disabled="vm.uploadingFile">
<span class="input-group-btn">
<button class="btn btn-default" type="button"
ng-click="vm.clickBrowseFile()">Browse</button>
</span>
</div>
在 Angular 端,我必须设置点击按钮来触发文件输入控件以打开 **“打开文件”** 对话框的功能。方法如下
vm.clickBrowseFile = function () {
angular.element("#fileUploadForm #chosenFile").click();
};
点击按钮并选择文件后,文件名将显示在文本字段中。这可以通过以下方式完成
angular.element("#fileUploadForm #chosenFile").bind("change", function(evt) {
if (evt) {
if (evt.target.files && evt.target.files.length > 0) {
vm.fileName = evt.target.files[0].name;
}
$scope.$apply();
}
});
这些都从我 之前的教程 中复制的。除了最后一部分,我获取文件名并将其显示在文本字段中。我重写了这部分,因为新方法将更有效。而新方法让我看起来更专业(开玩笑)。如果您有兴趣回顾这些,可以 查看。下一部分也是上一教程的一部分,我已经对其进行了修改,以使用新的上传服务
vm.clickUpload = function () {
vm.uploadingFile = true;
vm.uploadSuccess = false;
vm.uploadFailed = false;
var upoadFileField = angular.element("#fileUploadForm #chosenFile");
if (upoadFileField != null && upoadFileField.length > 0) {
if (upoadFileField[0].files && upoadFileField[0].files.length > 0) {
testSampleUploadService.uploadFile("./uploadFile", upoadFileField[0].files[0])
.then(function (result) {
if (result && result.status === 200) {
if (result.data && result.data.opSuccess === true) {
vm.uploadSuccess = true;
} else {
vm.uploadFailed = true;
}
} else {
vm.uploadFailed = true;
}
}, function (error) {
if (error) {
console.log(error);
}
vm.uploadFailed = true;
}).finally(function () {
upoadFileField[0].files = null;
vm.fileName = null;
vm.uploadingFile = false;
});
}
}
};
这段代码有两个部分。第一个是获取要上传的选定文件。为此,我查询文件输入元素。然后我检查此输入是否附加了文件。一旦我确定选择了文件,我将调用文件上传服务来上传文件。检查附加文件
...
var upoadFileField = angular.element("#fileUploadForm #chosenFile");
if (upoadFileField != null && upoadFileField.length > 0) {
if (upoadFileField[0].files && upoadFileField[0].files.length > 0) {
...
}
...
}
...
调用文件上传服务
testSampleUploadService.uploadFile("./uploadFile", upoadFileField[0].files[0])
.then(function (result) {
if (result && result.status === 200) {
if (result.data && result.data.opSuccess === true) {
vm.uploadSuccess = true;
} else {
vm.uploadFailed = true;
}
} else {
vm.uploadFailed = true;
}
}, function (error) {
if (error) {
console.log(error);
}
vm.uploadFailed = true;
}).finally(function () {
upoadFileField[0].files = null;
vm.fileName = null;
vm.uploadingFile = false;
});
如所示,这不是一个复杂的设计。从文件输入字段获取文件对象很容易,只需引用 upoadFileField[0].files[0]
。其余的是处理返回的响应。在调用服务之前,输入字段都使用条件变量 vm.uploadingFile
禁用了。当它设置为 true
时,输入字段和按钮将被禁用。当与后端的上传操作完成时,它被设置为 false
,输入字段将被启用。
当后端成功处理上传时,在最顶部将显示一条状态消息。如果失败,示例位置将显示一条红色错误消息。这由 vm.uploadSuccess
和 vm.uploadFailed
的值控制。这些值是由成功回调或错误回调设置的。这就是前端的全部内容。
使用 Java RESTful 服务处理文件上传
文件上传的后端处理一点也不复杂。多部分文件上传可以包含一个或多个文件,以及其他对象(由唯一键标识)。后端处理中最困难的部分是获取同一请求中的文件和其他对象。一旦获得这些,处理就相对容易了。对于本教程,我所做的就是获取数据,对于文件,我将其保存到预定义的位置。对于其他数据对象(这是一个 JSON 格式的对象),我只打印出该对象的一个属性。
为此设置 RESTFul API 控制器很简单。接下来,是最难的部分,如何声明一个可以处理传入请求的请求处理方法,这里是方法声明
@RequestMapping(value="uploadFile", method=RequestMethod.POST)
public ResponseEntity<StatusResponse> uploadFile(
@RequestParam("file") MultipartFile srcFile,
@RequestParam("fileMetadata") String fileMetadata)
{
...
}
Spring Framework 提供了一个名为 MultipartFile
的对象类型。有了它,处理文件上传就容易多了。以下是我获取文件信息的方式,即上述方法的前 4 行
System.out.println("file received.");
System.out.println("file name: " + srcFile.getOriginalFilename());
System.out.println("file size: " + srcFile.getSize());
System.out.println("content type: " + srcFile.getContentType());
当文件被选为文件上传时,文件名(仅文件名,不包括完整的文件路径和文件夹等)可以从代表文件的对象 MultipartFile srcFile
中检索。这是通过方法 getOriginalFilename()
完成的。不要将其与名为 getName()
的方法混淆。如果您调用 getName()
,您猜它会返回什么?返回的值是“file
”,这是我在 JavaScript 代码中分配给文件数据的名称。您也可以在 @RequestParam("file")
的值中找到它。所以,不要混淆这两个方法。
还可以通过调用 getSize()
来获取文件的大小。这将返回文件的整数大小。另一个有用的方法是调用 getContentType()
方法。它可以返回文件的 mime 类型。这很有用,因为如果您将其保存,您可以将其与文件本身一起发送回去。这样就省去了查找合适的系统 API 来查找文件 mime 类型的时间。这是可行的,但是为什么要在第一次上传文件时检索并存储文件 mime 类型时浪费时间和精力去查找合适的系统 API 或第三方 jar 呢?这只是一个想法。
接下来,我想向您展示如何保存文件。MultipartFile
对象类型有一个名为 getInputStream()
的方法。利用它的最简单方法是将输入流复制到 FileOutputStream
。然后关闭输出流,我们就完成了。这是
FileOutputStream outFile = null;
...
try
{
outFile = new FileOutputStream("C:\\DevJunk\\" + srcFile.getOriginalFilename());
IOUtils.copy(srcFile.getInputStream(), outFile);
...
}
...
finally
{
if (outFile != null) {
try
{
outFile.close();
}
catch(Exception ex) {}
}
在上一个代码片段中,我将文件保存到的文件路径硬编码了。我使用了上传时传入的相同文件名。还有其他保存文件的方式,获取所有字节作为字节数组并保存到数据库或磁盘。我不会展示这些是如何完成的。只是给您一些关于其他可能性的想法。
我想解释的最后一个概念是如何将元数据对象从 string
转换为 object
。这是通过 Jackson 框架完成的。Jackson 框架用于将 JSON string
转换为 Java object
,反之亦然。在这种情况下,我有一个基于 string
的 JSON object
。我使用 Jackson 框架将其转换为 Java object
。这是我做到的方法
...
ObjectMapper objectMapper = new ObjectMapper();
FileMetadata fileMeta = objectMapper.readValue(fileMetadata, FileMetadata.class);
...
System.out.println("Additional Metadata: " + fileMeta.getDescription());
...
一旦 string
成功转换为 Java 对象,我使用控制台输出来显示其中的 description 属性。这是上面代码片段的最后一行。
这是 REST API 控制器的全部源代码。
package org.hanbo.boot.rest.controllers;
import java.io.FileOutputStream;
import org.apache.tomcat.util.http.fileupload.IOUtils;
import org.hanbo.boot.rest.models.FileMetadata;
import org.hanbo.boot.rest.models.StatusResponse;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import com.fasterxml.jackson.databind.ObjectMapper;
@RestController
public class SampleFileUploadController
{
@RequestMapping(value="uploadFile", method=RequestMethod.POST)
public ResponseEntity<StatusResponse> uploadFile(
@RequestParam("file") MultipartFile srcFile,
@RequestParam("fileMetadata") String fileMetadata)
{
System.out.println("file received.");
System.out.println("file name: " + srcFile.getOriginalFilename());
System.out.println("file size: " + srcFile.getSize());
System.out.println("content type: " + srcFile.getContentType());
StatusResponse retResp = new StatusResponse();
FileOutputStream outFile = null;
try
{
outFile = new FileOutputStream("C:\\DevJunk\\" + srcFile.getOriginalFilename());
IOUtils.copy(srcFile.getInputStream(), outFile);
ObjectMapper objectMapper = new ObjectMapper();
FileMetadata fileMeta = objectMapper.readValue(fileMetadata, FileMetadata.class);
System.out.println("Additional Metadata: " + fileMeta.getDescription());
retResp.setSummary("Upload successful.");
retResp.setOpSuccess(true);
retResp.setDetailedMessage("Sample test file upload is succesful.");
}
catch(Exception ex)
{
ex.printStackTrace();
retResp.setSummary("Upload failed.");
retResp.setOpSuccess(false);
retResp.setDetailedMessage(String.format
("Sample test file upload has failed. Exception [%s]", ex.getMessage()));
}
finally
{
if (outFile != null) {
try
{
outFile.close();
}
catch(Exception ex) {}
}
}
ResponseEntity<StatusResponse> retVal = ResponseEntity.ok(retResp);
return retVal;
}
}
这就是 Java 方面的全部内容。我知道,源代码(前端和后端)都非常糟糕。它们仅用于演示目的。它们应该清楚地展示文件上传是如何工作的。
如何测试
要运行示例应用程序,请打开命令提示符。然后 cd
进入此应用程序源代码所在的基目录。运行以下命令
mvn clean install
确保构建成功。然后使用以下命令启动服务
java -jar target\hanbo-agularjs-uploadformdata-1.0.1.jar
服务成功启动后,使用浏览器导航到
https://:8080/
页面将显示如下
使用“**浏览**”按钮选择一个文件,然后点击“**上传**”。您将看到文件被存储在您指定的位置。尽管您可能希望在运行示例应用程序之前更改文件存储的位置。
摘要
这不是一个出色的教程,也不是一个伟大的教程。我希望它是一个不错的教程。在本教程中,我想展示如何使用 AngularJS $http
和 JavaScript 对象类型 FormData
上传文件。在 Java 端,我使用 RESTFul API 控制器来处理上传。
除了正常的文件上传操作之外,本教程还展示了如何将额外数据作为对象包含在上传请求中。用 Jackson 框架可以将表示为 string
的数据转换为对象,将 JSON string
转换为 Java object
。
后端服务功能不多,只是将文件保存到磁盘并将接收到的数据打印到后端。我特意将其做得简单,以便我可以参考本教程,从而构建更复杂的功能。本质上,我为自己写了这个,以便有一天我需要时可以回来查看。总之,它对我会有帮助。我希望它对您也有帮助。祝您好运!
历史
- 2020 年 12 月 24 日 - 初稿