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

在 Angular 应用程序中集成文件上传

starIconstarIconstarIconstarIconstarIcon

5.00/5 (1投票)

2023年8月15日

MIT

23分钟阅读

viewsIcon

8569

downloadIcon

74

在本教程中,我将讨论如何在 Angular Web 应用程序中添加文件上传功能。

引言

在本教程中,我想讨论我在上一篇文章中已经涵盖过的内容——如何在单页 Web 应用程序中上传文件。我之前的一篇教程与从 AngularJS Web 应用程序上传文件有关。在这个新教程中,我想讨论如何从基于 Angular 的 Web 应用程序上传文件。两者的实现方法几乎相同。然而,存在一些细微的差异值得讨论。例如,在开发基于 Angular 的 Web 应用程序时,通常将其开发为一个独立于基于 Spring Boot 的服务应用程序项目的项目。如果我必须对两者进行集成测试,我需要在 Spring Boot 项目上进行一些 CORS 配置,以便两者可以成功通信。Angular 应用程序上传文件的方式与 AngularJS 应用程序上传文件的方式几乎相同。编程语法完全不同。为了使本教程更有趣,我还将描述将 ng-bootstrap 集成到 Angular 应用程序代码中的步骤,以便我可以使用模态弹出窗口。这些并不是最有趣的要点。请继续阅读,我解释如何从文件输入字段中提取文件数据的部分将是本教程中最有价值的部分。我保证你不会失望。

整体架构

我为本教程准备的示例应用程序是一个单页 Web 应用程序。页面上有一个按钮。当用户点击此按钮时,将显示一个弹出窗口。在弹出窗口上,用户可以使用文件输入字段指定要上传的文件。将有两个按钮,一个是上传文件;另一个是关闭弹出窗口。一旦用户成功上传文件并点击关闭按钮关闭弹出窗口,如果文件是图像文件,则基本页面将显示该文件。如果用户上传非图像文件,则基本页面将显示一个损坏的图像。

前端应用程序是在 node.js 项目中开发的。大部分讨论将集中在这个项目上。我将解释如何设置项目、添加 ng-bootstrap 弹出窗口以及将文件上传发送到后端。后端 Web 应用程序是作为 Spring Boot Web 应用程序开发的。后端 Web 应用程序有两个请求处理方法。一个用于处理文件上传请求。另一个用于将文件数据发送回前端 Web 应用程序以进行显示。

本教程将重点介绍 node.js 项目。在本教程的前半部分,我将从 node.js 项目设置开始。接下来,我将解释如何将 ng-bootstrap 组件集成到 Angular 应用程序中。在第一部分的最后,我将向您展示如何从 Angular 应用程序执行文件上传操作。我还会指出我学到的非常有用的东西。在本教程的后半部分,我将介绍 Spring Boot 项目中的工作。由于这两个项目是分开完成的,因此需要进行 CORS 配置,以便 Angular 应用程序可以与 Spring Boot 应用程序正确通信。我需要重新审视这部分,提醒自己 CORS 配置对于当今的 Web 应用程序至关重要。

在下一节中,我将从 Angular 应用程序开始讨论,首先是项目设置。

Angular 项目

正如我之前提到的,开发 Angular 应用程序的过程与开发 AngularJS 应用程序的过程不同。Web 应用程序的源代码必须位于 node.js 项目中。Angular 框架的官方网站上有一个教程,教您如何创建 Angular 应用程序。或者,您可以修改现有的 Angular 项目以实现一些新目的。本教程采用的是这种替代方法。在此过程中,我对项目进行了一些升级。如果您查看 node.js 项目文件 package.json,您会看到

  • 我使用 Angular 框架的 14.1.0 版本。
  • 6.2.1
  • 我使用 fontawesome 字体库的 6.2.1 版本。这可能有点旧。
  • 我使用 ng-bootstrap 库的 13.1.0 版本。

这是我的项目的 package.json 文件

{
  "name": "angular-fileupload-app",
  "version": "0.0.0",
  "scripts": {
    "ng": "ng",
    "start": "ng serve",
    "build": "ng build",
    "watch": "ng build --watch --configuration development",
    "test": "ng test"
  },
  "private": true,
  "dependencies": {
    "@angular/animations": "^14.1.0",
    "@angular/common": "^14.1.0",
    "@angular/compiler": "^14.1.0",
    "@angular/core": "^14.1.0",
    "@angular/forms": "^14.1.0",
    "@angular/platform-browser": "^14.1.0",
    "@angular/platform-browser-dynamic": "^14.1.0",
    "@angular/router": "^14.1.0",
    "@fontawesome/fontawesome-free": "^6.2.1",
    "@ng-bootstrap/ng-bootstrap": "^13.1.0",
    "bootstrap": "^5.2.0",
    "jquery": "^3.6.0",
    "rxjs": "~7.5.0",
    "tslib": "^2.3.0",
    "zone.js": "~0.11.4"
  },
  "devDependencies": {
    "@angular-devkit/build-angular": "^14.1.1",
    "@angular/cli": "~14.1.1",
    "@angular/compiler-cli": "^14.1.0",
    "@types/jasmine": "~4.0.0",
    "jasmine-core": "~4.2.0",
    "karma": "~6.4.0",
    "karma-chrome-launcher": "~3.1.0",
    "karma-coverage": "~2.2.0",
    "karma-jasmine": "~5.1.0",
    "karma-jasmine-html-reporter": "~2.0.0",
    "typescript": "~4.7.2"
  }
}

另一个重要的文件是 angular.json。此文件包含 Angular 项目的配置。假设您想向要打包用于生产的应用程序添加额外的 JavaScript 或 CSS 文件。这是我可以添加 JQuery 和 Bootstrap JavaScript 文件和样式文件的文件。如果您有兴趣了解如何集成 JQuery、Bootstrap 和 font-awesome 组件,请查看此文件的内容。

最后,我想提一下另一个用于编译 Angular 项目的重要文件。它是 tsconfig.json 文件。此文件是自动生成的。并且它是可修改的。我在名为 "compileOptions" 的部分中进行了两处更改。第一个是 outDir。我将值更改为一个文件夹,前端应用程序可以包含在 Java 项目中,这样当 Java 项目编译和打包时,前端 Web 应用程序可以作为一个整体集成到后端服务项目中。另一行是我添加的

...
    "strictPropertyInitialization": false,
...

这是非常重要的一行。它允许我在 TypeScript 类中创建属性,而无需通过其构造函数进行初始化。这允许我通过注解(即通过依赖注入)初始化类的属性。这也与我希望在下一节中指出的最有趣的“部分”有关。

还有一个 tsconfig.app.json 文件。它用于编译生产部署。您可以更改 compileOptions 部分中的 "outdir" 属性。这将设置前端应用程序用于生产部署的输出文件夹。

下一节内容会很多。在其中,我将详细解释 Angular 应用程序的设计。我多次提到的最有趣的“部分”也将在本节中解释。

如何在 Angular 应用程序中上传文件

Angular 应用程序包含两部分。第一部分是索引页,它有一个按钮,并且还显示正在上传的文件(仅限于图像文件)。第二部分是弹出窗口,用户可以在其中指定要上传的文件。由于第二部分最有趣,我将从它开始。所有有趣的内容都在这个弹出组件中。我将首先从弹出组件的 HTML 标记页开始。标记代码如下所示

<div class="modal-header">
   <h4 class="modal-title">Upload File</h4>
   <button type="button" class="btn-close" aria-label="Close" (click)="cancelPopup()">
   </button>
</div>
<form>
<div class="modal-body">
   <div class="mb-1">
      <label for="uploadFileField" class="form-label">Select a file</label>
      <input class="form-control" type="file"
             name="uploadFileField" id="uploadFileField" #fileToUpld />
   </div>
</div>
<div class="modal-footer">
   <button type="button" class="btn btn-outline-dark" (click)="uploadFile()">Upload
   </button>
   <button type="button" class="btn btn-outline-dark" (click)="closePopup()">Close
   </button>
</div>
</form>

我正在使用 Bootstrap 5 的模态组件。模态弹出窗口可以分为三部分:标题、正文和页脚。在标题部分,我添加了一个“x”按钮,用于关闭模态框。此按钮的事件处理程序是一个名为 cancelPopup() 的方法。我们将在本节的后面部分介绍此方法。在正文部分,我只使用了一个文件输入字段。用户可以在此处选择要上传的文件。这也是本教程最有趣的部分即将出现的地方。标题中的按钮取消上传操作。底部的按钮关闭弹出窗口,并将上传的文件信息传递给索引页,以便显示该文件(如果是图像文件)。

要上传和显示文件,我必须执行几个操作。首先,我需要从文件输入字段获取文件数据。接下来,我需要将文件内容打包到请求中并发送到后端服务。一旦后端服务收到文件内容,它可以作为文件保存到磁盘。然后后端服务将发送一个包含文件名的响应回 Angular 应用程序。弹出模态组件将持有来自后端的 HTTP 响应。当用户点击底部的“关闭”按钮时,包含文件信息的响应数据将传递到索引页。当模态关闭并且图像文件信息可用时,索引页代码将构建一个显示文件的 URL(如果它是图像文件)。

让我从选择文件的字段开始。此字段的标记如下所示

...
      <input class="form-control" type="file"
             name="uploadFileField" id="uploadFileField" #fileToUpld />
...

此输入字段没有关联的 ngModel。它只是一个像 #fileToUpld 这样的标签。这是一个重要的标签,它允许我在 Angular 代码中获取此 HTML 字段的引用,从而允许我通过注解初始化此弹出组件的属性。这是本教程最有趣的部分。一旦我可以获得此特定字段的引用,我就可以对其进行编码以提取上传文件的内容。

让我跳到弹出组件的 Angular 代码,向您展示如何使用此字段

import { Component, ViewChild, ElementRef } from '@angular/core';
...
...
@Component({
   ...
})
export class UploadPopupComponent {
   @ViewChild("fileToUpld")
   private _fileUploadField: ElementRef;
   
   ...
   ...
   public uploadFile(): void {
      if (this._fileUploadField &&
        this._fileUploadField.nativeElement) {
           if (this._fileUploadField.nativeElement.files && 
               this._fileUploadField.nativeElement.files.length > 0) {
              let fileData: any = this._fileUploadField.nativeElement.files[0];
    ...
    ...
          }
      }
   }
   ...
   ...
}

如果您以前从未接触过 Angular(我从亲身经历中知道这一点),上面的代码可能会有点难懂。在弹出组件的标记中,我定义了一个可以保存文件的输入字段。该定义包含标签 #fileToUpld。在上面的 Angular 组件类中,我需要此输入字段的引用。这是声明和初始化元素引用属性的行

   @ViewChild("fileToUpld")
   private _fileUploadField: ElementRef;

注解 @ViewChild("fileToUpld") 将 HTML 元素(带有标签 #fileToUpld 的文件输入字段)链接到弹出窗口上的 _fileUploadField 属性。此属性的类型为 ElementRef。此声明还使用对 HTML 元素的实际引用初始化该属性。请记住,在项目的 tsconfig.json 文件中,我有这个

...
    "strictPropertyInitialization": false,
...

如果没有这个,ElementRef 属性的声明将编译失败。错误将是关于此组件的属性从未在构造函数中初始化。此属性不需要初始化。它可能是通过注解通过依赖注入初始化的。

一旦我获得了文件输入字段的引用,我就可以提取用户选择的文件。具体方法如下:

if (this._fileUploadField &&
   this._fileUploadField.nativeElement) {
      if (this._fileUploadField.nativeElement.files && 
          this._fileUploadField.nativeElement.files.length > 0) {
         let fileData: any = this._fileUploadField.nativeElement.files[0];
...
...
   }
}

this._fileUploadField.nativeElement 是对实际 HTML 输入字段的引用。由于此输入字段表示选定的文件或文件(它允许用户一次选择多个文件)。本机元素引用将具有一个名为 files 的属性。它是一个数组,包含一个或多个文件的数据内容。由于我假设每次只上传一个文件,因此我只会获取此数组中的第一个文件

...
let fileData: any = this._fileUploadField.nativeElement.files[0];
...

对于我的文件输入字段,我从未将 ngModel 属性绑定到此字段。这是故意的。如果我绑定了 ngModel 属性,并且当应用程序运行时,调试控制台中将显示错误。在这种情况下,绑定 ngModel 属性是完全不必要的。如果您不相信我,欢迎您尝试一下。

接下来,我将向您展示如何执行文件上传操作。我们已经看到了从输入字段提取文件内容的代码逻辑。获取内容数据后,我们调用服务对象上传文件。具体操作如下:

...
   let fileData: any = this._fileUploadField.nativeElement.files[0],
       self = this;
   self.isUploadingMediaFile = true;
   this._fileUploadSvc.uploadFile(fileData)
      .subscribe((resp: any) => {
         if (resp) {
            if (resp.operationSuccess) {
               self._fileName = resp.fileName;
               self.isUploadingMediaFile = false;
               alert("Upload successful.");
            } else {
               self.isUploadingMediaFile = false;
               if (resp.statusMessage && resp.statusMessage.trim() !== "") {
                  alert("Error occurred: " + resp.statusMessage);
               } else {
                  alert("Unable to upload media file. Unknown error.");
               }
            }
         } else {
            self.isUploadingMediaFile = false;
            alert("Response object is invalid. Unknown error.");
         }
      }, (error: HttpErrorResponse) => {
         self.isUploadingMediaFile = false;
         console.log(error);
         alert("Failed to upload file. See JavaScript console output for more details.");
      });
...

这是 TypeScript 文件 uploadPopup.component.tsuploadFile() 方法的一部分。调用服务对象进行上传的行是这个

...
this._fileUploadSvc.uploadFile(fileData)
...

服务对象名为 _fileUploadSvc。它在组件类的顶部声明和初始化。服务类在名为 fileUpload.service.ts 的 TypeScript 文件中定义。这个类出奇地简单。它有一个构造函数,有助于初始化文件上传操作所需的 HttpClient 对象。除了构造函数,只有一个方法 uploadFile()。以下是整个类的定义

import { Injectable } from '@angular/core';
import { HttpClient } from "@angular/common/http";
import { Observable } from 'rxjs';
import { environment } from '../../environments/environment';

@Injectable({
   providedIn: 'root',
})
export class FileUploadService {
   constructor(private _uploadClient: HttpClient) {
      
   }
   
   public uploadFile(fileData: any): Observable<any> {
      let formData:FormData = new FormData();
      formData.append("MediaUpload", fileData);
      return this._uploadClient.post<any>
             (environment.apiBaseUrl + "api/fileUpload", formData);
   }
}

在 Angular 应用程序中进行文件上传的方式与在 AngularJS 应用程序中完全相同。我们需要执行 HTTP post 并将文件内容打包到 FormData 对象中。老实说,我在研究时并没有意识到这一点。当所有在线搜索都指向相同的结果时,我才意识到我正在追寻一个我已经知道的答案。FormData 是 JavaScript 内置数据类型。它表示“古老”的 HTTP 表单请求数据,包括文件内容。FormData 对象是键值对的集合。在 Spring Boot 端,JavaScript 数据对象 FormData 被称为 MultipartFile 对象。

上述代码片段创建了一个 FormData 对象。然后它添加了一个键值对,其中键名为 MediaUpload,值为文件内容数据。最后,它调用 httpClient 对象向后端服务发送一个 POST 请求。可以通过调用 HttpClient 类型的 post() 方法将 post 请求发送到后端服务。它接受几个参数。第一个是表示服务 URL 的 string 值。第二个是 POST 请求的请求数据。post() 方法可以接受更多参数,例如头部集合、URL 参数等等。在这个简单的演示应用程序中,我们所需的参数只有后端服务的 URL 和请求正文的内容数据。

让我们回到索引组件,看看如何调用弹出模态框。在文件 index.component.ts 中,您可以找到以下代码段

...
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
import { UploadPopupComponent } from '../uploadPopup/uploadPopup.component';
import { environment } from '../../environments/environment';

@Component({
...
})
export class IndexComponent implements OnInit {
   
...

   constructor(private popup: NgbModal) {
   }
...
   public handleClickUploadBtn(): void {
      const popupInst: NgbModalRef = this.popup.open(UploadPopupComponent);
      popupInst.componentInstance.name = "Choose Media to Upload";
      let result:Promise<any> = popupInst.result;
      let self = this;
      
      result.then((x) => {
         if (x && x.completed) {
            if (x.fileName != null && x.fileName.trim() !== "") {
               self.imageFile = environment.apiBaseUrl + 
                                "api/downloadFile/" + x.fileName
            }
         }
      }, (x) => { });
   }
}</any>

要显示弹出模态框,我必须从 ng-bootstrap 库导入 NgbModalNgbModalRef。这些在 handleClickUploadBtn() 方法中使用。如您所见,首先,我需要声明一个 NgbModalRef 类型的对象。此引用用于持有弹出模态框的引用。声明变量后,可以通过调用实例属性 this.popupopen() 方法创建弹出模态实例。输入参数是上传弹出类类型的类定义。这就是弹出模态框的显示方式。一旦弹出模态框显示,就可以获取对其的引用。接下来,我想获取弹出模态框的 Promise。根据用户采取的操作,关闭弹出模态框可能导致两种可能的结果。一是用户完成操作并解决 Promise。另一个是用户取消操作并拒绝 Promise。处理这两种结果的方式与我们在 AngularJS 中所做的方式相同。Promise 有一个名为 then() 的方法。它接受两个参数。两者都是回调方法的引用。第一个方法用于处理用户在弹出模态框上操作的完成。第二个方法用于处理用户取消操作的情况。在上面的代码段中,我只关心用户完成他们在弹出模态框上的操作的可能性。它所做的一切就是检查用户是否确认操作成功。然后它将构建一个可以下载文件的 URL。此 URL 用于视图上以显示刚刚上传的文件。

在索引页上,下载 URL 用于显示上传的文件,如下所示

...
<div class="row" *ngIf="imageFile != null && imageFile.trim() !== ''">
   <div class="col">
      <img [src]="imageFile" class="rounded mx-auto d-block img-fluid" 
       alt="Uploaded file display here." />
   </div>
</div>
...

这就是 Angular 项目的全部内容。在继续之前,我想强调为什么我认为使用 @ViewChild() 注解是本教程中最有趣的部分。它可以通过将元素从视图注入到组件类中使用。在组件类中,代码可以执行操作,例如操作 HTML 字段、从字段中提取值、更改样式以及许多其他所需的工作。示例应用程序演示了此注解的完美用法。使用此注解,我们将 HTML 元素注入到组件类中。然后,代码可以从此文件输入字段中提取文件内容数据,并将其用于上传操作。

在 Angular 应用程序方面,我们已经看到了文件上传操作的工作原理,以及它如何调用后端下载文件并显示。现在是时候看看后端如何处理这些请求了。这些都不是新内容。我将回顾这些已知技术。

后端服务请求处理

处理文件上传和文件下载的后端服务并不复杂。我在之前的教程中已经介绍过这些操作。我想介绍一些我之前教程中没有提到的重要注意事项。现在我可以在这里讨论这些。后端应用程序是一个 Spring Boot Web 应用程序。它有两个控制器。一个控制器用于处理文件上传请求。另一个控制器用于处理文件下载请求。由于调用客户端是独立的应用程序,当两者交互时,必须在后端应用程序上进行 CORS 配置,以便 Angular 应用程序可以成功地将请求传递给它。

我已经在我的上一篇教程中介绍了 CORS 配置。但我将在这里再次回顾它。Angular 应用程序在 localhost 端口 4200 上运行。后端服务在 localhost 端口 8080 上运行。由于端口差异,我必须将带端口 4200 的 localhost URL 添加到后端服务的 CORS 配置中。我在配置类中硬编码了 CORS 配置,如下所示

package org.hanbo.boot.rest.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebAppConfig
{
   @Bean
   public WebMvcConfigurer corsConfigurer()
   {
      String[] allowDomains = new String[2];
      allowDomains[0] = "https://:4200";

      return new WebMvcConfigurer() {
         @Override
         public void addCorsMappings(CorsRegistry registry) {
            registry.addMapping("/**").allowedOrigins(allowDomains)
                    .allowedMethods("GET", "POST", "HEAD", "PUT", "DELETE");
         }
      };
   }
}

如您所见,代码段不难理解。我所需要做的就是将允许的域 URL 数组(包括 URL 中的端口号)和所有允许的 HTTP 请求方法添加到 CORS 配置映射中。如果我不这样做,浏览器调试控制台中会出现一些错误,表明后端服务拒绝请求,因为默认的 CORS 映射不允许这些请求通过。

接下来,我想讨论如何设置上传文件大小。这非常重要。Spring Boot Web 应用程序的默认上传文件大小为 1 MB。如果您不将上传文件大小设置为优化值,您将无法上传超过 1 MB 的文件。这可能会很烦人。它需要更改代码并重新部署才能修复。我宁愿尽早完成此操作。这可以在应用程序配置文件中轻松完成。您可以在文件 src/main/resources/application.properties 中找到它。我只需要这一行

spring.servlet.multipart.max-file-size=400MB
spring.servlet.multipart.max-request-size=2GB

第一行指定一个文件的最大文件大小。第二行指定整个请求的最大文件大小。这意味着在一个 HTTP post 请求中,它可以附加最多 5 个文件,每个文件的大小最大为 400 MB。它也可以意味着请求可以包含尽可能多的文件,只要所有文件大小加起来不超过 2 GB,并且单个文件的大小不超过最大 400 MB。

让我向您展示处理文件上传的控制器

package org.hanbo.boot.rest.controllers;

import java.io.File;
import java.io.FileOutputStream;

import org.apache.commons.io.IOUtils;
import org.hanbo.boot.rest.models.UploadResponse;
import org.springframework.beans.factory.annotation.Value;
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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@RestController
public class FileUploadController
{
   private static Logger _logger = LoggerFactory.getLogger(FileUploadController.class);
   
   @Value("${resource.basedir}")
   private String uploadFileBaseDir;
   
   @RequestMapping(value="/api/fileUpload", method=RequestMethod.POST)
   public ResponseEntity<UploadResponse> uploadFile(
         @RequestParam("MediaUpload") MultipartFile srcFile)
   {
      UploadResponse resp = new UploadResponse();
      if (srcFile != null)
      {
         String origFileName = srcFile.getOriginalFilename();
         String contentType = srcFile.getContentType();
         
         System.out.println("Uploaded File Name: " + origFileName);
         System.out.println("Uploaded File MIME Type: " + contentType);
         
         FileOutputStream writeToFile = null;
         try
         {
            String fileName = String.format("%s/%s", uploadFileBaseDir, origFileName);
            File destMediaFile = new File(fileName);
            
            writeToFile = new FileOutputStream(destMediaFile);
            IOUtils.copy(srcFile.getInputStream(), writeToFile);
            writeToFile.flush();
            
            _logger.info(String.format
                    ("Uploaded file has been saved to [%s].", fileName));
            
            resp.setFileId("000000001");
            resp.setFileName(origFileName);
            resp.setMimeType(contentType);
            resp.setOperationSuccess(true);
            resp.setStatusMessage(String.format
                 ("File [%s] has been saved successfully.", origFileName));
         }
         catch (Exception ex)
         {
            String errorDetails = String.format
            ("Exception occurred when write media file to disk: %s", ex.getMessage());
            _logger.error("MediaMgmtServiceImpl.saveMediaFile: " + errorDetails);
            throw new RuntimeException(ex);
         }
         finally
         {
            if (writeToFile != null)
            {
               try
               {
                  writeToFile.close();
               }
               catch(Exception ex)
               { }
            }
         }
      }
      else
      {
         resp.setFileId("");
         resp.setFileName("");
         resp.setMimeType("");
         resp.setOperationSuccess(false);
         resp.setStatusMessage("HTTP request does not contain a file object.");
      }
      
      return ResponseEntity.ok(resp);
   }
}

请求处理方法将接收 MultipartFile 对象作为输入。该参数带有 @RequestParam() 注解,因为它是一个基于表单的请求。该方法将提取文件名、文件的 MIME 类型和文件内容流。我使用 IOUtils.copy() 将数据从请求输入流复制到目标文件输出流。一切成功后,该方法将返回一个操作结果对象给调用者。

这是处理文件下载的控制器

package org.hanbo.boot.rest.controllers;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;

@RestController
public class FileDownloadController
{
   private static Logger _logger = 
           LoggerFactory.getLogger(FileDownloadController.class);

   @Value("${resource.basedir}")
   private String resourceFileBaseDir;
   
   @RequestMapping(value="/api/downloadFile/{fileName}", method=RequestMethod.GET)
   public ResponseEntity<StreamingResponseBody> downloadFile(
         @PathVariable("fileName") String fileName)
   {
      System.out.println("Received file name: " + fileName);
      ResponseEntity<StreamingResponseBody> retVal = null;
      if (StringUtils.hasText(fileName))
      {
         String fileFullPath = String.format("%s/%s", resourceFileBaseDir, fileName);
         
         try
         {
            File f = new File(fileFullPath);
            if (f.exists() && f.isFile())
            {
               StreamingResponseBody respBody = 
                        loadFileDataIntoResponseStream(fileFullPath);
               if (respBody != null)
               {
                  Path path = Paths.get(fileFullPath);
                  String mimeType = Files.probeContentType(path);
                  
                  retVal = ResponseEntity.ok()
                        .header("Content-type", mimeType)
                        .header("Content-length", String.format("%d", f.length()))
                        .body(respBody);
               }
               else
               {
                  retVal = ResponseEntity.notFound().build();
               }
            }
            else
            {
               _logger.error(String.format("File not found: %s", fileFullPath));
               retVal = ResponseEntity.internalServerError().build();
            }
         }
         catch(Exception ex)
         {
            _logger.error(String.format("Exception occurred: %s", ex.getMessage()));
            retVal = ResponseEntity.internalServerError().build();
         }
      }
      else
      {
         _logger.error(String.format("Invalid file name: %s", fileName));
         retVal = ResponseEntity.badRequest().build();
      }
      
      return retVal;
   }
   
   private StreamingResponseBody loadFileDataIntoResponseStream(String imageFileFullPath)
   {
      StreamingResponseBody retVal = null;
      File imageFile = new File(imageFileFullPath);
      if (imageFile.exists() && imageFile.isFile())
      {
         try
         {
            retVal = new StreamingResponseBody()
            {
               @Override
               public void writeTo(OutputStream outputStream) throws IOException
               {
                  FileInputStream fs = null;
                  try
                  {
                     fs = new FileInputStream(imageFile);
                     IOUtils.copy(fs, outputStream);
                     outputStream.flush();
                  }
                  finally
                  {
                     outputStream.close();
                     if (fs != null)
                     {
                        fs.close();
                     }
                  }
               }
            };
         }
         catch (Exception ex)
         {
            _logger.error(String.format("Exception: %s", ex.getMessage()));
            retVal = null;
         }
      }
      else
      {
         _logger.error(String.format("Unable to find image file [%s]. 
                       file does not exist. Error 404", imageFileFullPath));
         retVal = null;
      }
      
      return retVal;
   }
}

这个控制器有两个方法。一个方法用于处理请求。另一个是辅助方法,它将文件内容从磁盘加载到可以打包到 HTTP 响应中的输出流。以前,我会将 HttpServletResponse 对象作为参数“注入”到请求处理方法中。然后我可以将文件内容写入 HttpServletResponse 对象的输出流中。这就是我们以前习惯于返回大型数据对象的方式。现在的新方法是返回一个 ResponseEntity<StreamingResponseBody> 类型的对象。StreamingResponseBody 将是作为输出流的文件内容的包装器。我认为这种新方法比旧方法更简洁。我将在未来的工作中采用这种方法。

至此,我已解释了本教程的所有重要方面。在下一节中,我将向您展示如何运行示例应用程序。

如何运行示例应用程序

是时候运行示例应用程序了。在本节中,我将教您另一个技巧。有一种方法可以将 Angular 应用程序集成到 Spring Boot 项目中,这样您就不必单独运行两个应用程序。首先,让我们看看单独运行两个应用程序的方式。

在 Spring Boot 项目中,application.properties 文件包含用于存储文件上传的基目录。您必须使用应用程序可以读写该目录的路径来指定它

resource.basedir=/home/<Your home folder>/Projects/DevJunk/AngularFileUpload

首先,进入基目录下的 webapp 目录。运行以下命令安装 Angular 应用程序所需的所有 node.js 库

npm install 

下载并安装 Angular 项目的 node_modules 将需要几秒到几分钟。在撰写本文时,我发现安装的库存在两个中等严重程度的问题。我相信如果我使用更新版本的库重新启动项目,问题就会消失。请忽略此问题。在终端窗口中运行以下命令以运行 Angular 应用程序

ng serve 

应用程序将在构建过程完成后启动。如果这是第一次运行,可能需要几分钟。随后的构建将花费更少的时间。如果运行成功,Web 应用程序将在以下 URL 上运行

https://:4200/ 

您还需要运行 Spring Boot 应用程序。如果您不运行,Angular 应用程序将无法正常工作。首先,您需要运行 Maven 构建来编译和打包后端服务应用程序。命令如下:

mvn clean install 

Java 构建成功后,您可以在终端窗口中运行以下命令以运行 Spring Boot 应用程序

java -jar target/hanbo-angular-file-upload-1.0.1.jar 

如果您一切操作正确,两个应用程序应该同时运行。测试的 URL 是:https://:4200/。当您第一次导航到它时,您将看到以下内容

您可以将应用程序作为一个应用程序开始测试。您需要执行以下步骤

步骤 1: 点击按钮 “选择要上传的文件”。

步骤 2: 当 ng-bootstrap 弹出窗口显示时,点击文件输入字段以指定要上传的文件。请选择图像文件(jpg/png/gif)。

步骤 3: 在弹出窗口上,点击“上传”按钮。上传成功后,将显示一个警报窗口,指示文件上传成功。

步骤 4: 在弹出窗口上,点击“关闭”按钮以关闭弹出窗口。索引页将显示新上传的文件。

可选步骤:在弹出窗口上,如果您点击标题栏上的“X”。它将取消操作。如果您上传了图像文件。文件将不会显示。如果显示了旧图像,图像将不会更改为新上传的图像。

现在,我将讨论我添加的惊喜——将 Angular 应用程序集成到 Spring Boot 项目中。当您构建 Angular 应用程序时,构建过程会将所有内容打包成几个压缩文件。您可以指定将这些文件放置在 Spring Boot 项目的某个目录中。然后,当您构建 Spring Boot 应用程序时,Angular 应用程序就可以成为其中的一部分。您需要做的就是从 Spring Boot 应用程序打开索引页,然后您可以重复在该页面上的所有演示操作步骤。

要指定最终的索引页和相关的 JavaScript 文件输出到哪里,您只需打开 Angular 项目中的 angular.json 文件。在 JSON 的 "architect" -> "build" -> "options" 部分,有一个属性 "outputPath"。您可以将输出路径设置为 Spring Boot 项目的目录 "src/main/resources/static"。我已经为您设置好了,如下所示

...
      "architect": {
        "build": {
          "builder": "@angular-devkit In the s/build-angular:browser",
          "options": {
            "outputPath": "../src/main/resources/static",
            "index": "src/index.html",
...

这是 Spring Boot Web 应用程序的静态内容可以提供给用户的地方。当 Angular 应用程序文件在此处生成时,它们将成为 Spring Boot 项目的一部分。以下是尝试此集成的步骤

步骤 1: 在终端窗口中,进入 Angular 项目的目录。然后运行以下命令

ng build 

步骤 2: Angular 项目构建完成后,进入 Spring Boot 项目的目录 src/main/resources/static,检查 Angular 应用程序的文件。您应该在该目录中看到以下文件

步骤 3: 在 Spring Boot 项目的根文件夹中运行以下命令

mvn clean install 

步骤 4: Spring Boot 项目构建完成后。继续启动 Spring Boot 应用程序。您无需单独启动 Angular 应用程序。它是 Spring Boot 应用程序的一部分。运行以下命令在终端窗口中启动 Spring Boot 应用程序

java -jar target/hanbo-angular-file-upload-1.0.1.jar 

步骤 5: 导航到此 URL 并运行演示步骤

https://:8080/index.html 

很有趣,不是吗?如果您能做到这一点,那么您已经正确地完成了所有操作。请随意探索源代码。

摘要

我希望你喜欢这个教程。我回收了很多旧材料,例如文件上传操作机制、从文件输入字段获取文件内容的方法、两个不同应用程序之间通信的 CORS 配置,以及将 Angular 应用程序文件复制到 Spring Boot 项目的构建集成。对我来说,最大的收获是使用 @ViewChild() 注解将 HTML 元素字段注入到 Angular 组件类中。从那里,我可以获取 HTML 字段的本机元素引用,并自由地操作该字段。这种能力使我能够从输入字段获取文件内容,以便我可以将文件内容传递给 Angular 服务对象进行上传。另一个很好的收获是我们可以将 Angular 应用程序打包为 Spring Boot 应用程序的一部分。在本教程中,我已经描述了如何做到这一点。但是,node.js 应用程序可以通过多种方式托管,因此不一定需要打包到 Spring Boot 项目中。

本教程中讨论的其他概念与我之前的教程中已经涵盖的类似。我只是想要一个连接 AngularJS 和 Angular 之间差距的教程。我相信本教程已成功实现了这一点。我写这个教程非常开心。希望您喜欢。感谢您阅读本教程,祝您好运。

历史

  • 2023年7月29日 - 初稿
© . All rights reserved.