1. 程式人生 > >用Spring Boot & Angular2快速開發檔案上傳服務

用Spring Boot & Angular2快速開發檔案上傳服務

檔案上傳可以作為一個獨立的微服務。用Spring Boot和Angular2開發這樣的服務非常有優勢,可以用最少的程式碼,實現非常強的功能。如果比了解Spring Boot和Angular2的,請先看這幾個文章:

Angular2檔案上傳元件

Angular2帶來了全新的元件模型。如果你熟悉面向物件這個程式語言正規化,就會發現這個新的元件模型和麵向物件非常接近。我在學習到Angular2的元件模型的時候非常的激動,因為我覺得這是對前端開發模型的一個革命,就像C++語言之於組合語言。它的好處顯而易見,使用元件對程式碼基本上沒有侵入性,容易寫出高內聚鬆耦合的程式碼,等等。

言歸正傳,Angular2的檔案上傳元件我使用了這個:https://github.com/valor-software/ng2-file-upload/,然後簡化了它的官方示例。下面是開發的步驟。

建立Angular2專案

在node環境中,先安裝Angular2的CLI:
npm install -g angular-cli

用CLI的好處是,幾個簡單的命令就可以初始化Angular2專案,並且會自動生成相應的檔案,不用自己手寫了,另外打包釋出也非常方便。用下面的命令建立uploader-client專案:
ng new uploader-client
之後進入uploader-client目錄,可以看到專案的相關檔案都生成好了,我們可以直接開發業務程式碼了。具體請參考官網:https://github.com/angular/angular-cli

實現上傳功能

進入uploader-client/src/app,下面app打頭的檔案我們需要修改一下:

app.module.ts:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';

import { AppComponent } from './app.component';

import { FileUploadModule } from 'ng2-file-upload';

@NgModule({
  declarations: [
    AppComponent,
  ],
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule,
    FileUploadModule
], providers: [], bootstrap: [AppComponent] }) export class AppModule { }

該檔案匯入了檔案上傳元件(FileUploadModule),因此修改的程式碼總共有2行,其它程式碼都是自動生成的。增加的行用綠底色標出子,但是由於頁面編輯器的原因,實際上頁面看不到綠色,看到的可能是這樣的字串:
<span style="background-color: rgb(51, 204, 0);">

請自行腦轉,下面的程式碼有多處也是這樣的。

app.component.ts:

import { Component } from '@angular/core';
import { FileUploader, FileSelectDirective } from 'ng2-file-upload';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  title = 'app works!';
  
  public url:string = 'http://localhost:8080';
  public uploader:FileUploader = new FileUploader({url: this.url});
}
這個檔案添加了3行程式碼,其它也是自動生成的。增加的行也很簡單,就是添加了一個名字叫做uploader的屬性,是FileUploader的例項。

app.component.html

<h1>
  {{title}}
</h1>

<div>
  <h3>Select files</h3>
  
  Multiple
  <input type="file" ng2FileSelect [uploader]="uploader" multiple ><br/>
  
  Single
  <input type="file" ng2FileSelect [uploader]="uploader" />
  
  <h3>Upload queue</h3>
  <p>Queue length: {{ uploader?.queue?.length }}</p>

  <table class="table">
    <thead>
    <tr>
    <th width="30%">Name</th>
      <th width="5%">Size</th>
      <th width="20%">Progress</th>
      <th width="20%">Status</th>
      <th width="25%">Actions</th>
    </tr>
    </thead>

    <tbody>
    <tr *ngFor="let item of uploader.queue">
      <td><strong>{{ item?.file?.name }}</strong></td>
      <td *ngIf="uploader.isHTML5" nowrap>{{ item?.file?.size/1024/1024 | number:'.2' }} MB</td>
      <td *ngIf="uploader.isHTML5">
        <div [ngStyle]="{ 'width': item.progress + '%' }"></div>
      </td>
      <td>
        <span *ngIf="item.isSuccess">OK</span>
        <span *ngIf="item.isCancel">Cancel</span>
        <span *ngIf="item.isError">Error</span>
      </td>
      <td>
        <button type="button" (click)="item.upload()" [disabled]="item.isReady || item.isUploading || item.isSuccess">
          Upload
        </button>
        <button type="button" (click)="item.cancel()" [disabled]="!item.isUploading">
          Cancel
        </button>
        <button type="button" (click)="item.remove()">
          Remove
        </button>
      </td>
    </tr>
    </tbody>
    </table>
    
    <div>
      Queue progress:
      <div [ngStyle]="{ 'width': uploader.progress + '%' }"></div>
      
      <button type="button" (click)="uploader.uploadAll()" [disabled]="!uploader.getNotUploadedItems().length">
        Upload all
      </button>
      <button type="button" (click)="uploader.cancelAll()" [disabled]="!uploader.isUploading">
        Cancel all
      </button>
            <button type="button" (click)="uploader.clearQueue()" [disabled]="!uploader.queue.length">
        Remove all
      </button>
    </div>
</div>
這個檔案增加了65行,稍微複雜了一點,不過提供了單檔案上傳,多檔案上傳,檔案佇列管理,上傳取消等很多功能,甚至還有根據檔案的不同狀態去設定按鈕的狀態。我覺得這裡面充分體現了Angular2元件模型的威力。其中最關鍵的程式碼是Multiple和Single下面的input標籤,將選擇的檔案跟uploader綁定了起來。之後就是利用uploader元件的功能了。

編譯打包

使用下面的命令就可以產生編譯打包好的前端:
ng build
這樣生成的是開發環境的包。引數--proc可以生成生產環境的包。執行該命令之後,會在uploader-client目錄下面生成一個dist目錄,裡面有index.html,以及inline.js,main.bundle.js和styles.bundle.js。這就是前端需要的所以檔案了。將這些生成的檔案(還有.map檔案幫助除錯)一起拷貝到下面要講到的uploader-server/src/main/resources/static下面。或者可以修改uploader-client/angular-cli.json,將dist改為該目錄的相對路徑。這樣ng build之後就不需要拷貝了。

建立Spring Boot專案

還是去http://start.spring.io/,建立一個專案模板,在Dependencies中增加Web和Configuration Processor。之後拷貝Spring官方的檔案上傳示例:https://spring.io/guides/gs/uploading-files/,將示例裡面所有java檔案拷貝過來。修改其中的一個檔案:
package com.shdanyan;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

import com.shdanyan.storage.StorageFileNotFoundException;
import com.shdanyan.storage.StorageService;

@Controller
public class FileUploadController {

    private final StorageService storageService;

    @Autowired
    public FileUploadController(StorageService storageService) {
        this.storageService = storageService;
    }

    /*
     * @GetMapping("/") public String listUploadedFiles(Model model) throws
     * IOException {
     * 
     * model.addAttribute("files", storageService .loadAll() .map(path ->
     * MvcUriComponentsBuilder .fromMethodName(FileUploadController.class,
     * "serveFile", path.getFileName().toString()) .build().toString())
     * .collect(Collectors.toList()));
     * 
     * return "uploadForm"; }
     */
    @GetMapping("/")
    public String index() {

        return "forward:index.html";
    }

    @GetMapping("/files/{filename:.+}")
    @ResponseBody
    public ResponseEntity<Resource> serveFile(@PathVariable String filename) {

        Resource file = storageService.loadAsResource(filename);
        return ResponseEntity
                .ok()
                .header(HttpHeaders.CONTENT_DISPOSITION,
                        "attachment; filename=\"" + file.getFilename() + "\"")
                .body(file);
    }

    @PostMapping("/")
    public String handleFileUpload(@RequestParam("file") MultipartFile file,
            RedirectAttributes redirectAttributes) {

        storageService.store(file);
        redirectAttributes.addFlashAttribute("message",
                "You successfully uploaded " + file.getOriginalFilename()
                        + "!");

        return "redirect:/";
    }

    @ExceptionHandler(StorageFileNotFoundException.class)
    public ResponseEntity<?> handleStorageFileNotFound(
            StorageFileNotFoundException exc) {
        return ResponseEntity.notFound().build();
    }

}
修改的頁面用綠底色顯示出來,即將原來的頁面替換成了我們的Angular2的頁面,總共是3行程式碼。這樣一個完整的上傳功能就實現了。上傳的關鍵程式碼是Controller裡面響應POST請求的方法handleFileUpload。用spring-boot:run就可以運行了,然後在瀏覽器中開啟連結http://localhost:8080就可以看到上傳的網頁了。

總結:

我們用100行不到的程式碼,就實現了功能完善的檔案上傳功能。完整的程式碼可以在這裡下載:https://github.com/cuiwader/file-uploader。那麼您是否認同Spring Boot和Angular2的強大功能呢?歡迎留言。另外裡面的一個技術細節,ng2-file-upload使用了HTML5的一些新特性,通過XMLHttpRequest(XHR)API完成了檔案上傳和進度查詢功能。從它原始碼裡面的這一行也可以看出,它似乎還能工作在不支援HTML5的瀏覽器中,我沒有測試過。
let transport = this.options.isHTML5 ? '_xhrTransport' : '_iframeTransport';
在上傳檔案的時候,可以看到HTTP請求包含了這個頭:Content-Type:multipart/form-data; boundary=----WebKitFormBoundaryQ8K9wTfL0BIB6BTG。後面的boundary是分隔。在其後的Request Payload裡面,可以看到該分隔被使用:
------WebKitFormBoundaryQ8K9wTfL0BIB6BTG
Content-Disposition: form-data; name="file"; filename="a.pptx"
Content-Type: application/vnd.openxmlformats-officedocument.presentationml.presentation


------WebKitFormBoundaryQ8K9wTfL0BIB6BTG--
第一個分隔Content-Type表明傳送的是一個office的PPT檔案。第二個分隔後面就是檔案的內容,但是沒有顯示出來。我猜測應該是用二進位制傳輸的,multipart/form-data是支援二進位制傳輸的,如果轉換為base64,則會有一個Content-Transfer-Encoding指明用了base64編碼,現在沒有指明。另外Content-Type實際上指明瞭這是一個二進位制檔案,所以是直接二進位制傳輸的。同時我們還可以看到,不同的分隔還有CR+LF。