第一部分: http://www.cnblogs.com/cgzl/p/8478993.html
第二部分: http://www.cnblogs.com/cgzl/p/8481825.html
第三部分: https://www.cnblogs.com/cgzl/p/8525541.html
第四部分: https://www.cnblogs.com/cgzl/p/8536350.html
這部分就講從angular5的客戶端上傳圖片到asp.net core 2.0的 web api.
這是需要的原始碼: https://pan.baidu.com/s/1Eqc4MRiQDwOHmu0OHyttqA
當前的效果如下:
點選這個超連結後:
好的, 下面開始編寫上傳相關的程式碼.
Asp.net core 2.0 檔案上傳
按照順序, 先建立Photo的domain model:
建立Models/Photo.cs:
using System.ComponentModel.DataAnnotations; namespace Tv.Models { public class Photo { public int Id { get; set; } [Required] [StringLength(255)] public string FileName { get; set; } } }
然後編輯TvShow.cs:
using System.Collections.Generic; using System.ComponentModel.DataAnnotations; namespace Tv.Models { public class TvShow { public TvShow() { Photoes = new List<Photo>(); } public int Id { get; set; } [Required] [StringLength(50)] public string Name { get; set; } public int TvNetworkId { get; set; } public TvNetwork TvNetwork { get; set; } public ICollection<Photo> Photoes { get; set; } } }
TvContext.cs:
using Microsoft.EntityFrameworkCore; using Tv.Models; namespace Tv.Database { public class TvContext : DbContext { public TvContext(DbContextOptions<TvContext> options) : base(options) { } public DbSet<TvNetwork> TvNetworks { get; set; } public DbSet<TvShow> TvShows { get; set; } public DbSet<Photo> Photoes { get; set; } } }
然後新增遷移和更新資料庫, 您應該知道怎麼做了, 這部分就略了.
新增PhotoViewModel.cs:
namespace Tv.ViewModels { public class PhotoViewModel { public int Id { get; set; } public string FileName { get; set; } } }
不要忘了做一下Mapping對映, 這裡我就不寫了.
然後建立PhotoesController.cs:
using System; using System.IO; using System.Threading.Tasks; using AutoMapper; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Tv.Database; using Tv.Models; using Tv.ViewModels; namespace Tv.Controllers { [Route("api/tvshows/{tvShowId}/photoes")] public class PhotoesController : Controller { private readonly IHostingEnvironment host; private readonly ITvRepository tvRepository; private readonly IUnitOfWork unitOfWork; private readonly IMapper mapper; public PhotoesController(IHostingEnvironment host, ITvRepository tvRepository, IUnitOfWork unitOfWork, IMapper mapper) { this.host = host; this.tvRepository = tvRepository; this.unitOfWork = unitOfWork; this.mapper = mapper; } [HttpPost] public async Task<IActionResult> Upload(int tvShowId, IFormFile file) { var tvShow = await tvRepository.GetTvShowByIdAsync(tvShowId, includeRelated: false); if (tvShow == null) { return NotFound(); } var uploadsFolderPath = Path.Combine(host.WebRootPath, "Uploads"); if (!Directory.Exists(uploadsFolderPath)) { Directory.CreateDirectory(uploadsFolderPath); } var fileName = Guid.NewGuid().ToString() + Path.GetExtension(file.FileName); var filePath = Path.Combine(uploadsFolderPath, fileName); using (var stream = new FileStream(filePath, FileMode.Create)) { await file.CopyToAsync(stream); } var photo = new Photo { FileName = fileName }; tvShow.Photoes.Add(photo); await unitOfWork.SaveAsync(); var result = mapper.Map<Photo, PhotoViewModel>(photo); return Ok(result); } } }
這裡要簡單講一下. asp.net core 上傳檔案的文件在這: https://docs.microsoft.com/en-us/aspnet/core/mvc/models/file-uploads
首先該controller的路由應該遵循web api的規範, 注意controller 的路由地址.
上傳單個檔案需要使用IFormFile作為Action的引數. 如果上傳的是多個檔案, 那麼應該使用IFormCollection.
這裡我做的是單檔案上傳, 所以使用IFormFile.
隨後使用注入的IHostingEnvironment獲得wwwroot目錄, 我想要把檔案上傳到wwwroot/uploads下, 判斷該目錄是否存在, 如果不存在則建立該目錄.
為了防黑, 把檔名改成Guid, 字尾名不變.
然後使用FileStream建立該檔案.
後邊的內容就是把檔名儲存到資料庫了.
接下來, 使用Postman來測試這個api.
開啟postman, 按照圖示輸入:
注意這裡的引數的key為file, 這個名字要與action的引數名一致:
send:
很好, 測試通過.
下面為Action新增一些驗證:
這就是一些常規的驗證, 沒有什麼特別的, 就不累述了.
針對這些東西, 您可以使用配置類, 並把相關的值放在appSettings.json裡面. 這部分您自己學一下吧 https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/?tabs=basicconfiguration.
下面是客戶端
Angular 5 檔案上傳
先做ui, tv-show-detail.component.html:
<form> <h2>基本資訊</h2> <div class="form-group row"> <label for="name" class="col-sm-2 col-form-label">名稱</label> <div class="col-sm-10"> <input type="text" readonly class="form-control-plaintext" id="name" value="{{model.name}}"> </div> </div> <h2>電視劇照片</h2> <div class="form-group row"> <label for="file" class="col-sm-2 col-form-label">照片</label> <input type="file" name="file" id="file" class="form-control" #fileInput (change)="upload()"> </div> </form>
注意這裡使用了template reference.
然後建立一個photo.service:
import { Injectable } from '@angular/core'; import { HttpHeaders, HttpClient } from '@angular/common/http'; @Injectable() export class PhotoService { constructor( private http: HttpClient ) { } upload(tvShowId: number, photo) { const formData = new FormData(); formData.append('file', photo); return this.http.post(`/api/tvshows/${tvShowId}/photoes`, formData); } }
其中post的引數型別是FormData, 它是js原生物件. formData裡面檔案的key要和後臺Action方法的引數名一樣.
最後改一下tv-show-detail.component.ts:
import { Component, OnInit, ElementRef, ViewChild } from '@angular/core'; import { TvShowService } from '../../services/tv-show.service'; import { Router, ActivatedRoute, ParamMap } from '@angular/router'; import { TvShow } from '../../models/tv-show'; import { Subscription } from 'rxjs/Subscription'; import { ToastrService } from 'ngx-toastr'; import { PhotoService } from '../../services/photo.service'; @Component({ selector: 'app-tv-show-detail', templateUrl: './tv-show-detail.component.html', styleUrls: ['./tv-show-detail.component.css'] }) export class TvShowDetailComponent implements OnInit { tvShowId: number; @ViewChild('fileInput') fileInput: ElementRef; model: TvShow = new TvShow(); busy: Subscription; constructor( private tvShowService: TvShowService, private router: Router, private route: ActivatedRoute, private toastr: ToastrService, private photoService: PhotoService ) { } ngOnInit() { this.route.paramMap.switchMap((params: ParamMap) => { this.tvShowId = +params.get('id'); return this.tvShowService.getById(this.tvShowId); }).subscribe(item => { this.model = item; }); } upload() { const ele = this.fileInput.nativeElement; this.photoService.upload(this.tvShowId, ele.files[0]).subscribe(x => { console.log(x); }); } }
如果上傳成功, 那麼回來先只做列印到log. 試一下:
上傳成功. 檔案即出現在wwwroot下, 檔名也儲存到了資料庫.
回顯照片:
首先修改Photo.cs:
using System.ComponentModel.DataAnnotations; namespace Tv.Models { public class Photo { public int Id { get; set; } [Required] [StringLength(255)] public string FileName { get; set; } public int TvShowId { get; set; } public TvShow TvShow { get; set; } } }
不要忘記遷移資料庫.
然後建立Repository, 並註冊:
using System.Collections.Generic; using System.Threading.Tasks; using Tv.Models; namespace Tv.Database { public interface IPhotoRepository { Task<List<Photo>> GetPhotoesByTvShowIdAsync(int tvShowId); } }
using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using Tv.Models; namespace Tv.Database { public class PhotoRepository : IPhotoRepository { private readonly TvContext context; public PhotoRepository(TvContext context) { this.context = context; } public async Task<List<Photo>> GetPhotoesByTvShowIdAsync(int tvShowId) { var photoes = await context.Photoes.Where(x => x.TvShowId == tvShowId).ToListAsync(); return photoes; } } }
最後修改PhotoesController:
using System; using System.IO; using System.Linq; using System.Threading.Tasks; using AutoMapper; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Tv.Database; using Tv.Models; using Tv.ViewModels; namespace Tv.Controllers { [Route("api/tvshows/{tvShowId}/photoes")] public class PhotoesController : Controller { private readonly IHostingEnvironment host; private readonly ITvRepository tvRepository; private readonly IUnitOfWork unitOfWork; private readonly IMapper mapper; private readonly IPhotoRepository photoRepository; public PhotoesController(IHostingEnvironment host, ITvRepository tvRepository, IUnitOfWork unitOfWork, IMapper mapper, IPhotoRepository photoRepository) { this.host = host; this.tvRepository = tvRepository; this.unitOfWork = unitOfWork; this.mapper = mapper; this.photoRepository = photoRepository; } [HttpPost] public async Task<IActionResult> Upload(int tvShowId, IFormFile file) { var tvShow = await tvRepository.GetTvShowByIdAsync(tvShowId, includeRelated: false); if (tvShow == null) { return NotFound(); } if (file == null) { return BadRequest("File is null"); } if (file.Length == 0) { return BadRequest("File is Empty"); } if (file.Length > 10 * 1024 * 1024) { return BadRequest("檔案大小不能超過10M"); } var acceptedTypes = new[] { ".jpg", ".png", ".jpeg" }; if (acceptedTypes.All(t => t != Path.GetExtension(file.FileName).ToLower())) { return BadRequest("檔案型別不對"); } var uploadsFolderPath = Path.Combine(host.WebRootPath, "Uploads"); if (!Directory.Exists(uploadsFolderPath)) { Directory.CreateDirectory(uploadsFolderPath); } var fileName = Guid.NewGuid().ToString() + Path.GetExtension(file.FileName); var filePath = Path.Combine(uploadsFolderPath, fileName); using (var stream = new FileStream(filePath, FileMode.Create)) { await file.CopyToAsync(stream); } var photo = new Photo { FileName = fileName }; tvShow.Photoes.Add(photo); await unitOfWork.SaveAsync(); var result = mapper.Map<Photo, PhotoViewModel>(photo); return Ok(result); } [HttpGet] public async Task<IActionResult> GetPhotoesByTvShowId(int tvShowId) { var photoes = await photoRepository.GetPhotoesByTvShowIdAsync(tvShowId); return Ok(photoes); } } }
然後修改angular部分:
新增Photo到model:
export class Photo {
id: number;
tvShowId: number;
fileName: string;
}
修改photo service:
import { Injectable } from '@angular/core'; import { HttpHeaders, HttpClient } from '@angular/common/http'; import { Observable } from 'rxjs/Observable'; import { Photo } from '../models/photo'; @Injectable() export class PhotoService { constructor( private http: HttpClient ) { } upload(tvShowId: number, photo): Observable<Photo> { const formData = new FormData(); formData.append('file', photo); return this.http.post<Photo>(`/api/tvshows/${tvShowId}/photoes`, formData); } getPhotoes(tvShowId: number): Observable<Photo[]> { return this.http.get<Photo[]>(`/api/tvshows/${tvShowId}/photoes`); } }
tv-show-detail.component.html:
<form> <h2>基本資訊</h2> <div class="form-group row"> <label for="name" class="col-sm-2 col-form-label">名稱</label> <div class="col-sm-10"> <input type="text" readonly class="form-control-plaintext" id="name" value="{{model.name}}"> </div> </div> <h2>電視劇照片</h2> <div class="form-group row"> <label for="file" class="col-sm-2 col-form-label">照片</label> <input type="file" name="file" id="file" class="form-control" #fileInput (change)="upload()"> </div> <div> <img [src]="'http://localhost:5000/Uploads/' + p.fileName" [alt]="p.fileName" *ngFor="let p of photoes" class="m-1" width="200" height="200" /> </div> </form>
tv-show-detail.component.ts:
import { Component, OnInit, ElementRef, ViewChild } from '@angular/core'; import { TvShowService } from '../../services/tv-show.service'; import { Router, ActivatedRoute, ParamMap } from '@angular/router'; import { TvShow } from '../../models/tv-show'; import { Subscription } from 'rxjs/Subscription'; import { ToastrService } from 'ngx-toastr'; import { PhotoService } from '../../services/photo.service'; import { Photo } from '../../models/photo'; import { Observable } from 'rxjs/Observable'; import 'rxjs/add/observable/forkJoin'; @Component({ selector: 'app-tv-show-detail', templateUrl: './tv-show-detail.component.html', styleUrls: ['./tv-show-detail.component.css'] }) export class TvShowDetailComponent implements OnInit { tvShowId: number; @ViewChild('fileInput') fileInput: ElementRef; model: TvShow = new TvShow(); busy: Subscription; photoes: Photo[] = []; constructor( private tvShowService: TvShowService, private router: Router, private route: ActivatedRoute, private toastr: ToastrService, private photoService: PhotoService ) { } ngOnInit() { this.route.paramMap.switchMap((params: ParamMap) => { this.tvShowId = +params.get('id'); return Observable.forkJoin<TvShow, Photo[]>( this.tvShowService.getById(this.tvShowId), this.photoService.getPhotoes(this.tvShowId) ); }).subscribe(([tvShow, photoes]) => { this.model = tvShow; this.photoes = photoes; }); } upload() { const ele = this.fileInput.nativeElement; this.photoService.upload(this.tvShowId, ele.files[0]).subscribe(photo => { this.photoes.push(photo); }); } }
這部分比較簡單, 注意同時傳送多個請求可以使用forkJoin.
看看效果:
如果照片沒有顯示出來, 可能是asp.net core沒有啟用靜態檔案到支援, 在Startup.cs新增這句話即可:
using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using AutoMapper; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Tv.Database; namespace Tv { public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; } public IConfiguration Configuration { get; } // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { services.AddAutoMapper(); // services.AddDbContext<TvContext>(opt => opt.UseSqlServer(Configuration["ConnectionStrings:Default"])); services.AddDbContext<TvContext>(opt => opt.UseSqlServer(Configuration.GetConnectionString("Default"))); services.AddScoped<ITvRepository, TvRepository>(); services.AddScoped<IPhotoRepository, PhotoRepository>(); services.AddScoped<IUnitOfWork, UnitOfWork>(); services.AddMvc(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseStaticFiles(); app.UseMvc(); } } }
很好. 即使是剛新增完到照片也會即時顯示出來.
上傳進度顯示.
首先建立一個修改photo service:
根據官方文件, 如果想要上傳檔案時顯示進度, 那麼應該使用HttpRequest, 並設定屬性reportProgress為true:
import { Injectable } from '@angular/core'; import { HttpHeaders, HttpClient, HttpRequest, HttpEvent, HttpEventType, HttpErrorResponse } from '@angular/common/http'; import { Observable } from 'rxjs/Observable'; import { Photo } from '../models/photo'; @Injectable() export class PhotoService { constructor( private http: HttpClient ) { } upload(tvShowId: number, photo: File) { const formData = new FormData(); formData.append('file', photo); // return this.http.post<Photo>(`/api/tvshows/${tvShowId}/photoes`, formData); const req = new HttpRequest('POST', `/api/tvshows/${tvShowId}/photoes`, formData, { reportProgress: true }); return this.http.request<Photo>(req); } getPhotoes(tvShowId: number): Observable<Photo[]> { return this.http.get<Photo[]>(`/api/tvshows/${tvShowId}/photoes`); } }
回到 tv-show-detail.component.ts:
import { Component, OnInit, ElementRef, ViewChild } from '@angular/core'; import { TvShowService } from '../../services/tv-show.service'; import { Router, ActivatedRoute, ParamMap } from '@angular/router'; import { TvShow } from '../../models/tv-show'; import { Subscription } from 'rxjs/Subscription'; import { ToastrService } from 'ngx-toastr'; import { PhotoService } from '../../services/photo.service'; import { Photo } from '../../models/photo'; import { Observable } from 'rxjs/Observable'; import 'rxjs/add/observable/forkJoin'; import { HttpEvent, HttpEventType } from '@angular/common/http'; import { HttpResponse } from 'selenium-webdriver/http'; @Component({ selector: 'app-tv-show-detail', templateUrl: './tv-show-detail.component.html', styleUrls: ['./tv-show-detail.component.css'] }) export class TvShowDetailComponent implements OnInit { tvShowId: number; @ViewChild('fileInput') fileInput: ElementRef; model: TvShow = new TvShow(); busy: Subscription; photoes: Photo[] = []; constructor( private tvShowService: TvShowService, private router: Router, private route: ActivatedRoute, private toastr: ToastrService, private photoService: PhotoService ) { } ngOnInit() { this.route.paramMap.switchMap((params: ParamMap) => { this.tvShowId = +params.get('id'); return Observable.forkJoin<TvShow, Photo[]>( this.tvShowService.getById(this.tvShowId), this.photoService.getPhotoes(this.tvShowId) ); }).subscribe(([tvShow, photoes]) => { this.model = tvShow; this.photoes = photoes; }); } upload() { const ele = this.fileInput.nativeElement; const file = ele.files[0]; this.photoService.upload(this.tvShowId, file).subscribe((event: HttpEvent<any>) => { switch (event.type) { case HttpEventType.Sent: console.log(`開始上傳 "${file.name}", 大小是: ${file.size}.`); break; case HttpEventType.UploadProgress: const percentDone = Math.round(100 * event.loaded / event.total); console.log(`檔案 "${file.name}" 的上傳進度是 ${percentDone}%.`); break; case HttpEventType.Response: console.log(`檔案 "${file.name}" 上傳成功!`); this.toastr.success(`檔案 "${file.name}" 上傳成功!`); this.photoes.push(<Photo>(event.body)); break; default: console.log(`檔案 "${file.name}" 的事件型別: ${event.type}.`); break; } }); } }
這樣, 上傳檔案時, 每個進度都會返回一個event, 我暫時就先把它列印到控制檯.
看一下效果:
好的, 檔案太小, 本地到速度又太快, 進度直接100%了.
那我改一下Chrome的設定, 開啟Developer Tools的Network 選項, 然後點選這裡:
然後新增:
新增一個非常慢的網速限制:
最後選取這個限制:
實際上, 選擇Slow 3G就很慢了.
這時, 再上傳一次試試效果:
很好, 沒問題.
接下來就是UI顯示進度條的問題了, 很簡單:
開啟html:
<form> <h2>基本資訊</h2> <div class="form-group row"> <label for="name" class="col-sm-2 col-form-label">名稱</label> <div class="col-sm-10"> <input type="text" readonly class="form-control-plaintext" id="name" value="{{model.name}}"> </div> </div> <h2>電視劇照片</h2> <div class="form-group row"> <label for="file" class="col-sm-2 col-form-label">照片</label> <input type="file" name="file" id="file" class="form-control" #fileInput (change)="upload()"> </div> <div class="progress" *ngIf="progress"> <div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" [style.width]="progress"></div> </div> <div> <img [src]="'http://localhost:5000/Uploads/' + p.fileName" [alt]="p.fileName" *ngFor="let p of photoes" class="m-1" width="200" height="200" /> </div> </form>
開啟tv-show-detail.component.ts:
import { Component, OnInit, ElementRef, ViewChild } from '@angular/core'; import { TvShowService } from '../../services/tv-show.service'; import { Router, ActivatedRoute, ParamMap } from '@angular/router'; import { TvShow } from '../../models/tv-show'; import { Subscription } from 'rxjs/Subscription'; import { ToastrService } from 'ngx-toastr'; import { PhotoService } from '../../services/photo.service'; import { Photo } from '../../models/photo'; import { Observable } from 'rxjs/Observable'; import 'rxjs/add/observable/forkJoin'; import { HttpEvent, HttpEventType } from '@angular/common/http'; import { HttpResponse } from 'selenium-webdriver/http'; @Component({ selector: 'app-tv-show-detail', templateUrl: './tv-show-detail.component.html', styleUrls: ['./tv-show-detail.component.css'] }) export class TvShowDetailComponent implements OnInit { tvShowId: number; @ViewChild('fileInput') fileInput: ElementRef; model: TvShow = new TvShow(); busy: Subscription; photoes: Photo[] = []; progress: string; constructor( private tvShowService: TvShowService, private router: Router, private route: ActivatedRoute, private toastr: ToastrService, private photoService: PhotoService ) { } ngOnInit() { this.route.paramMap.switchMap((params: ParamMap) => { this.tvShowId = +params.get('id'); return Observable.forkJoin<TvShow, Photo[]>( this.tvShowService.getById(this.tvShowId), this.photoService.getPhotoes(this.tvShowId) ); }).subscribe(([tvShow, photoes]) => { this.model = tvShow; this.photoes = photoes; }); } upload() { const ele = this.fileInput.nativeElement; const file = ele.files[0];
ele.value = ''; // 上傳圖片後,把input的值清空. this.photoService.upload(this.tvShowId, file).subscribe((event: HttpEvent<any>) => { switch (event.type) { case HttpEventType.Sent: console.log(`開始上傳 "${file.name}", 大小是: ${file.size}.`); break; case HttpEventType.UploadProgress: const percentDone = Math.round(100 * event.loaded / event.total); this.progress = `${percentDone}%`; console.log(`檔案 "${file.name}" 的上傳進度是 ${percentDone}%.`); break; case HttpEventType.Response: console.log(`檔案 "${file.name}" 上傳成功!`); this.toastr.success(`檔案 "${file.name}" 上傳成功!`); this.photoes.push(<Photo>(event.body)); this.progress = null; break; default: console.log(`檔案 "${file.name}" 的事件型別: ${event.type}.`); break; } }); } }
試試效果:
OK, 沒問題!
今天就寫到這吧.