這是一篇學習筆記. angular 5 正式版都快出了, 不過主要是效能升級.
我認為angular 4還是很適合企業的, 就像.net一樣.
我用的是windows 10
安裝工具:
git for windows: 官網很慢, 所以找一個映象站下載: https://github.com/waylau/git-for-win, 淘寶映象的速度還是蠻快的:
安裝的時候, 建議選擇這個, 會新增很多命令列工具:
nodejs: 去官網下載就行: https://nodejs.org/en/
正常安裝即可. npm的版本不要低於5.0吧:
angular-cli, 官網: https://github.com/angular/angular-cli
npm install -g @angular/cli
visual studio code: https://code.visualstudio.com/
and visual studio 2017 of course.
建立angular專案
進入命令列在某個地方執行命令:
ng new client-panel
這就會建立一個client-panel資料夾, 裡面是該專案的檔案, 然後它會立即執行npm install命令(這裡不要使用淘寶的cnpm進行安裝, 有bug), 稍等一會就會結束.
使用vscode開啟該目錄, 然後在vscode裡面開啟terminal:
terminal預設的可能是powershell, 如果你感覺powershell有點慢的話, 可以換成bash(安裝git時候帶的)或者windows command line等.
第一次開啟terminal的時候, vscode上方會提示你配置terminal, 這時就可以更換預設的terminal. 否則的話, 你可以點選選單file-reference-settings, 自己選擇一個terminal應用:
同樣可以安裝幾個vscode的外掛:
然後試執行一下專案, 在terminal執行 ng serve, 如果沒問題的話, 大概是這樣:
瀏覽器執行: http://localhost:4200
安裝bootstrap4等:
安裝bootstrap4, tether, jquery等:
npm install bootstrap@4.0.0-beta.2 tether jquery --save
安裝成功後, 開啟 .angular-cli.json, 把相關的css和js新增進去:
然後在執行試試 ng serve, 重新整理:
字型已經改變, bootstrap起作用了.
建立Components
建立dashboard:
terminal執行
ng g component components/dashboard
執行成功後會生成4個檔案:
並且會自動在app.module.ts裡面宣告:
建立其他 components:
ng g component components/clients ng g component components/clientDetails ng g component components/addClient ng g component components/editClient ng g component components/navbar ng g component components/sidebar
ng g component components/login
ng g component components/register
ng g component components/settings
ng g component components/pageNotFound
建立Route路由
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { AppComponent } from './app.component'; import { DashboardComponent } from './components/dashboard/dashboard.component'; import { ClientsComponent } from './components/clients/clients.component'; import { ClientDetailsComponent } from './components/client-details/client-details.component'; import { AddClientComponent } from './components/add-client/add-client.component'; import { EditClientComponent } from './components/edit-client/edit-client.component'; import { NavbarComponent } from './components/navbar/navbar.component'; import { SidebarComponent } from './components/sidebar/sidebar.component'; import { LoginComponent } from './components/login/login.component'; import { RegisterComponent } from './components/register/register.component'; import { SettingsComponent } from './components/settings/settings.component'; import { PageNotFoundComponent } from './components/page-not-found/page-not-found.component'; const appRoutes: Routes = [ { path: '', component: DashboardComponent }, { path: 'register', component: RegisterComponent }, { path: 'login', component: LoginComponent } ]; @NgModule({ declarations: [ AppComponent, DashboardComponent, ClientsComponent, ClientDetailsComponent, AddClientComponent, EditClientComponent, NavbarComponent, SidebarComponent, LoginComponent, RegisterComponent, SettingsComponent, PageNotFoundComponent ], imports: [ BrowserModule, RouterModule.forRoot(appRoutes) ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }
新增router-outlet:
開啟app.component.html, 清空內容, 新增一個div(可以輸入div.container然後按tab健):
<div class="container"> <router-outlet></router-outlet> </div>
現在重新整理瀏覽器, 大約這樣:
新增navbar:
修改navbar.component.html:
<nav class="navbar navbar-expand-md navbar-light bg-light"> <div class="container"> <a class="navbar-brand" href="#">Client Panel</a> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarsExampleDefault" aria-controls="navbarsExampleDefault" aria-expanded="false" aria-label="Toggle navigation"> <span class="navbar-toggler-icon"></span> </button> <div class="collapse navbar-collapse" id="navbarsExampleDefault"> <ul class="navbar-nav mr-auto"> <li class="nav-item"> <a class="nav-link" href="#" routerLink="/">Dashboard </a> </li> </ul> <ul class="navbar-nav ml-auto"> <li class="nav-item"> <a class="nav-link" href="#" routerLink="/register">Register </a> </li> <li class="nav-item"> <a class="nav-link" href="#" routerLink="/login">Login </a> </li> </ul> </div> </div> </nav>
修改app.component.html:
<app-navbar></app-navbar> <div class="container"> <router-outlet></router-outlet> </div>
執行:
建立Service
建立一個client.service:
ng g service services/client
然後在app.module.ts新增引用:
// Services Imports import { ClientService } from "./services/client.service";
並新增在providers裡:
providers: [
ClientService
],
前端先暫時到這, 現在開始搞後端 web api.
建立asp.net core 2.0 的 Web api專案
web api專案原始碼: https://github.com/solenovex/asp.net-core-2.0-web-api-boilerplate
專案列表如圖:
AspNetIdentityAuthorizationServer是一個單獨的authorization server, 這裡暫時還沒用到, 它的埠是5000, 預設不啟動.
CoreApi.Infrastructure 裡面有一些基類和介面, 還放了一個公共的工具類等.
CoreApi.Models就是 models/entities
CoreApi.DataContext 裡面就是DbContext相關的
CoreApi.Repositories 裡面是Repositories
CoreApi.Services 裡面就是各種services
CoreApi.ViewModels 裡面就是各種ViewModels或者叫Dtos
CoreApi.Web是web啟動專案.
SharedSettings是橫跨authorization server和 web api的一些公共設定.
上面說的這些都沒什麼用, 下面開始建立Client的api.
建立Client Model(或者叫Entity)
在CoreApi.Models建立資料夾Angular, 然後建立Client.cs:
using CoreApi.Infrastructure.Features.Common; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; namespace CoreApi.Models.Angular { public class Client : EntityBase { public decimal Balance { get; set; } public string Email { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public string Phone { get; set; } } public class ClientConfiguration : EntityBaseConfiguration<Client> { public override void ConfigureDerived(EntityTypeBuilder<Client> builder) { builder.Property(x => x.Balance).HasColumnType("decimal(18,2)"); builder.Property(x => x.Email).IsRequired().HasMaxLength(100); builder.Property(x => x.FirstName).IsRequired().HasMaxLength(50); builder.Property(x => x.LastName).IsRequired().HasMaxLength(50); builder.Property(x => x.Phone).HasMaxLength(50); } } }
其中父類EntityBase裡面含有一些通用屬性,Id, CreateUser, UpdateUser, CreateTime, UpdateTime, LastAction, 這些是我公司做專案必須的, 你們隨意.
下面ClientConfiguration是針對Client的fluent api配置類. 他的父類EntityBaseConfiguration實現了EF的IEntityTypeConfiguration介面, 並在父類裡面針對EntityBase那些屬性使用fluent api做了限制:
namespace CoreApi.Infrastructure.Features.Common { public abstract class EntityBaseConfiguration<T> : IEntityTypeConfiguration<T> where T : EntityBase { public virtual void Configure(EntityTypeBuilder<T> builder) { builder.HasKey(e => e.Id); builder.Property(x => x.CreateTime).IsRequired(); builder.Property(x => x.UpdateTime).IsRequired(); builder.Property(x => x.CreateUser).IsRequired().HasMaxLength(50); builder.Property(x => x.UpdateUser).IsRequired().HasMaxLength(50); builder.Property(x => x.LastAction).IsRequired().HasMaxLength(50); ConfigureDerived(builder); } public abstract void ConfigureDerived(EntityTypeBuilder<T> b); } }
弄完Model和它的配置之後, 就新增到DbContext裡面. 開啟CoreApi.DataContext的CoreContext, 新增Model和配置:
protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.HasDefaultSchema(AppSettings.DefaultSchema); modelBuilder.ApplyConfiguration(new UploadedFileConfiguration()); modelBuilder.ApplyConfiguration(new ClientConfiguration()); }
public DbSet<UploadedFile> UploadedFiles { get; set; } public DbSet<Client> Clients { get; set; }
然後建立ClientRepository
在CoreApi.Repositories裡面建立Angular目錄, 建立ClientRepository.cs:
namespace CoreApi.Repositories.Angular { public interface IClientRepository : IEntityBaseRepository<Client> { } public class ClientRepository : EntityBaseRepository<Client>, IClientRepository { public ClientRepository(IUnitOfWork unitOfWork) : base(unitOfWork) { } } }
圖省事, 我把repository和它的interface放在一個檔案了.
IEntityBaseRepository<T>定義了一些常用的方法:
namespace CoreApi.DataContext.Infrastructure { public interface IEntityBaseRepository<T> where T : class, IEntityBase, new() { IQueryable<T> All { get; } IQueryable<T> AllIncluding(params Expression<Func<T, object>>[] includeProperties); int Count(); Task<int> CountAsync(); T GetSingle(int id); Task<T> GetSingleAsync(int id); T GetSingle(Expression<Func<T, bool>> predicate); Task<T> GetSingleAsync(Expression<Func<T, bool>> predicate); T GetSingle(Expression<Func<T, bool>> predicate, params Expression<Func<T, object>>[] includeProperties); Task<T> GetSingleAsync(Expression<Func<T, bool>> predicate, params Expression<Func<T, object>>[] includeProperties); IQueryable<T> FindBy(Expression<Func<T, bool>> predicate); void Add(T entity); void Update(T entity); void Delete(T entity); void DeleteWhere(Expression<Func<T, bool>> predicate); void AddRange(IEnumerable<T> entities); void DeleteRange(IEnumerable<T> entities); void Attach(T entity); void AttachRange(IEnumerable<T> entities); void Detach(T entity); void DetachRange(IEnumerable<T> entities); void AttachAsModified(T entity); } }
EntityBaseRepository<T>是它的實現:
namespace CoreApi.DataContext.Infrastructure { public class EntityBaseRepository<T> : IEntityBaseRepository<T> where T : class, IEntityBase, new() { #region Properties protected CoreContext Context { get; } public EntityBaseRepository(IUnitOfWork unitOfWork) { Context = unitOfWork as CoreContext; } #endregion public virtual IQueryable<T> All => Context.Set<T>(); public virtual IQueryable<T> AllIncluding(params Expression<Func<T, object>>[] includeProperties) { IQueryable<T> query = Context.Set<T>(); foreach (var includeProperty in includeProperties) { query = query.Include(includeProperty); } return query; } public virtual int Count() { return Context.Set<T>().Count(); } public async Task<int> CountAsync() { return await Context.Set<T>().CountAsync(); } public T GetSingle(int id) { return Context.Set<T>().FirstOrDefault(x => x.Id == id); } public async Task<T> GetSingleAsync(int id) { return await Context.Set<T>().FirstOrDefaultAsync(x => x.Id == id); } public T GetSingle(Expression<Func<T, bool>> predicate) { return Context.Set<T>().FirstOrDefault(predicate); } public async Task<T> GetSingleAsync(Expression<Func<T, bool>> predicate) { return await Context.Set<T>().FirstOrDefaultAsync(predicate); } public T GetSingle(Expression<Func<T, bool>> predicate, params Expression<Func<T, object>>[] includeProperties) { IQueryable<T> query = Context.Set<T>(); foreach (var includeProperty in includeProperties) { query = query.Include(includeProperty); } return query.Where(predicate).FirstOrDefault(); } public async Task<T> GetSingleAsync(Expression<Func<T, bool>> predicate, params Expression<Func<T, object>>[] includeProperties) { IQueryable<T> query = Context.Set<T>(); foreach (var includeProperty in includeProperties) { query = query.Include(includeProperty); } return await query.Where(predicate).FirstOrDefaultAsync(); } public virtual IQueryable<T> FindBy(Expression<Func<T, bool>> predicate) { return Context.Set<T>().Where(predicate); } public virtual void Add(T entity) { Context.Set<T>().Add(entity); } public virtual void Update(T entity) { EntityEntry<T> dbEntityEntry = Context.Entry(entity); dbEntityEntry.State = EntityState.Modified; dbEntityEntry.Property(x => x.Id).IsModified = false; dbEntityEntry.Property(x => x.CreateUser).IsModified = false; dbEntityEntry.Property(x => x.CreateTime).IsModified = false; } public virtual void Delete(T entity) { Context.Set<T>().Remove(entity); } public virtual void AddRange(IEnumerable<T> entities) { Context.Set<T>().AddRange(entities); } public virtual void DeleteRange(IEnumerable<T> entities) { foreach (var entity in entities) { Context.Set<T>().Remove(entity); } } public virtual void DeleteWhere(Expression<Func<T, bool>> predicate) { IEnumerable<T> entities = Context.Set<T>().Where(predicate); foreach (var entity in entities) { Context.Entry<T>(entity).State = EntityState.Deleted; } } public void Attach(T entity) { Context.Set<T>().Attach(entity); } public void AttachRange(IEnumerable<T> entities) { foreach (var entity in entities) { Attach(entity); } } public void Detach(T entity) { Context.Entry<T>(entity).State = EntityState.Detached; } public void DetachRange(IEnumerable<T> entities) { foreach (var entity in entities) { Detach(entity); } } public void AttachAsModified(T entity) { Attach(entity); Update(entity); } } }
建立Client的ViewModels
在CoreApi.ViewModels建立Angular資料夾, 分別針對查詢, 新增, 修改建立3個ViewModel(Dto):
ClientViewModel:
namespace CoreApi.ViewModels.Angular { public class ClientViewModel : EntityBase { public decimal Balance { get; set; } public string Email { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public string Phone { get; set; } } }
ClientCreationViewModel:
namespace CoreApi.ViewModels.Angular { public class ClientCreationViewModel { public decimal Balance { get; set; } [Required] [MaxLength(100)] public string Email { get; set; } [Required] [MaxLength(50)] public string FirstName { get; set; } [Required] [MaxLength(50)] public string LastName { get; set; } [Required] [MaxLength(50)] public string Phone { get; set; } } }
ClientModificationViewModel:
namespace CoreApi.ViewModels.Angular { public class ClientModificationViewModel { public decimal Balance { get; set; } [Required] [MaxLength(100)] public string Email { get; set; } [Required] [MaxLength(50)] public string FirstName { get; set; } [Required] [MaxLength(50)] public string LastName { get; set; } [Required] [MaxLength(50)] public string Phone { get; set; } } }
配置AutoMapper
針對Client和它的Viewmodels, 分別從兩個方向進行配置:
DomainToViewModelMappingProfile:
namespace CoreApi.Web.MyConfigurations { public class DomainToViewModelMappingProfile : Profile { public override string ProfileName => "DomainToViewModelMappings"; public DomainToViewModelMappingProfile() { CreateMap<UploadedFile, UploadedFileViewModel>(); CreateMap<Client, ClientViewModel>(); CreateMap<Client, ClientModificationViewModel>(); } } }
ViewModelToDomainMappingProfile:
namespace CoreApi.Web.MyConfigurations { public class ViewModelToDomainMappingProfile : Profile { public override string ProfileName => "ViewModelToDomainMappings"; public ViewModelToDomainMappingProfile() { CreateMap<UploadedFileViewModel, UploadedFile>(); CreateMap<ClientViewModel, Client>(); CreateMap<ClientCreationViewModel, Client>(); CreateMap<ClientModificationViewModel, Client>(); } } }
註冊Repository的DI:
在web專案的StartUp.cs的ConfigureServices裡面為ClientRepository註冊DI:
services.AddScoped<IClientRepository, ClientRepository>();
建立Controller
在controllers目錄建立Angular/ClientController.cs:
namespace CoreApi.Web.Controllers.Angular { [Route("api/[controller]")] public class ClientController : BaseController<ClientController> { private readonly IClientRepository _clientRepository; public ClientController(ICoreService<ClientController> coreService, IClientRepository clientRepository) : base(coreService) { _clientRepository = clientRepository; } [HttpGet] public async Task<IActionResult> GetAll() { var items = await _clientRepository.All.ToListAsync(); var results = Mapper.Map<IEnumerable<ClientViewModel>>(items); return Ok(results); } [HttpGet] [Route("{id}", Name = "GetClient")] public async Task<IActionResult> Get(int id) { var item = await _clientRepository.GetSingleAsync(id); if (item == null) { return NotFound(); } var result = Mapper.Map<ClientViewModel>(item); return Ok(result); } [HttpPost] public async Task<IActionResult> Post([FromBody] ClientCreationViewModel clientVm) { if (clientVm == null) { return BadRequest(); } if (!ModelState.IsValid) { return BadRequest(ModelState); } var newItem = Mapper.Map<Client>(clientVm); newItem.SetCreation(UserName); _clientRepository.Add(newItem); if (!await UnitOfWork.SaveAsync()) { return StatusCode(500, "儲存客戶時出錯"); } var vm = Mapper.Map<ClientViewModel>(newItem); return CreatedAtRoute("GetClient", new { id = vm.Id }, vm); } [HttpPut("{id}")] public async Task<IActionResult> Put(int id, [FromBody] ClientModificationViewModel clientVm) { if (clientVm == null) { return BadRequest(); } if (!ModelState.IsValid) { return BadRequest(ModelState); } var dbItem = await _clientRepository.GetSingleAsync(id); if (dbItem == null) { return NotFound(); } Mapper.Map(clientVm, dbItem); dbItem.SetModification(UserName); _clientRepository.Update(dbItem); if (!await UnitOfWork.SaveAsync()) { return StatusCode(500, "儲存客戶時出錯"); } return NoContent(); } [HttpPatch("{id}")] public async Task<IActionResult> Patch(int id, [FromBody] JsonPatchDocument<ClientModificationViewModel> patchDoc) { if (patchDoc == null) { return BadRequest(); } var dbItem = await _clientRepository.GetSingleAsync(id); if (dbItem == null) { return NotFound(); } var toPatchVm = Mapper.Map<ClientModificationViewModel>(dbItem); patchDoc.ApplyTo(toPatchVm, ModelState); TryValidateModel(toPatchVm); if (!ModelState.IsValid) { return BadRequest(ModelState); } Mapper.Map(toPatchVm, dbItem); if (!await UnitOfWork.SaveAsync()) { return StatusCode(500, "更新的時候出錯"); } return NoContent(); } [HttpDelete("{id}")] public async Task<IActionResult> Delete(int id) { var model = await _clientRepository.GetSingleAsync(id); if (model == null) { return NotFound(); } _clientRepository.Delete(model); if (!await UnitOfWork.SaveAsync()) { return StatusCode(500, "刪除的時候出錯"); } return NoContent(); } } }
首先, Controller繼承了ControllerBase這個類, ControllerBase是自己寫的類, 裡面可以放置一些公用的方法或屬性, 目前裡面的東西都沒用:
namespace CoreApi.Web.Controllers.Bases { public abstract class BaseController<T> : Controller { protected readonly IUnitOfWork UnitOfWork; protected readonly ILogger<T> Logger; protected readonly IFileProvider FileProvider; protected readonly ICoreService<T> CoreService; protected BaseController(ICoreService<T> coreService) { CoreService = coreService; UnitOfWork = coreService.UnitOfWork; Logger = coreService.Logger; FileProvider = coreService.FileProvider; } #region Current Information protected DateTime Now => DateTime.Now; protected string UserName => User.Identity.Name ?? "Anonymous"; #endregion } }
由於父類建構函式依賴的類太多了, 所以我建立了一個CoreService, 裡面包含著這些依賴, 然後用一個變數就注入進去了, 這種寫法不一定正確:
public interface ICoreService<out T> : IDisposable { IUnitOfWork UnitOfWork { get; } ILogger<T> Logger { get; } IFileProvider FileProvider { get; } }
Controller裡面的方法應該都能看明白吧. 需要提一下的是UnitOfWork.
Unit Of Work
我才用的是UnitOfWork和Repository模式, 多個Repository掛起的資料庫操作, 可以使用一個UnitOfWork一次性提交.
由於DBContext已經實現了UnitOfWork模式, 所以可以直接在Controller裡面使用DbContext, 但是我還是做了一個介面 IUnitOfWork:
namespace CoreApi.DataContext.Infrastructure { public interface IUnitOfWork: IDisposable { int SaveChanges(); int SaveChanges(bool acceptAllChangesOnSuccess); Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default(CancellationToken)); Task<int> SaveChangesAsync(CancellationToken cancellationToken = default(CancellationToken)); bool Save(); bool Save(bool acceptAllChangesOnSuccess); Task<bool> SaveAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default(CancellationToken)); Task<bool> SaveAsync(CancellationToken cancellationToken = default(CancellationToken)); } }
裡面前4個方法就是DbContext內建的方法, 後面4個方法可有可無, 就是上面4個方法的簡單變形.
看一下CoreContext:
namespace CoreApi.DataContext.Core { public class CoreContext : DbContext, IUnitOfWork { public CoreContext(DbContextOptions<CoreContext> options) : base(options) { } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.HasDefaultSchema(AppSettings.DefaultSchema); modelBuilder.ApplyConfiguration(new UploadedFileConfiguration()); modelBuilder.ApplyConfiguration(new ClientConfiguration()); } public DbSet<UploadedFile> UploadedFiles { get; set; } public DbSet<Client> Clients { get; set; } public bool Save() { return SaveChanges() >= 0; } public bool Save(bool acceptAllChangesOnSuccess) { return SaveChanges(acceptAllChangesOnSuccess) >= 0; } public async Task<bool> SaveAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default(CancellationToken)) { return await SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken) >= 0; } public async Task<bool> SaveAsync(CancellationToken cancellationToken = default(CancellationToken)) { return await SaveChangesAsync(cancellationToken) >= 0; } } }
差不多了, 開始
遷移資料庫
在Package Manager Console分別執行 Add-Migration XXX和 Update-database命令.
注意這個時候 解決方案的啟動專案必須是Web專案, 如果設定了多個啟動專案, 遷移命令會不太好用.
然後執行一下: 選擇CoreApi.Web而不是IISExpress, 這樣的話埠應該是 http://localhost:5001/api/values
到Swagger裡簡單測試下
然後進入swagger簡單測試一下ClientController: http://localhost:5001/swagger/
先新增資料 POST:
先點選右側, 然後會把資料的json模板複製到左邊的框裡, 然後修改值, 然後點選try It out, 結果如下:
然後兩個Get, Delete, Put您都應該會測試.
這裡試一下 Patch:
再查詢一下, 應該沒有什麼問題.
先寫到這, 明天就能差不多寫完了吧.