文件上传可以作为一个独立的微服务。用Spring Boot和Angular2开发这样的服务非常有优势,可以用最少的代码,实现非常强的功能。如果比了解Spring Boot和Angular2的,请先看这几个文章:
用Spring Boot & Cloud,Angular2快速搭建微服务web应用 - 实现RESTful CRUD
用Spring Boot & Cloud,Angular2快速搭建微服务web应用 - AngularJS2客户端
用Spring Boot & Cloud,Angular2快速搭建微服务web应用 - 增加代理服务器
用Spring Boot & Cloud,Angular2快速搭建微服务web应用 - 增加权限控制
Angular2带来了全新的组件模型。如果你熟悉面向对象这个编程语言范式,就会发现这个新的组件模型和面向对象非常接近。我在学习到Angular2的组件模型的时候非常的激动,因为我觉得这是对前端开发模型的一个革命,就像C++语言之于汇编语言。它的好处显而易见,使用组件对代码基本上没有侵入性,容易写出高内聚松耦合的代码,等等。
言归正传,Angular2的文件上传组件我使用了这个:https://github.com/valor-software/ng2-file-upload/,然后简化了它的官方示例。下面是开发的步骤。
npm install -g angular-cli
ng new uploader-client
之后进入uploader-client目录,可以看到项目的相关文件都生成好了,我们可以直接开发业务代码了。具体请参考官网: https://github.com/angular/angular-cli
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 { }
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的实例。
{{title}}
Select files
Multiple
Single
Upload queue
Queue length: {{ uploader?.queue?.length }}
Name
Size
Progress
Status
Actions
{{ item?.file?.name }}
{{ item?.file?.size/1024/1024 | number:'.2' }} MB
OK
Cancel
Error
Queue progress:
这个文件增加了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之后就不需要拷贝了。
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 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就可以看到上传的网页了。
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。