最近迷恋上了登山,游山玩水,我当时给同事们算了一下,一个夏季从5月开始到8月底,总共四个月,一个月休8天,总共是可以休32天,刨去下雨有事不能出去玩,其实一个夏季能出去玩的时间也就是20天左右,好好珍惜,现在年纪大了,每一天都很宝贵。
好了,废话不多说,要实现大文件上传,首先要考虑不要超时,上传的时候给用户展示进度,上传中途突然断网,需要支持断点续传,不至于500M的文件上传了499M了,结果最后一M失败了,前功尽弃。
OK,今天就给大家看一下在Angular中如何实现这个上传功能,首先我们选择Kendo UI for angular。组件采用Kendo Upload,请参考:https://www.telerik.com/kendo-angular-ui/components/uploads/upload/chunk-upload/
首先我们看一下angular端,在app.module中引入组件,整个代码如下
import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { AppComponent } from './app.component'; import { AWSUploadService } from '../services/aws-upload-service'; import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; import { InputsModule } from '@progress/kendo-angular-inputs'; import { UploadsModule } from "@progress/kendo-angular-upload"; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { UploadInterceptor } from './app.component'; @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, HttpClientModule, InputsModule, UploadsModule, BrowserAnimationsModule ], providers: [AWSUploadService, { provide: HTTP_INTERCEPTORS, useClass: UploadInterceptor, multi: true, }], bootstrap: [AppComponent] }) export class AppModule { }
我们需要引入UploadsModule,UploadInterceptor,并提供拦截器UploadInterceptor。
接下来我们看一下UI,UI上我们只放一个Upload组件实现上传。
<div class="content" role="main"> <kendo-upload [saveUrl]="uploadSaveUrl" [removeUrl]="uploadRemoveUrl" [withCredentials]="false" [concurrent]="false" [chunkable]="chunkSettings"> </kendo-upload> </div>
这里我们需要设置几个属性,一个是SaveUrl,就是接收上传文件的api地址。withCredentials设置为false,是为了跨域的时候不验证凭证。concurrent为false意思是当上传多个文件的时候,是否是并发上传。chunkable设置的是分片上传时的一些参数,如下
OK,我们看一下js逻辑
import { Component, Injectable } from '@angular/core'; import { AWSUploadService } from '../services/aws-upload-service'; import { HttpEventType, HttpClient, HttpParams } from '@angular/common/http'; import { ChunkSettings } from '@progress/kendo-angular-upload'; import { SelectEvent, RemoveEvent, FileRestrictions, } from "@progress/kendo-angular-upload"; import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpProgressEvent, HttpResponse, } from "@angular/common/http"; import { Observable, of, concat } from "rxjs"; import { delay } from "rxjs/operators"; import { ChunkMetadata } from "@progress/kendo-angular-upload"; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent { title = 'aws-upload-app'; constructor(private awsUploadService: AWSUploadService , private http: HttpClient) { } public chunkSettings: ChunkSettings = { size: 1024000, resumable: true, maxAutoRetries: 2 }; ngOnInit() { } uploadSaveUrl = "http://localhost:60923/api/Upload"; uploadRemoveUrl = "removeUrl"; } @Injectable() export class UploadInterceptor implements HttpInterceptor { public fileMap: any = []; intercept( req: HttpRequest<any>, next: HttpHandler ): Observable<HttpEvent<any>> { if (req.url === "saveUrl") { // Parsing the file metadata. const metadata: ChunkMetadata = JSON.parse(req.body.get("metadata")); // Retrieving the uid of the uploaded file. const fileUid = metadata.fileUid; if (metadata.chunkIndex === 0) { this.fileMap[fileUid] = []; } // Storing the chunks of the file. this.fileMap[fileUid].push(req.body.get("files")); // Checking if this is the last chunk of the file if (metadata.chunkIndex === metadata.totalChunks - 1) { // Reconstructing the initial file // const completeFile = new Blob( // this.fileMap[metadata.fileUid], // { type: metadata.contentType } // ); } const events: Observable<HttpEvent<any>>[] = [30, 60, 100].map((x) => of(<HttpProgressEvent>{ type: HttpEventType.UploadProgress, loaded: x, total: 100, }) ); const success = of(new HttpResponse({ status: 200 })).pipe(delay(1000)); events.push(success); return concat(...events); } if (req.url === "removeUrl") { return of(new HttpResponse({ status: 200 })); } return next.handle(req); } }
前端这里其实官方demo已经讲得很清楚了,我就不再赘述。我们看一下官方没有的后端api是如何实现的。
[HttpPost] [Route("upload/chunk")] public HttpResponseMessage UploadByChunk() { try { if (HttpContext.Current.Request.Files.AllKeys.Length > 0) { var metaData = HttpContext.Current.Request.Form["metadata"]; var fileMetaData = JsonConvert.DeserializeObject<FileMetaData>(metaData); var httpPostedChunkFile = HttpContext.Current.Request.Files[0]; if (httpPostedChunkFile != null) { var saveFile = HttpContext.Current.Server.MapPath("~/uploaded"); var saveFilePath = Path.Combine(saveFile, fileMetaData.FileUid + ".part"); var chunkIndex = fileMetaData.ChunkIndex; if (chunkIndex == 0) { if (File.Exists(saveFilePath)) { File.Delete(saveFilePath); } httpPostedChunkFile.SaveAs(saveFilePath); } else { MergeChunkFile(saveFilePath, httpPostedChunkFile.InputStream); var totalChunk = fileMetaData.TotalChunks; if (Convert.ToInt32(chunkIndex) == (Convert.ToInt32(totalChunk) - 1)) { var savedFile = HttpContext.Current.Server.MapPath("~/uploaded"); var originalFilePath = Path.Combine(savedFile, fileMetaData.FileName); if (File.Exists(originalFilePath)) { File.Delete(originalFilePath); } File.Move(saveFilePath, originalFilePath); } } } } return Request.CreateResponse(HttpStatusCode.OK); } catch (Exception e) { return Request.CreateResponse(HttpStatusCode.InternalServerError); } } public void MergeChunkFile(string fullPath, Stream chunkContent) { try { using (FileStream stream = new FileStream(fullPath, FileMode.Append, FileAccess.Write, FileShare.ReadWrite)) { using (chunkContent) { chunkContent.CopyTo(stream); } } } catch (IOException ex) { throw ex; } }
大家可以看到我们首先获取到metadata,因为文件被切分后,不是一个完整的文件,所以每一个分片文件发送到服务端都有一个元数据,这个元数据信息是放在form中的,所以每次我们拿到后,先进行反序列化拿到元数据信息。那么元数据将到底有哪些信息呢,看官网说明,有如下一些元数据信息,英文已经写得很明白了。
所以结合上面所述,整个流程是这样的,客户端将文件进行切割,比如一个文件500M,根据chunkSettings配置,比如切片大小为10M,那么这个文件就会被分成50个小块,然后一块块的顺序往api发送。发送的时候怎么保证文件的片能够在服务端顺序拼起来,其实这里就要用到元数据。元数据其实是一个json字符串,包含了文件名,文件大小,文件唯一标识uid,分片索引,总分片数。有了这些元数据,我们就能判断当前数据片是什么位置的。当第一个分片到达api的时候,我们在服务端先存一个临时文件uid.part。当后面的分片到达时,只是把文件流追加到临时文件中。当最后一片文件内容到达时,我们对文件进行重命名,完成文件的最终合并。
好了,整个流程就是这么简单。我们看一下效果吗,选择文件上传,会显示进度,以及暂停按钮,暂停上传后,可以重新resume继续上传,对于客户端,肯定有记录文件分片上传到哪个位置,否则无法继续上传。这里请大家参考UploadInterceptor代码。
在文件还没有彻底上传到服务器之前,会在服务端产生临时文件,如下
ok,最后再强调一下,上传的大小限制我们要改一下,一个是system.web下
<httpRuntime targetFramework="4.5.2" maxRequestLength="102400000" />
另外一个节点是system.webserver下面
<security> <requestFiltering> <requestLimits maxAllowedContentLength="2147483648" /> </requestFiltering> </security>
设置这两个是为了我们运行时和部署IIS的时候上传文件的大小不要受太大限制。最后,如果要支持跨域,安装Microsoft.Asp.Net.WebApi.Cors,在api config中设置
config.EnableCors(new EnableCorsAttribute("*", "*", "*"));
上一篇 React 实战(二)