65.9K
CodeProject 正在变化。 阅读更多。
Home

AngularJS FormData 多部分文件上传

emptyStarIconemptyStarIconemptyStarIconemptyStarIconemptyStarIcon

0/5 (0投票)

2021 年 1 月 19 日

MIT

11分钟阅读

viewsIcon

20693

downloadIcon

145

又一篇关于使用 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 的对象。
  • 使用 $httpFormData 发送到后端 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.uploadSuccessvm.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 日 - 初稿
© . All rights reserved.