Angular 5 และ ASP.NET Core
เผยแพร่แล้ว: 2022-03-11ฉันเคยคิดที่จะเขียนบล็อกโพสต์ตั้งแต่เวอร์ชันแรกของ Angular ที่ฆ่า Microsoft ทางฝั่งไคลเอ็นต์ เทคโนโลยีอย่าง ASP.Net, Web Forms และ MVC Razor ล้าสมัย แทนที่ด้วยเฟรมเวิร์ก JavaScript ที่ไม่ใช่ของ Microsoft อย่างแน่นอน อย่างไรก็ตาม ตั้งแต่ Angular เวอร์ชันที่สอง Microsoft และ Google ได้ทำงานร่วมกันเพื่อสร้าง Angular 2 และนี่คือช่วงเวลาที่เทคโนโลยีที่ฉันโปรดปรานทั้งสองเริ่มทำงานร่วมกัน
ในบล็อกนี้ ฉันต้องการช่วยให้ผู้คนสร้างสถาปัตยกรรมที่ดีที่สุดที่ผสมผสานสองโลกนี้เข้าด้วยกัน คุณพร้อมไหม? ไปเลย!
เกี่ยวกับสถาปัตยกรรม
คุณจะสร้างไคลเอนต์ Angular 5 ที่ใช้บริการ RESTful Web API Core 2
ฝั่งไคลเอ็นต์:
- เชิงมุม 5
- CLI .เชิงมุม
- วัสดุเชิงมุม
ฝั่งเซิร์ฟเวอร์:
- .NET C# เว็บ API Core 2
- การพึ่งพาการฉีด
- การรับรองความถูกต้อง JWT
- รหัสเฟรมเวิร์กของเอนทิตีก่อน
- SQL Server
บันทึก
ในบล็อกโพสต์นี้ เราถือว่าผู้อ่านมีความรู้พื้นฐานเกี่ยวกับ TypeScript, โมดูลเชิงมุม, ส่วนประกอบ และการนำเข้า/ส่งออกแล้ว เป้าหมายของโพสต์นี้คือการสร้างสถาปัตยกรรมที่ดีที่จะช่วยให้โค้ดเติบโตเมื่อเวลาผ่านไป |
อะไรที่คุณต้องการ?
เริ่มต้นด้วยการเลือก IDE แน่นอนว่านี่เป็นเพียงความชอบของฉัน และคุณสามารถใช้อันที่คุณรู้สึกสบายใจได้ ในกรณีของฉัน ฉันจะใช้ Visual Studio Code และ Visual Studio 2017
ทำไมสอง IDE ที่แตกต่างกัน? เนื่องจาก Microsoft ได้สร้าง Visual Studio Code สำหรับส่วนหน้า ฉันจึงไม่สามารถหยุดใช้ IDE นี้ได้ อย่างไรก็ตาม เราจะเห็นวิธีการผสานรวม Angular 5 ไว้ในโครงการโซลูชัน ซึ่งจะช่วยคุณได้หากคุณเป็นนักพัฒนาประเภทที่ต้องการแก้ไขจุดบกพร่องทั้งส่วนหลังและส่วนหน้าด้วย F5 เพียงอันเดียว
ในส่วนแบ็คเอนด์ คุณสามารถติดตั้ง Visual Studio 2017 เวอร์ชันล่าสุดซึ่งมีรุ่นฟรีสำหรับนักพัฒนา แต่จะสมบูรณ์มาก: ชุมชน
ต่อไปนี้คือรายการของสิ่งที่เราต้องติดตั้งสำหรับบทช่วยสอนนี้:
- รหัส Visual Studio
- ชุมชน Visual Studio 2017 (หรืออะไรก็ได้)
- Node.js v8.10.0
- SQL Server 2017
บันทึก
ตรวจสอบว่าคุณกำลังรันอย่างน้อย Node 6.9.x และ npm 3.xx โดยการรัน node -v และ npm -v ในหน้าต่างเทอร์มินัลหรือคอนโซล เวอร์ชันเก่าทำให้เกิดข้อผิดพลาด แต่เวอร์ชันที่ใหม่กว่าก็ใช้ได้ |
The Front End
เริ่มต้นอย่างรวดเร็ว
เริ่มความสนุกได้เลย! สิ่งแรกที่เราต้องทำคือติดตั้ง Angular CLI ทั่วโลก ดังนั้นให้เปิดพรอมต์คำสั่ง node.js และรันคำสั่งนี้:
npm install -g @angular/cli
เอาล่ะ ตอนนี้เรามีตัวรวมโมดูลของเราแล้ว ซึ่งมักจะติดตั้งโมดูลภายใต้โฟลเดอร์ผู้ใช้ของคุณ นามแฝงไม่จำเป็นตามค่าเริ่มต้น แต่ถ้าคุณต้องการ คุณสามารถดำเนินการในบรรทัดถัดไป:
alias ng="<UserFolder>/.npm/lib/node_modules/angular-cli/bin/ng"
ขั้นตอนต่อไปคือการสร้างโครงการใหม่ ฉันจะเรียกมันว่า angular5-app
ขั้นแรก เรานำทางไปยังโฟลเดอร์ที่เราต้องการสร้างไซต์ จากนั้น:
ng new angular5-app
สร้างครั้งแรก
ในขณะที่คุณสามารถทดสอบเว็บไซต์ใหม่ของคุณเพียงแค่เรียก ng serve --open
ฉันขอแนะนำให้ทดสอบไซต์จากบริการเว็บที่คุณโปรดปราน ทำไม? ปัญหาบางอย่างอาจเกิดขึ้นได้ในการผลิตเท่านั้น และการสร้างไซต์ด้วย ng build
เป็นวิธีที่ใกล้เคียงที่สุดในการเข้าถึงสภาพแวดล้อมนี้ จากนั้นเราสามารถเปิดโฟลเดอร์ angular5-app
ด้วย Visual Studio Code และเรียกใช้ ng build
บนเทอร์มินัล bash:
โฟลเดอร์ใหม่ที่เรียกว่า dist
จะถูกสร้างขึ้น และเราสามารถให้บริการโดยใช้ IIS หรือเว็บเซิร์ฟเวอร์ใดก็ได้ที่คุณต้องการ จากนั้นคุณสามารถพิมพ์ URL ในเบราว์เซอร์และ…เสร็จสิ้น!
บันทึก
บทช่วยสอนนี้ไม่มีจุดประสงค์เพื่อแสดงวิธีตั้งค่าเว็บเซิร์ฟเวอร์ ดังนั้นฉันคิดว่าคุณมีความรู้นั้นอยู่แล้ว |
โฟลเดอร์ src
โฟลเดอร์ src
ของฉันมีโครงสร้างดังนี้: ภายในโฟลเดอร์ app
เรามี components
ที่เราจะสร้างไฟล์ css
, ts
, spec
และ html
สำหรับแต่ละองค์ประกอบเชิงมุม นอกจากนี้เรายังจะสร้างโฟลเดอร์การกำหนด config
เพื่อรักษาการกำหนดค่าไซต์ directives
จะมีคำสั่งที่กำหนดเองทั้งหมด helpers
จะจัดเก็บรหัสทั่วไปเช่นตัวจัดการการตรวจสอบสิทธิ์ เลย์ layout
ต์จะมีส่วนประกอบหลักเช่น เนื้อหา ส่วนหัว และแผงด้านข้าง models
จะเก็บสิ่งที่จะ ตรงกับโมเดลมุมมองแบ็กเอนด์ และในที่สุด services
จะมีรหัสสำหรับการโทรทั้งหมดไปยังส่วนหลัง
นอกโฟลเดอร์ app
เราจะเก็บโฟลเดอร์ที่สร้างไว้ตามค่าเริ่มต้น เช่น assets
และ environments
และไฟล์รูทด้วย
การสร้างไฟล์กำหนดค่า
มาสร้างไฟล์ config.ts
ในโฟลเดอร์ config
ของเรา แล้วเรียกคลาส AppConfig
นี่คือที่ที่เราสามารถตั้งค่าทั้งหมดที่เราจะใช้ในที่ต่างๆ ในโค้ดของเรา ตัวอย่างเช่น URL ของ API โปรดทราบว่าคลาสใช้คุณสมบัติ get
ซึ่งรับโครงสร้างคีย์/ค่าเป็นพารามิเตอร์ และวิธีการง่ายๆ ในการเข้าถึงค่าเดียวกัน ด้วยวิธีนี้ จะเป็นการง่ายที่จะได้รับค่าเพียงแค่เรียก this.config.setting['PathAPI']
จากคลาสที่สืบทอดมา
import { Injectable } from '@angular/core'; @Injectable() export class AppConfig { private _config: { [key: string]: string }; constructor() { this._config = { PathAPI: 'http://localhost:50498/api/' }; } get setting():{ [key: string]: string } { return this._config; } get(key: any) { return this._config[key]; } };
วัสดุเชิงมุม
ก่อนเริ่มเลย์เอาต์ เรามาตั้งค่าเฟรมเวิร์กคอมโพเนนต์ UI กันก่อน แน่นอน คุณสามารถใช้อย่างอื่นเช่น Bootstrap ได้ แต่ถ้าคุณชอบสไตล์ของ Material ฉันขอแนะนำเพราะ Google รองรับเช่นกัน
ในการติดตั้ง เราเพียงแค่เรียกใช้คำสั่งสามคำสั่งถัดไป ซึ่งเราสามารถดำเนินการบนเทอร์มินัล Visual Studio Code:
npm install --save @angular/material @angular/cdk npm install --save @angular/animations npm install --save hammerjs
คำสั่งที่สองเป็นเพราะองค์ประกอบวัสดุบางอย่างขึ้นอยู่กับแอนิเมชั่นเชิงมุม ฉันยังแนะนำให้อ่านหน้าอย่างเป็นทางการเพื่อทำความเข้าใจว่าเบราว์เซอร์ใดที่รองรับและโพลีฟิลคืออะไร
คำสั่งที่สามเป็นเพราะส่วนประกอบวัสดุบางอย่างพึ่งพา HammerJS สำหรับท่าทางสัมผัส
ตอนนี้ เราสามารถดำเนินการนำเข้าโมดูลส่วนประกอบที่เราต้องการใช้ในไฟล์ app.module.ts
ของเรา:
import {MatButtonModule, MatCheckboxModule} from '@angular/material'; import {MatInputModule} from '@angular/material/input'; import {MatFormFieldModule} from '@angular/material/form-field'; import {MatSidenavModule} from '@angular/material/sidenav'; // ... @NgModule({ imports: [ BrowserModule, BrowserAnimationsModule, MatButtonModule, MatCheckboxModule, MatInputModule, MatFormFieldModule, MatSidenavModule, AppRoutingModule, HttpClientModule ],
ขั้นตอนต่อไปคือการเปลี่ยน style.css
โดยเพิ่มประเภทของธีมที่คุณต้องการใช้:
@import "~@angular/material/prebuilt-themes/deeppurple-amber.css";
ตอนนี้นำเข้า HammerJS โดยเพิ่มบรรทัดนี้ในไฟล์ main.ts
:
import 'hammerjs';
และสุดท้าย สิ่งที่เราขาดหายไปคือการเพิ่มไอคอน Material ไปที่ index.html
ภายในส่วนหัว:
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
เค้าโครง
ในตัวอย่างนี้ เราจะสร้างเลย์เอาต์ง่ายๆ ดังนี้:
แนวคิดคือการเปิด/ซ่อนเมนูโดยคลิกที่ปุ่มบางปุ่มบนส่วนหัว Angular Responsive จะทำงานที่เหลือให้เราเอง ในการดำเนินการนี้ เราจะสร้างโฟลเดอร์ layout
ย์เอาต์และใส่ไฟล์ app.component
ที่สร้างขึ้นโดยค่าเริ่มต้นไว้ในนั้น แต่เราจะสร้างไฟล์เดียวกันสำหรับแต่ละส่วนของเลย์เอาต์ดังที่คุณเห็นในภาพถัดไป จากนั้น app.component
จะเป็นเนื้อหา head.component
ส่วนหัวและ left-panel.component
เมนู
ตอนนี้เรามาเปลี่ยน app.component.html
ดังนี้:
<div *ngIf="authentication"> <app-head></app-head> <button type="button" mat-button (click)="drawer.toggle()"> Menu </button> <mat-drawer-container class="example-container" autosize> <mat-drawer #drawer class="example-sidenav" mode="side"> <app-left-panel></app-left-panel> </mat-drawer> <div> <router-outlet></router-outlet> </div> </mat-drawer-container> </div> <div *ngIf="!authentication"><app-login></app-login></div>
โดยพื้นฐานแล้ว เราจะมีคุณสมบัติการ authentication
ในส่วนประกอบ ซึ่งจะทำให้เราสามารถลบส่วนหัวและเมนูได้หากผู้ใช้ไม่ได้เข้าสู่ระบบ และแสดงหน้าเข้าสู่ระบบอย่างง่ายแทน
head.component.html
มีลักษณะดังนี้:
<h1>{{title}}</h1> <button mat-button [routerLink]=" ['./logout'] ">Logout!</button>
เพียงปุ่มเดียวเพื่อออกจากระบบผู้ใช้ เราจะกลับมาที่นี่อีกครั้งในภายหลัง สำหรับ left-panel.component.html
ตอนนี้เพียงแค่เปลี่ยน HTML เป็น:
<nav> <a routerLink="/dashboard">Dashboard</a> <a routerLink="/users">Users</a> </nav>
เราได้ทำให้มันง่าย: จนถึงตอนนี้ มีเพียงสองลิงก์เพื่อไปยังหน้าต่างๆ สองหน้า (เราจะกลับมาที่นี้ในภายหลัง)
นี่คือสิ่งที่ส่วนหัวและไฟล์ TypeScript ขององค์ประกอบด้านซ้ายมีลักษณะดังนี้:
import { Component } from '@angular/core'; @Component({ selector: 'app-head', templateUrl: './head.component.html', styleUrls: ['./head.component.css'] }) export class HeadComponent { title = 'Angular 5 Seed'; }
import { Component } from '@angular/core'; @Component({ selector: 'app-left-panel', templateUrl: './left-panel.component.html', styleUrls: ['./left-panel.component.css'] }) export class LeftPanelComponent { title = 'Angular 5 Seed'; }
แต่แล้วโค้ด TypeScript สำหรับ app.component
ล่ะ เราจะทิ้งความลึกลับไว้ที่นี่และหยุดชั่วขณะหนึ่ง และกลับมาที่สิ่งนี้หลังจากใช้การรับรองความถูกต้อง
การกำหนดเส้นทาง
เอาล่ะ ตอนนี้เรามี Angular Material ที่ช่วยเราด้วย UI และเลย์เอาต์ง่ายๆ เพื่อเริ่มสร้างเพจของเรา แต่เราจะนำทางระหว่างหน้าต่างๆ ได้อย่างไร
เพื่อสร้างตัวอย่างง่ายๆ ให้สร้างหน้าสองหน้า: "ผู้ใช้" ซึ่งเราสามารถรับรายชื่อผู้ใช้ที่มีอยู่ในฐานข้อมูล และ "แดชบอร์ด" ซึ่งเป็นหน้าที่เราสามารถแสดงสถิติได้
ภายในโฟลเดอร์ app
เราจะสร้างไฟล์ชื่อ app-routing.modules.ts
ลักษณะดังนี้:
import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { AuthGuard } from './helpers/canActivateAuthGuard'; import { LoginComponent } from './components/login/login.component'; import { LogoutComponent } from './components/login/logout.component'; import { DashboardComponent } from './components/dashboard/dashboard.component'; import { UsersComponent } from './components/users/users.component'; const routes: Routes = [ { path: '', redirectTo: '/dashboard', pathMatch: 'full', canActivate: [AuthGuard] }, { path: 'login', component: LoginComponent}, { path: 'logout', component: LogoutComponent}, { path: 'dashboard', component: DashboardComponent, canActivate: [AuthGuard] }, { path: 'users', component: UsersComponent,canActivate: [AuthGuard] } ]; @NgModule({ imports: [ RouterModule.forRoot(routes) ], exports: [ RouterModule ] }) export class AppRoutingModule {}
ง่ายมาก เพียงแค่นำเข้า RouterModule
และ Routes
จาก @angular/router
เราก็สามารถจับคู่เส้นทางที่เราต้องการนำไปใช้ ที่นี่เรากำลังสร้างสี่เส้นทาง:
-
/dashboard
: หน้าแรกของเรา -
/login
: หน้าที่ผู้ใช้สามารถยืนยันตัวตนได้ -
/logout
: เส้นทางง่ายๆ ในการออกจากระบบผู้ใช้ -
/users
: หน้าแรกของเราที่เราต้องการแสดงรายการผู้ใช้จากส่วนหลัง
โปรดทราบว่า dashboard
คือหน้าของเราโดยค่าเริ่มต้น ดังนั้นหากผู้ใช้พิมพ์ URL /
หน้าจะเปลี่ยนเส้นทางไปยังหน้านี้โดยอัตโนมัติ นอกจากนี้ ให้ดูที่พารามิเตอร์ canActivate
: ที่นี่เรากำลังสร้างการอ้างอิงถึงคลาส AuthGuard
ซึ่งจะช่วยให้เราสามารถตรวจสอบว่าผู้ใช้ลงชื่อเข้าใช้หรือไม่ ถ้าไม่ ระบบจะเปลี่ยนเส้นทางไปยังหน้าเข้าสู่ระบบ ในส่วนถัดไป ฉันจะแสดงวิธีสร้างชั้นเรียนนี้
ตอนนี้ สิ่งที่เราต้องทำคือสร้างเมนู จำในส่วนเค้าโครงเมื่อเราสร้างไฟล์ left-panel.component.html
ให้มีลักษณะเช่นนี้หรือไม่
<nav> <a routerLink="/dashboard">Dashboard</a> <a routerLink="/users">Users</a> </nav>
นี่คือจุดที่โค้ดของเรามาบรรจบกับความเป็นจริง ตอนนี้ เราสามารถสร้างโค้ดและทดสอบใน URL ได้แล้ว: คุณควรจะสามารถนำทางจากหน้าแดชบอร์ดไปยังผู้ใช้ได้ แต่จะเกิดอะไรขึ้นถ้าคุณพิมพ์ URL our.site.url/users
ในเบราว์เซอร์โดยตรง
โปรดทราบว่าข้อผิดพลาดนี้จะปรากฏขึ้นหากคุณรีเฟรชเบราว์เซอร์หลังจากนำทางไปยัง URL นั้นผ่านแผงด้านข้างของแอปเรียบร้อยแล้ว เพื่อให้เข้าใจข้อผิดพลาดนี้ ให้ฉันอ้างอิงถึงเอกสารอย่างเป็นทางการที่มีความชัดเจนมาก:
แอปพลิเคชันที่กำหนดเส้นทางควรสนับสนุนลิงก์ในรายละเอียด ลิงก์ในรายละเอียดคือ URL ที่ระบุเส้นทางไปยังส่วนประกอบภายในแอป ตัวอย่างเช่น
http://www.mysite.com/users/42
เป็นลิงก์ในรายละเอียดไปยังหน้ารายละเอียดฮีโร่ที่แสดงฮีโร่ด้วย id: 42ไม่มีปัญหาเมื่อผู้ใช้นำทางไปยัง URL นั้นจากภายในไคลเอนต์ที่ทำงานอยู่ เราเตอร์ Angular ตีความ URL และเส้นทางไปยังหน้าและฮีโร่นั้น
แต่การคลิกลิงก์ในอีเมล ป้อนลงในแถบที่อยู่ของเบราว์เซอร์ หรือเพียงแค่รีเฟรชเบราว์เซอร์ขณะอยู่ในหน้ารายละเอียดฮีโร่ การดำเนินการทั้งหมดเหล่านี้ได้รับการจัดการโดยเบราว์เซอร์เอง นอกแอปพลิเคชันที่ทำงานอยู่ เบราว์เซอร์ส่งคำขอโดยตรงไปยังเซิร์ฟเวอร์สำหรับ URL นั้น โดยข้ามเราเตอร์เซิร์ฟเวอร์สแตติกจะส่งคืน index.html เป็นประจำเมื่อได้รับคำขอ
http://www.mysite.com/
แต่มันปฏิเสธhttp://www.mysite.com/users/42
และส่งคืนข้อผิดพลาด 404 - Not Found เว้นแต่จะได้รับการกำหนดค่าให้ส่งคืน index.html แทน
เพื่อแก้ไขปัญหานี้ง่ายมาก เราเพียงแค่ต้องสร้างการกำหนดค่าไฟล์ของผู้ให้บริการ เนื่องจากฉันทำงานกับ IIS ที่นี่ ฉันจะแสดงให้คุณเห็นว่าต้องทำอย่างไรในสภาพแวดล้อมนี้ แต่แนวคิดนี้คล้ายกับ Apache หรือเว็บเซิร์ฟเวอร์อื่นๆ
ดังนั้นเราจึงสร้างไฟล์ภายในโฟลเดอร์ src
ชื่อ web.config
ซึ่งมีลักษณะดังนี้:
<?xml version="1.0"?> <configuration> <system.webServer> <rewrite> <rules> <rule name="Angular Routes" stopProcessing="true"> <match url=".*" /> <conditions logicalGrouping="MatchAll"> <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" /> <add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" /> </conditions> <action type="Rewrite" url="/index.html" /> </rule> </rules> </rewrite> </system.webServer> <system.web> <compilation debug="true"/> </system.web> </configuration>
จากนั้นเราต้องแน่ใจว่าเนื้อหานี้จะถูกคัดลอกไปยังโฟลเดอร์ที่ปรับใช้ สิ่งที่เราต้องทำคือเปลี่ยนไฟล์การตั้งค่า Angular CLI angular-cli.json
:
{ "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "project": { "name": "angular5-app" }, "apps": [ { "root": "src", "outDir": "dist", "assets": [ "assets", "favicon.ico", "web.config" // or whatever equivalent is required by your web server ], "index": "index.html", "main": "main.ts", "polyfills": "polyfills.ts", "test": "test.ts", "tsconfig": "tsconfig.app.json", "testTsconfig": "tsconfig.spec.json", "prefix": "app", "styles": [ "styles.css" ], "scripts": [], "environmentSource": "environments/environment.ts", "environments": { "dev": "environments/environment.ts", "prod": "environments/environment.prod.ts" } } ], "e2e": { "protractor": { "config": "./protractor.conf.js" } }, "lint": [ { "project": "src/tsconfig.app.json", "exclude": "**/node_modules/**" }, { "project": "src/tsconfig.spec.json", "exclude": "**/node_modules/**" }, { "project": "e2e/tsconfig.e2e.json", "exclude": "**/node_modules/**" } ], "test": { "karma": { "config": "./karma.conf.js" } }, "defaults": { "styleExt": "css", "component": {} } }
การตรวจสอบสิทธิ์
คุณจำวิธีที่เราใช้คลาส AuthGuard
เพื่อตั้งค่าการกำหนดเส้นทางได้อย่างไร ทุกครั้งที่เราไปยังหน้าอื่น เราจะใช้คลาสนี้เพื่อตรวจสอบว่าผู้ใช้ได้รับการพิสูจน์ตัวตนด้วยโทเค็นหรือไม่ ถ้าไม่เช่นนั้น เราจะเปลี่ยนเส้นทางไปยังหน้าเข้าสู่ระบบโดยอัตโนมัติ ไฟล์สำหรับสิ่งนี้คือ canActivateAuthGuard.ts
—สร้างมันในโฟลเดอร์ helpers
และมีลักษณะดังนี้:
import { CanActivate, Router } from '@angular/router'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs/Observable'; import { Helpers } from './helpers'; import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; @Injectable() export class AuthGuard implements CanActivate { constructor(private router: Router, private helper: Helpers) {} canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | boolean { if (!this.helper.isAuthenticated()) { this.router.navigate(['/login']); return false; } return true; } }
ดังนั้นทุกครั้งที่เราเปลี่ยนหน้าเมธอด canActivate
จะถูกเรียก ซึ่งจะตรวจสอบว่าผู้ใช้ได้รับการตรวจสอบสิทธิ์หรือไม่ และหากไม่เป็นเช่นนั้น เราจะใช้อินสแตนซ์ Router
เพื่อเปลี่ยนเส้นทางไปยังหน้าเข้าสู่ระบบ แต่วิธีการใหม่ในคลาส Helper
คืออะไร? ใต้โฟลเดอร์ helpers
ให้สร้างไฟล์ helpers.ts
ที่นี่เราต้องจัดการ localStorage
ซึ่งเราจะเก็บโทเค็นที่เราได้รับจากส่วนหลัง
บันทึก
เกี่ยวกับ localStorage คุณยังสามารถใช้คุกกี้หรือ sessionStorage และการตัดสินใจจะขึ้นอยู่กับพฤติกรรมที่เราต้องการนำไปใช้ ตามชื่อที่แนะนำ sessionStorage จะใช้ได้เฉพาะในช่วงเวลาของเซสชันของเบราว์เซอร์ และจะถูกลบออกเมื่อปิดแท็บหรือหน้าต่าง อย่างไรก็ตาม มันไม่สามารถโหลดหน้าซ้ำได้ หากข้อมูลที่คุณกำลังจัดเก็บจำเป็นต้องมีอยู่อย่างต่อเนื่อง localStorage จะดีกว่า sessionStorage คุกกี้มีไว้สำหรับการอ่านฝั่งเซิร์ฟเวอร์เป็นหลัก ในขณะที่ localStorage สามารถอ่านได้เฉพาะฝั่งไคลเอ็นต์เท่านั้น คำถามคือ ในแอปของคุณ ใครต้องการข้อมูลนี้ ไคลเอนต์หรือเซิร์ฟเวอร์ |
import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { Subject } from 'rxjs/Subject'; @Injectable() export class Helpers { private authenticationChanged = new Subject<boolean>(); constructor() { } public isAuthenticated():boolean { return (!(window.localStorage['token'] === undefined || window.localStorage['token'] === null || window.localStorage['token'] === 'null' || window.localStorage['token'] === 'undefined' || window.localStorage['token'] === '')); } public isAuthenticationChanged():any { return this.authenticationChanged.asObservable(); } public getToken():any { if( window.localStorage['token'] === undefined || window.localStorage['token'] === null || window.localStorage['token'] === 'null' || window.localStorage['token'] === 'undefined' || window.localStorage['token'] === '') { return ''; } let obj = JSON.parse(window.localStorage['token']); return obj.token; } public setToken(data:any):void { this.setStorageToken(JSON.stringify(data)); } public failToken():void { this.setStorageToken(undefined); } public logout():void { this.setStorageToken(undefined); } private setStorageToken(value: any):void { window.localStorage['token'] = value; this.authenticationChanged.next(this.isAuthenticated()); } }
รหัสการรับรองความถูกต้องของเราเหมาะสมหรือไม่ เราจะกลับมาที่คลาส Subject
ในภายหลัง แต่ตอนนี้ กลับมาที่การกำหนดค่าการกำหนดเส้นทางสักครู่ ลองดูที่บรรทัดนี้:
{ path: 'logout', component: LogoutComponent},
นี่เป็นองค์ประกอบของเราในการออกจากระบบไซต์ และเป็นเพียงคลาสง่ายๆ ในการทำความสะอาด localStorage
มาสร้างมันภายใต้โฟลเดอร์ components/login
ด้วยชื่อ logout.component.ts
:
import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { Helpers } from '../../helpers/helpers'; @Component({ selector: 'app-logout', template:'<ng-content></ng-content>' }) export class LogoutComponent implements OnInit { constructor(private router: Router, private helpers: Helpers) { } ngOnInit() { this.helpers.logout(); this.router.navigate(['/login']); } }
ดังนั้นทุกครั้งที่เราไปที่ URL /logout
localStorage
จะถูกลบออกและเว็บไซต์จะเปลี่ยนเส้นทางไปยังหน้าเข้าสู่ระบบ สุดท้ายมาสร้าง login.component.ts
แบบนี้:
import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { TokenService } from '../../services/token.service'; import { Helpers } from '../../helpers/helpers'; @Component({ selector: 'app-login', templateUrl: './login.component.html', styleUrls: [ './login.component.css' ] }) export class LoginComponent implements OnInit { constructor(private helpers: Helpers, private router: Router, private tokenService: TokenService) { } ngOnInit() { } login(): void { let authValues = {"Username":"pablo", "Password":"secret"}; this.tokenService.auth(authValues).subscribe(token => { this.helpers.setToken(token); this.router.navigate(['/dashboard']); }); } }
อย่างที่คุณเห็น ในขณะนี้เราได้ฮาร์ดโค้ดข้อมูลประจำตัวของเราที่นี่ โปรดทราบว่าที่นี่เรากำลังเรียกคลาสบริการ เราจะสร้างคลาสบริการเหล่านี้เพื่อเข้าถึงแบ็กเอนด์ของเราในหัวข้อถัดไป
สุดท้ายเราต้องกลับไปที่ไฟล์ app.component.ts
ซึ่งเป็นเลย์เอาต์ของไซต์ ที่นี่ หากผู้ใช้ได้รับการตรวจสอบสิทธิ์ จะแสดงเมนูและส่วนหัว แต่ถ้าไม่ เค้าโครงจะเปลี่ยนเพื่อแสดงเฉพาะหน้าเข้าสู่ระบบของเรา
export class AppComponent implements AfterViewInit { subscription: Subscription; authentication: boolean; constructor(private helpers: Helpers) { } ngAfterViewInit() { this.subscription = this.helpers.isAuthenticationChanged().pipe( startWith(this.helpers.isAuthenticated()), delay(0)).subscribe((value) => this.authentication = value ); } title = 'Angular 5 Seed'; ngOnDestroy() { this.subscription.unsubscribe(); } }
จำคลาส Subject
ในคลาส helper ของเราได้ไหม? นี้เป็นที่ Observable
ได้ Observable
ให้การสนับสนุนการส่งข้อความระหว่างผู้เผยแพร่และสมาชิกในแอปพลิเคชันของคุณ ทุกครั้งที่มีการเปลี่ยนแปลงโทเค็นการ authentication
ตัวตน คุณสมบัติการรับรองความถูกต้องจะได้รับการอัปเดต การตรวจสอบไฟล์ app.component.html
น่าจะสมเหตุสมผลกว่าในตอนนี้:

<div *ngIf="authentication"> <app-head></app-head> <button type="button" mat-button (click)="drawer.toggle()"> Menu </button> <mat-drawer-container class="example-container" autosize> <mat-drawer #drawer class="example-sidenav" mode="side"> <app-left-panel></app-left-panel> </mat-drawer> <div> <router-outlet></router-outlet> </div> </mat-drawer-container> </div> <div *ngIf="!authentication"><app-login></app-login></div>
บริการ
ณ จุดนี้ เรากำลังนำทางไปยังหน้าต่างๆ รับรองความถูกต้องฝั่งไคลเอ็นต์ของเรา และแสดงเลย์เอาต์ที่เรียบง่ายมาก แต่เราจะได้รับข้อมูลจากส่วนหลังได้อย่างไร ฉันขอแนะนำอย่างยิ่งให้ทำการเข้าถึงแบ็กเอนด์ทั้งหมดจากคลาส บริการ โดยเฉพาะ บริการแรกของเราจะอยู่ในโฟลเดอร์ services
ที่เรียกว่า token.service.ts
:
import { Injectable } from '@angular/core'; import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Observable } from 'rxjs/Observable'; import { of } from 'rxjs/observable/of'; import { catchError, map, tap } from 'rxjs/operators'; import { AppConfig } from '../config/config'; import { BaseService } from './base.service'; import { Token } from '../models/token'; import { Helpers } from '../helpers/helpers'; @Injectable() export class TokenService extends BaseService { private pathAPI = this.config.setting['PathAPI']; public errorMessage: string; constructor(private http: HttpClient, private config: AppConfig, helper: Helpers) { super(helper); } auth(data: any): any { let body = JSON.stringify(data); return this.getToken(body); } private getToken (body: any): Observable<any> { return this.http.post<any>(this.pathAPI + 'token', body, super.header()).pipe( catchError(super.handleError) ); } }
การเรียกไปยังแบ็กเอนด์ครั้งแรกเป็นการเรียก POST ไปยังโทเค็น API โทเค็น API ไม่ต้องการสตริงโทเค็นในส่วนหัว แต่จะเกิดอะไรขึ้นหากเราเรียกปลายทางอื่น ดังที่คุณเห็นที่นี่ TokenService
(และคลาสบริการโดยทั่วไป) สืบทอดจากคลาส BaseService
ลองดูที่นี้:
import { Injectable } from '@angular/core'; import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Observable } from 'rxjs/Observable'; import { of } from 'rxjs/observable/of'; import { catchError, map, tap } from 'rxjs/operators'; import { Helpers } from '../helpers/helpers'; @Injectable() export class BaseService { constructor(private helper: Helpers) { } public extractData(res: Response) { let body = res.json(); return body || {}; } public handleError(error: Response | any) { // In a real-world app, we might use a remote logging infrastructure let errMsg: string; if (error instanceof Response) { const body = error.json() || ''; const err = body || JSON.stringify(body); errMsg = `${error.status} - ${error.statusText || ''} ${err}`; } else { errMsg = error.message ? error.message : error.toString(); } console.error(errMsg); return Observable.throw(errMsg); } public header() { let header = new HttpHeaders({ 'Content-Type': 'application/json' }); if(this.helper.isAuthenticated()) { header = header.append('Authorization', 'Bearer ' + this.helper.getToken()); } return { headers: header }; } public setToken(data:any) { this.helper.setToken(data); } public failToken(error: Response | any) { this.helper.failToken(); return this.handleError(Response); } }
ดังนั้นทุกครั้งที่เราทำการโทร HTTP เราจะใช้ส่วนหัวของคำขอโดยใช้ super.header
หากโทเค็นอยู่ใน localStorage
โทเค็นนั้นจะถูกผนวกไว้ในส่วนหัว แต่ถ้าไม่ใช่ เราจะตั้งค่ารูปแบบ JSON อีกสิ่งหนึ่งที่เราเห็นได้คือจะเกิดอะไรขึ้นหากการรับรองความถูกต้องล้มเหลว
องค์ประกอบการเข้าสู่ระบบจะเรียกคลาสบริการและคลาสบริการจะเรียกส่วนหลัง เมื่อเรามีโทเค็นแล้ว คลาสตัวช่วยจะจัดการโทเค็น และตอนนี้เราก็พร้อมที่จะรับรายชื่อผู้ใช้จากฐานข้อมูลของเราแล้ว
ในการรับข้อมูลจากฐานข้อมูล ก่อนอื่นต้องแน่ใจว่าเราจับคู่คลาสโมเดลกับโมเดลมุมมองส่วนหลังในการตอบกลับของเรา
ใน user.ts
:
export class User { id: number; name: string; }
และเราสามารถสร้างไฟล์ user.service.ts
ได้ในขณะนี้:
import { Injectable } from '@angular/core'; import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Observable } from 'rxjs/Observable'; import { of } from 'rxjs/observable/of'; import { catchError, map, tap } from 'rxjs/operators'; import { BaseService } from './base.service'; import { User } from '../models/user'; import { AppConfig } from '../config/config'; import { Helpers } from '../helpers/helpers'; @Injectable() export class UserService extends BaseService { private pathAPI = this.config.setting['PathAPI']; constructor(private http: HttpClient, private config: AppConfig, helper: Helpers) { super(helper); } /** GET heroes from the server */ getUsers (): Observable<User[]> { return this.http.get(this.pathAPI + 'user', super.header()).pipe( catchError(super.handleError)); }
The Back End
เริ่มต้นอย่างรวดเร็ว
ยินดีต้อนรับสู่ขั้นตอนแรกของแอปพลิเคชัน Web API Core 2 ของเรา สิ่งแรกที่เราต้องการคือสร้าง ASP.Net Core Web Application ซึ่งเราจะเรียกว่า SeedAPI.Web.API
อย่าลืมเลือกเทมเพลตที่ว่างเปล่าเพื่อการเริ่มต้นใหม่ทั้งหมดดังที่คุณเห็นด้านล่าง:
นั่นคือทั้งหมด เราสร้างโซลูชันโดยเริ่มจากเว็บแอปพลิเคชันเปล่า ตอนนี้สถาปัตยกรรมของเราจะเป็นไปตามรายการด้านล่าง ดังนั้นจะต้องสร้างโครงการต่างๆ:
ในการดำเนินการนี้ ให้คลิกขวาที่โซลูชันแล้วเพิ่มโครงการ "Class Library (.NET Core)" สำหรับแต่ละรายการ
สถาปัตยกรรม
ในส่วนที่แล้ว เราสร้างโครงการไว้แปดโครงการ แต่มีไว้เพื่ออะไร? นี่คือคำอธิบายง่ายๆ ของแต่ละรายการ:
-
Web.API
: นี่คือโครงการเริ่มต้นของเราและสร้างจุดสิ้นสุด ที่นี่เราจะตั้งค่า JWT การขึ้นต่อกันของการฉีด และตัวควบคุม -
ViewModels
: ที่นี่เราทำการแปลงจากประเภทของข้อมูลที่ผู้ควบคุมจะส่งคืนในการตอบสนองต่อส่วนหน้า แนวปฏิบัติที่ดีในการจับคู่คลาสเหล่านี้กับโมเดลส่วนหน้า - อินเทอร์
Interfaces
: สิ่งนี้จะเป็นประโยชน์ในการใช้การพึ่งพาการฉีด ประโยชน์ที่น่าสนใจของภาษาที่พิมพ์แบบสแตติกคือคอมไพเลอร์สามารถช่วยตรวจสอบว่าสัญญาที่โค้ดของคุณใช้นั้นเป็นไปตามสัญญาจริงหรือไม่ -
Commons
: พฤติกรรมที่ใช้ร่วมกันและรหัสยูทิลิตี้ทั้งหมดจะอยู่ที่นี่ -
Models
: เป็นการดีที่จะไม่จับคู่ฐานข้อมูลโดยตรงกับViewModels
ที่หันหน้าไปทาง front-end ดังนั้นจุดประสงค์ของModels
คือการสร้างคลาสฐานข้อมูลเอนทิตีที่เป็นอิสระจากส่วนหน้า ซึ่งจะทำให้เราสามารถเปลี่ยนแปลงฐานข้อมูลของเราในอนาคตโดยไม่จำเป็นต้องมีผลกระทบต่อส่วนหน้าของเรา นอกจากนี้ยังช่วยเมื่อเราเพียงแค่ต้องการทำการปรับโครงสร้างใหม่ -
Maps
: นี่คือที่ที่เราจับคู่ViewModels
กับModels
และในทางกลับกัน ขั้นตอนนี้เรียกว่าระหว่างตัวควบคุมและบริการ -
Services
: ห้องสมุดสำหรับจัดเก็บตรรกะทางธุรกิจทั้งหมด -
Repositories
: นี่เป็นที่เดียวที่เราเรียกฐานข้อมูล
การอ้างอิงจะมีลักษณะดังนี้:
การรับรองความถูกต้องตาม JWT
ในส่วนนี้ เราจะเห็นการกำหนดค่าพื้นฐานของการพิสูจน์ตัวตนโทเค็นและเจาะลึกเรื่องความปลอดภัยอีกเล็กน้อย
ในการเริ่มต้นตั้งค่าโทเค็นเว็บ JSON (JWT) ให้สร้างคลาสถัดไปภายในโฟลเดอร์ App_Start
ชื่อ JwtTokenConfig.cs
รหัสภายในจะมีลักษณะดังนี้:
namespace SeedAPI.Web.API.App_Start { public class JwtTokenConfig { public static void AddAuthentication(IServiceCollection services, IConfiguration configuration) { services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = true, ValidateIssuerSigningKey = true, ValidIssuer = configuration["Jwt:Issuer"], ValidAudience = configuration["Jwt:Issuer"], IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["Jwt:Key"])) }; services.AddCors(); }); } } }
ค่าของพารามิเตอร์การตรวจสอบความถูกต้องจะขึ้นอยู่กับความต้องการของแต่ละโครงการ ผู้ใช้ที่ถูกต้องและผู้ชมที่เราสามารถตั้งค่าการอ่านไฟล์การกำหนดค่า appsettings.json
:
"Jwt": { "Key": "veryVerySecretKey", "Issuer": "http://localhost:50498/" }
จากนั้นเราต้องเรียกจากวิธี ConfigureServices
ใน startup.cs
เท่านั้น:
// This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { DependencyInjectionConfig.AddScope(services); JwtTokenConfig.AddAuthentication(services, Configuration); DBContextConfig.Initialize(services, Configuration); services.AddMvc(); }
ตอนนี้เราพร้อมที่จะสร้างคอนโทรลเลอร์ตัวแรกของเราที่ชื่อว่า TokenController.cs
ค่าที่เราตั้งค่าใน appsettings.json
เป็น "veryVerySecretKey"
ควรตรงกับค่าที่เราใช้สร้างโทเค็น แต่ก่อนอื่น ให้สร้าง LoginViewModel
ภายในโปรเจ็กต์ ViewModels
ของเราก่อน:
namespace SeedAPI.ViewModels { public class LoginViewModel : IBaseViewModel { public string username { get; set; } public string password { get; set; } } }
และสุดท้ายตัวควบคุม:
namespace SeedAPI.Web.API.Controllers { [Route("api/Token")] public class TokenController : Controller { private IConfiguration _config; public TokenController(IConfiguration config) { _config = config; } [AllowAnonymous] [HttpPost] public dynamic Post([FromBody]LoginViewModel login) { IActionResult response = Unauthorized(); var user = Authenticate(login); if (user != null) { var tokenString = BuildToken(user); response = Ok(new { token = tokenString }); } return response; } private string BuildToken(UserViewModel user) { var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Jwt:Key"])); var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); var token = new JwtSecurityToken(_config["Jwt:Issuer"], _config["Jwt:Issuer"], expires: DateTime.Now.AddMinutes(30), signingCredentials: creds); return new JwtSecurityTokenHandler().WriteToken(token); } private UserViewModel Authenticate(LoginViewModel login) { UserViewModel user = null; if (login.username == "pablo" && login.password == "secret") { user = new UserViewModel { name = "Pablo" }; } return user; } } }
เมธอด BuildToken
จะสร้างโทเค็นด้วยรหัสความปลอดภัยที่กำหนด วิธีการ Authenticate
มีการตรวจสอบผู้ใช้แบบฮาร์ดโค้ดอยู่ชั่วขณะ แต่เราจะต้องเรียกฐานข้อมูลเพื่อตรวจสอบความถูกต้องในตอนท้าย
บริบทการสมัคร
การตั้งค่า Entity Framework นั้นง่ายมากตั้งแต่ Microsoft เปิดตัวรุ่น Core 2.0— EF Core 2 แบบสั้นๆ เราจะลงลึกในเชิงลึกด้วยโมเดลที่เน้นโค้ดเป็นอันดับแรกโดยใช้ identityDbContext
ดังนั้นก่อนอื่นให้แน่ใจว่าคุณได้ติดตั้งการพึ่งพาทั้งหมดแล้ว คุณสามารถใช้ NuGet เพื่อจัดการได้:
เมื่อใช้โปรเจ็กต์ Models
เราสามารถสร้างไฟล์สองไฟล์ในโฟลเดอร์ Context
ได้ที่นี่ ApplicationContext.cs
และ IApplicationContext.cs
นอกจากนี้ เราจะต้องมีคลาส EntityBase
ไฟล์ EntityBase
จะสืบทอดโดยแต่ละโมเดลเอนทิตี แต่ User.cs
เป็นคลาสเอกลักษณ์และเป็นเอนทิตีเดียวที่จะสืบทอดจาก IdentityUser
ด้านล่างนี้เป็นทั้งสองคลาส:
namespace SeedAPI.Models { public class User : IdentityUser { public string Name { get; set; } } }
namespace SeedAPI.Models.EntityBase { public class EntityBase { public DateTime? Created { get; set; } public DateTime? Updated { get; set; } public bool Deleted { get; set; } public EntityBase() { Deleted = false; } public virtual int IdentityID() { return 0; } public virtual object[] IdentityID(bool dummy = true) { return new List<object>().ToArray(); } } }
ตอนนี้เราพร้อมที่จะสร้าง ApplicationContext.cs
ซึ่งจะมีลักษณะดังนี้:
namespace SeedAPI.Models.Context { public class ApplicationContext : IdentityDbContext<User>, IApplicationContext { private IDbContextTransaction dbContextTransaction; public ApplicationContext(DbContextOptions options) : base(options) { } public DbSet<User> UsersDB { get; set; } public new void SaveChanges() { base.SaveChanges(); } public new DbSet<T> Set<T>() where T : class { return base.Set<T>(); } public void BeginTransaction() { dbContextTransaction = Database.BeginTransaction(); } public void CommitTransaction() { if (dbContextTransaction != null) { dbContextTransaction.Commit(); } } public void RollbackTransaction() { if (dbContextTransaction != null) { dbContextTransaction.Rollback(); } } public void DisposeTransaction() { if (dbContextTransaction != null) { dbContextTransaction.Dispose(); } } } }
เราสนิทกันมาก แต่ก่อนอื่น เราจะต้องสร้างคลาสเพิ่มเติม คราวนี้ในโฟลเดอร์ App_Start
ที่อยู่ในโครงการ Web.API
ชั้นหนึ่งคือการเริ่มต้นบริบทแอปพลิเคชันและชั้นที่สองคือการสร้างข้อมูลตัวอย่างเพียงเพื่อวัตถุประสงค์ในการทดสอบระหว่างการพัฒนา
namespace SeedAPI.Web.API.App_Start { public class DBContextConfig { public static void Initialize(IConfiguration configuration, IHostingEnvironment env, IServiceProvider svp) { var optionsBuilder = new DbContextOptionsBuilder(); if (env.IsDevelopment()) optionsBuilder.UseSqlServer(configuration.GetConnectionString("DefaultConnection")); else if (env.IsStaging()) optionsBuilder.UseSqlServer(configuration.GetConnectionString("DefaultConnection")); else if (env.IsProduction()) optionsBuilder.UseSqlServer(configuration.GetConnectionString("DefaultConnection")); var context = new ApplicationContext(optionsBuilder.Options); if(context.Database.EnsureCreated()) { IUserMap service = svp.GetService(typeof(IUserMap)) as IUserMap; new DBInitializeConfig(service).DataTest(); } } public static void Initialize(IServiceCollection services, IConfiguration configuration) { services.AddDbContext<ApplicationContext>(options => options.UseSqlServer(configuration.GetConnectionString("DefaultConnection"))); } } }
namespace SeedAPI.Web.API.App_Start { public class DBInitializeConfig { private IUserMap userMap; public DBInitializeConfig (IUserMap _userMap) { userMap = _userMap; } public void DataTest() { Users(); } private void Users() { userMap.Create(new UserViewModel() { id = 1, name = "Pablo" }); userMap.Create(new UserViewModel() { id = 2, name = "Diego" }); } } }
And we call them from our startup file:
// This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { DependencyInjectionConfig.AddScope(services); JwtTokenConfig.AddAuthentication(services, Configuration); DBContextConfig.Initialize(services, Configuration); 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, IServiceProvider svp) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } DBContextConfig.Initialize(Configuration, env, svp); app.UseCors(builder => builder .AllowAnyOrigin() .AllowAnyMethod() .AllowAnyHeader() .AllowCredentials()); app.UseAuthentication(); app.UseMvc(); }
การฉีดพึ่งพา
It is a good practice to use dependency injection to move among different projects. This will help us to communicate between controllers and mappers, mappers and services, and services and repositories.
Inside the folder App_Start
we will create the file DependencyInjectionConfig.cs
and it will look like this:
namespace SeedAPI.Web.API.App_Start { public class DependencyInjectionConfig { public static void AddScope(IServiceCollection services) { services.AddScoped<IApplicationContext, ApplicationContext>(); services.AddScoped<IUserMap, UserMap>(); services.AddScoped<IUserService, UserService>(); services.AddScoped<IUserRepository, UserRepository>(); } } }
We will need to create for each new entity a new Map
, Service
, and Repository
, and match them to this file. Then we just need to call it from the startup.cs
file:
// This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { DependencyInjectionConfig.AddScope(services); JwtTokenConfig.AddAuthentication(services, Configuration); DBContextConfig.Initialize(services, Configuration); services.AddMvc(); }
Finally, when we need to get the users list from the database, we can create a controller using this dependency injection:
namespace SeedAPI.Web.API.Controllers { [Route("api/[controller]")] [Authorize] public class UserController : Controller { IUserMap userMap; public UserController(IUserMap map) { userMap = map; } // GET api/user [HttpGet] public IEnumerable<UserViewModel> Get() { return userMap.GetAll(); ; } // GET api/user/5 [HttpGet("{id}")] public string Get(int id) { return "value"; } // POST api/user [HttpPost] public void Post([FromBody]string user) { } // PUT api/user/5 [HttpPut("{id}")] public void Put(int id, [FromBody]string user) { } // DELETE api/user/5 [HttpDelete("{id}")] public void Delete(int id) { } } }
Look how the Authorize
attribute is present here to be sure that the front end has logged in and how dependency injection works in the constructor of the class.
We finally have a call to the database but first, we need to understand the Map
project.
โครงการ Maps
ขั้นตอนนี้เป็นเพียงการแมป ViewModels
กับและจากโมเดลฐานข้อมูล เราต้องสร้างหนึ่งรายการสำหรับแต่ละเอนทิตี และตามตัวอย่างก่อนหน้าของเรา ไฟล์ UserMap.cs
จะมีลักษณะดังนี้:
namespace SeedAPI.Maps { public class UserMap : IUserMap { IUserService userService; public UserMap(IUserService service) { userService = service; } public UserViewModel Create(UserViewModel viewModel) { User user = ViewModelToDomain(viewModel); return DomainToViewModel(userService.Create(user)); } public bool Update(UserViewModel viewModel) { User user = ViewModelToDomain(viewModel); return userService.Update(user); } public bool Delete(int id) { return userService.Delete(id); } public List<UserViewModel> GetAll() { return DomainToViewModel(userService.GetAll()); } public UserViewModel DomainToViewModel(User domain) { UserViewModel model = new UserViewModel(); model.name = domain.Name; return model; } public List<UserViewModel> DomainToViewModel(List<User> domain) { List<UserViewModel> model = new List<UserViewModel>(); foreach (User of in domain) { model.Add(DomainToViewModel(of)); } return model; } public User ViewModelToDomain(UserViewModel officeViewModel) { User domain = new User(); domain.Name = officeViewModel.name; return domain; } } }
ดูเหมือนอีกครั้ง การฉีดการพึ่งพากำลังทำงานในตัวสร้างของคลาส โดยเชื่อมโยง Maps กับโปรเจ็กต์ Services
โครงการ Services
ไม่มีอะไรจะพูดมากที่นี่: ตัวอย่างของเรานั้นง่ายมาก และเราไม่มีตรรกะทางธุรกิจหรือรหัสที่จะเขียนที่นี่ โครงการนี้จะพิสูจน์ว่ามีประโยชน์ในข้อกำหนดขั้นสูงในอนาคต เมื่อเราต้องคำนวณหรือทำตรรกะบางอย่างก่อนหรือหลังขั้นตอนฐานข้อมูลหรือตัวควบคุม ตามตัวอย่าง คลาสจะดูโล่งมาก:
namespace SeedAPI.Services { public class UserService : IUserService { private IUserRepository repository; public UserService(IUserRepository userRepository) { repository = userRepository; } public User Create(User domain) { return repository.Save(domain); } public bool Update(User domain) { return repository.Update(domain); } public bool Delete(int id) { return repository.Delete(id); } public List<User> GetAll() { return repository.GetAll(); } } }
โครงการ Repositories
เรากำลังเข้าสู่ส่วนสุดท้ายของบทช่วยสอนนี้: เราเพียงแค่ทำการเรียกไปยังฐานข้อมูล ดังนั้นเราจึงสร้างไฟล์ UserRepository.cs
ที่ซึ่งเราสามารถอ่าน แทรก หรืออัปเดตผู้ใช้ในฐานข้อมูลได้
namespace SeedAPI.Repositories { public class UserRepository : BaseRepository, IUserRepository { public UserRepository(IApplicationContext context) : base(context) { } public User Save(User domain) { try { var us = InsertUser<User>(domain); return us; } catch (Exception ex) { //ErrorManager.ErrorHandler.HandleError(ex); throw ex; } } public bool Update(User domain) { try { //domain.Updated = DateTime.Now; UpdateUser<User>(domain); return true; } catch (Exception ex) { //ErrorManager.ErrorHandler.HandleError(ex); throw ex; } } public bool Delete(int id) { try { User user = Context.UsersDB.Where(x => x.Id.Equals(id)).FirstOrDefault(); if (user != null) { //Delete<User>(user); return true; } else { return false; } } catch (Exception ex) { //ErrorManager.ErrorHandler.HandleError(ex); throw ex; } } public List<User> GetAll() { try { return Context.UsersDB.OrderBy(x => x.Name).ToList(); } catch (Exception ex) { //ErrorManager.ErrorHandler.HandleError(ex); throw ex; } } } }
สรุป
ในบทความนี้ ฉันอธิบายวิธีสร้างสถาปัตยกรรมที่ดีโดยใช้ Angular 5 และ Web API Core 2 ณ จุดนี้ คุณได้สร้างฐานสำหรับโครงการขนาดใหญ่ที่มีโค้ดที่รองรับความต้องการที่เพิ่มขึ้นอย่างมาก
ความจริงก็คือ ไม่มีอะไรแข่งขันกับ JavaScript ในส่วนหน้า และอะไรสามารถแข่งขันกับ C# ได้ หากคุณต้องการการสนับสนุนจาก SQL Server และ Entity Framework ในส่วนหลัง ดังนั้น แนวคิดของบทความนี้คือการผสมผสานสิ่งที่ดีที่สุดของสองโลกเข้าด้วยกัน และฉันหวังว่าคุณจะสนุกกับมัน
อะไรต่อไป?
หากคุณกำลังทำงานในทีมของนักพัฒนา Angular อาจมีนักพัฒนาหลายคนที่ทำงานในส่วนหน้าและส่วนหลัง ดังนั้น ความคิดที่ดีที่จะประสานความพยายามของทั้งสองทีมอาจรวม Swagger กับ Web API 2 Swagger นั้นยอดเยี่ยม เครื่องมือในการจัดทำเอกสารและทดสอบ RESTFul API ของคุณ อ่านคู่มือของ Microsoft: เริ่มต้นใช้งาน Swashbuckle และ ASP.NET Core
หากคุณยังใหม่กับ Angular 5 และมีปัญหาในการติดตาม โปรดอ่านบทช่วยสอน Angular 5: คำแนะนำทีละขั้นตอนสำหรับแอป Angular 5 แรกของคุณโดยเพื่อน Toptaler Sergey Moiseev