Angular 5 i ASP.NET Core
Opublikowany: 2022-03-11Myślałem o napisaniu wpisu na blogu, odkąd pierwsza wersja Angulara praktycznie zabiła Microsoft po stronie klienta. Technologie takie jak ASP.Net, Web Forms i MVC Razor stały się przestarzałe, zastąpione przez framework JavaScript, który nie jest dokładnie Microsoft. Jednak od drugiej wersji Angulara Microsoft i Google współpracują ze sobą nad stworzeniem Angulara 2 i to właśnie wtedy moje dwie ulubione technologie zaczęły ze sobą współpracować.
Na tym blogu chcę pomóc ludziom stworzyć najlepszą architekturę łączącą te dwa światy. Jesteś gotowy? No to ruszamy!
O architekturze
Zbudujesz klienta Angular 5, który korzysta z usługi RESTful Web API Core 2.
Strona klienta:
- kątowe 5
- Kątowy CLI
- Materiał kątowy
Strona serwera:
- Rdzeń internetowego interfejsu API .NET C# 2
- Zależności wtrysku
- Uwierzytelnianie JWT
- Najpierw kod struktury encji
- Serwer SQL
Notatka
W tym poście zakładamy, że czytelnik ma już podstawową wiedzę na temat TypeScript, modułów Angular, komponentów oraz importowania/eksportowania. Celem tego posta jest stworzenie dobrej architektury, która pozwoli na rozwój kodu w czasie. |
Czego potrzebujesz?
Zacznijmy od wyboru IDE. Oczywiście to tylko moje preferencje i możesz użyć tego, z którym czujesz się bardziej komfortowo. W moim przypadku skorzystam z Visual Studio Code i Visual Studio 2017.
Dlaczego dwa różne IDE? Ponieważ Microsoft stworzył Visual Studio Code dla interfejsu użytkownika, nie mogę przestać używać tego IDE. W każdym razie zobaczymy również, jak zintegrować Angular 5 w projekcie rozwiązania, co pomoże ci, jeśli jesteś programistą, który woli debugować zarówno back-end, jak i front za pomocą tylko jednego klawisza F5.
Jeśli chodzi o zaplecze, możesz zainstalować najnowszą wersję programu Visual Studio 2017, która ma bezpłatną wersję dla programistów, ale jest bardzo kompletna: Społeczność.
Oto lista rzeczy, które musimy zainstalować w tym samouczku:
- Kod programu Visual Studio
- Społeczność programu Visual Studio 2017 (lub dowolna)
- Node.js v8.10.0
- Serwer SQL 2017
Notatka
Sprawdź, czy korzystasz z co najmniej Node 6.9.xi npm 3.xx, uruchamiając node -v i npm -v w oknie terminala lub konsoli. Starsze wersje generują błędy, ale nowsze wersje są w porządku. |
Frontend
Szybki start
Niech zabawa się zacznie! Pierwszą rzeczą, którą musimy zrobić, jest globalna instalacja Angular CLI, więc otwórz wiersz polecenia node.js i uruchom to polecenie:
npm install -g @angular/cli
Dobra, teraz mamy nasz modułowy pakiet. Zwykle instaluje to moduł w folderze użytkownika. Alias nie powinien być domyślnie potrzebny, ale jeśli go potrzebujesz, możesz wykonać następną linię:
alias ng="<UserFolder>/.npm/lib/node_modules/angular-cli/bin/ng"
Następnym krokiem jest stworzenie nowego projektu. Nazwę to angular5-app
. Najpierw przechodzimy do folderu, w którym chcemy stworzyć witrynę, a następnie:
ng new angular5-app
Pierwsza kompilacja
Chociaż możesz przetestować swoją nową witrynę, po prostu uruchamiając ng serve --open
, polecam przetestować witrynę z ulubionej usługi sieciowej. Czemu? Cóż, niektóre problemy mogą wystąpić tylko w środowisku produkcyjnym, a budowanie witryny za pomocą ng build
jest najbliższym sposobem podejścia do tego środowiska. Następnie możemy otworzyć folder angular5-app
za pomocą Visual Studio Code i uruchomić ng build
na bash terminala:
Zostanie utworzony nowy folder o nazwie dist
i możemy go obsłużyć za pomocą IIS lub dowolnego preferowanego serwera internetowego. Następnie możesz wpisać adres URL w przeglądarce i… gotowe!
Notatka
Celem tego samouczka nie jest pokazanie, jak skonfigurować serwer WWW, więc zakładam, że masz już tę wiedzę. |
Folder src
Mój folder src
ma następującą strukturę: Wewnątrz folderu app
mamy components
, w których dla każdego komponentu Angular utworzymy pliki css
, ts
, spec
i html
. Stworzymy również folder config
, aby zachować konfigurację witryny, directives
będą zawierały wszystkie nasze niestandardowe dyrektywy, helpers
będą zawierały wspólny kod, taki jak menedżer uwierzytelniania, layout
będzie zawierał główne komponenty, takie jak body, head i panele boczne, models
zachowają to, co będzie dopasuj do modeli widoku zaplecza, a wreszcie services
będą miały kod dla wszystkich wywołań zaplecza.
Poza folderem app
zachowamy foldery utworzone domyślnie, takie jak assets
i environments
, a także pliki główne.
Tworzenie pliku konfiguracyjnego
Utwórzmy plik config.ts
w naszym folderze config
i wywołajmy klasę AppConfig
. Tutaj możemy ustawić wszystkie wartości, których będziemy używać w różnych miejscach naszego kodu; na przykład adres URL interfejsu API. Zauważ, że klasa implementuje właściwość get
, która jako parametr otrzymuje strukturę klucz/wartość oraz prostą metodę uzyskania dostępu do tej samej wartości. W ten sposób łatwo będzie uzyskać wartości wywołujące po prostu this.config.setting['PathAPI']
z klas, które po nim dziedziczą.
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]; } };
Materiał kątowy
Przed rozpoczęciem tworzenia układu skonfigurujmy strukturę komponentów interfejsu użytkownika. Oczywiście możesz użyć innych, takich jak Bootstrap, ale jeśli podoba Ci się stylizacja materiału, polecam go, ponieważ jest również obsługiwany przez Google.
Aby go zainstalować, wystarczy uruchomić kolejne trzy polecenia, które możemy wykonać na terminalu Visual Studio Code:
npm install --save @angular/material @angular/cdk npm install --save @angular/animations npm install --save hammerjs
Drugie polecenie jest spowodowane tym, że niektóre komponenty materiału zależą od animacji kątowych. Polecam również przeczytać oficjalną stronę, aby zrozumieć, które przeglądarki są obsługiwane i czym jest wypełnienie.
Trzecie polecenie jest spowodowane tym, że niektóre komponenty Material opierają się na HammerJS dla gestów.
Teraz możemy przystąpić do importowania modułów komponentów, których chcemy użyć w naszym pliku 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 ],
Następnym krokiem jest zmiana pliku style.css
, dodając rodzaj motywu, którego chcesz użyć:
@import "~@angular/material/prebuilt-themes/deeppurple-amber.css";
Teraz zaimportuj HammerJS, dodając tę linię w pliku main.ts
:
import 'hammerjs';
I na koniec brakuje nam tylko dodania ikon Material do index.html
, w sekcji head:
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
Układ
W tym przykładzie stworzymy prosty układ, taki jak ten:
Chodzi o to, aby otworzyć/ukryć menu, klikając przycisk w nagłówku. Angular Responsive zrobi za nas resztę pracy. W tym celu utworzymy folder layout
i umieścimy w nim utworzone domyślnie pliki app.component
. Ale utworzymy również te same pliki dla każdej sekcji układu, jak widać na następnym obrazku. Następnie app.component
będzie treścią, head.component
nagłówkiem, a left-panel.component
menu.
Teraz app.component.html
w następujący sposób:
<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>
Zasadniczo będziemy mieli w komponencie właściwość authentication
, która pozwoli nam usunąć nagłówek i menu, jeśli użytkownik nie jest zalogowany, i zamiast tego wyświetlić prostą stronę logowania.
Plik head.component.html
wygląda tak:
<h1>{{title}}</h1> <button mat-button [routerLink]=" ['./logout'] ">Logout!</button>
Tylko przycisk do wylogowania użytkownika — wrócimy do tego później. Jeśli chodzi o left-panel.component.html
, na razie po prostu zmień kod HTML na:
<nav> <a routerLink="/dashboard">Dashboard</a> <a routerLink="/users">Users</a> </nav>
Postawiliśmy na prostotę: do tej pory są to tylko dwa linki do poruszania się po dwóch różnych stronach. (Wrócimy do tego również później.)
Teraz tak wyglądają pliki TypeScript nagłówka i komponentu po lewej stronie:
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'; }
Ale co z kodem TypeScript dla app.component
? Zostawimy tu małą tajemnicę, zatrzymamy ją na chwilę i wrócimy do tego po wdrożeniu uwierzytelniania.
Rozgromienie
Dobra, teraz mamy Angular Material, który pomaga nam w interfejsie użytkownika i prostym układzie, aby rozpocząć tworzenie naszych stron. Ale jak możemy nawigować między stronami?
Aby stworzyć prosty przykład, stwórzmy dwie strony: „Użytkownik”, gdzie możemy uzyskać listę istniejących użytkowników w bazie danych, oraz „Dashboard”, stronę, na której możemy wyświetlić statystyki.
Wewnątrz folderu app
utworzymy plik o nazwie app-routing.modules.ts
, który wygląda tak:
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 {}
To takie proste: po prostu importując RouterModule
i Routes
z @angular/router
, możemy mapować ścieżki, które chcemy zaimplementować. Tutaj tworzymy cztery ścieżki:
-
/dashboard
: Nasza strona główna -
/login
: Strona, na której użytkownik może się uwierzytelnić -
/logout
: Prosta ścieżka do wylogowania użytkownika -
/users
: Nasza pierwsza strona, na której chcemy wyświetlić listę użytkowników z zaplecza
Pamiętaj, że dashboard
jest domyślnie naszą stroną, więc jeśli użytkownik wpisze adres URL /
, strona automatycznie przekieruje na tę stronę. Przyjrzyj się również parametrowi canActivate
: Tutaj tworzymy referencję do klasy AuthGuard
, która pozwoli nam sprawdzić, czy użytkownik jest zalogowany. Jeśli nie, przekierowuje do strony logowania. W kolejnej sekcji pokażę, jak stworzyć tę klasę.
Teraz wystarczy tylko stworzyć menu. Pamiętasz w sekcji układu, kiedy utworzyliśmy plik left-panel.component.html
, który wyglądał tak?
<nav> <a routerLink="/dashboard">Dashboard</a> <a routerLink="/users">Users</a> </nav>
Tutaj nasz kod spotyka się z rzeczywistością. Teraz możemy zbudować kod i przetestować go w adresie URL: powinieneś być w stanie przejść ze strony pulpitu nawigacyjnego do użytkowników, ale co się stanie, jeśli wpiszesz adres URL our.site.url/users
bezpośrednio w przeglądarce?
Pamiętaj, że ten błąd pojawia się również, jeśli odświeżysz przeglądarkę po pomyślnym przejściu do tego adresu URL za pomocą panelu bocznego aplikacji. Aby zrozumieć ten błąd, pozwólcie, że odwołam się do oficjalnych dokumentów, gdzie jest to naprawdę jasne:
Aplikacja routowana powinna obsługiwać precyzyjne linki. Precyzyjny link to adres URL, który określa ścieżkę do komponentu w aplikacji. Na przykład
http://www.mysite.com/users/42
to precyzyjny link do strony szczegółów bohatera, która wyświetla bohatera o identyfikatorze: 42.Nie ma problemu, gdy użytkownik przechodzi do tego adresu URL z uruchomionego klienta. Router Angular interpretuje adres URL i kieruje do tej strony i bohatera.
Ale kliknięcie łącza w wiadomości e-mail, wpisanie go w pasku adresu przeglądarki lub po prostu odświeżenie przeglądarki na stronie szczegółów bohatera — wszystkie te działania są obsługiwane przez samą przeglądarkę, poza uruchomioną aplikacją. Przeglądarka wysyła bezpośrednie żądanie do serwera o ten adres URL, z pominięciem routera.Serwer statyczny rutynowo zwraca index.html po otrzymaniu żądania dotyczącego
http://www.mysite.com/
. Ale odrzucahttp://www.mysite.com/users/42
i zwraca błąd 404 — Nie znaleziono, chyba że jest skonfigurowany do zwracania index.html.
Rozwiązanie tego problemu jest bardzo proste, wystarczy utworzyć konfigurację pliku usługodawcy. Ponieważ pracuję tutaj z IIS, pokażę ci, jak to zrobić w tym środowisku, ale koncepcja jest podobna dla Apache lub dowolnego innego serwera WWW.
Dlatego tworzymy plik wewnątrz folderu src
o nazwie web.config
, który wygląda tak:
<?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>
Następnie musimy mieć pewność, że ten zasób zostanie skopiowany do wdrożonego folderu. Wszystko, co musimy zrobić, to zmienić nasz plik ustawień 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": {} } }
Uwierzytelnianie
Czy pamiętasz, jak zaimplementowaliśmy klasę AuthGuard
do ustawiania konfiguracji routingu? Za każdym razem, gdy przechodzimy na inną stronę, użyjemy tej klasy do sprawdzenia, czy użytkownik jest uwierzytelniony za pomocą tokena. Jeśli nie, automatycznie przekierujemy do strony logowania. Plik do tego to canActivateAuthGuard.ts
— utwórz go w folderze helpers
i spraw, aby wyglądał tak:
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; } }
Tak więc za każdym razem, gdy zmienimy stronę, zostanie wywołana metoda canActivate
, która sprawdzi, czy użytkownik jest uwierzytelniony, a jeśli nie, użyjemy naszej instancji Router
do przekierowania na stronę logowania. Ale czym jest ta nowa metoda w klasie Helper
? W folderze helpers
utwórzmy plik helpers.ts
. Tutaj musimy zarządzać localStorage
, gdzie będziemy przechowywać token, który otrzymamy z zaplecza.
Notatka
Jeśli chodzi o localStorage , możesz również użyć plików cookie lub sessionStorage , a decyzja będzie zależeć od zachowania, które chcemy wdrożyć. Jak sama nazwa wskazuje, sessionStorage jest dostępne tylko na czas trwania sesji przeglądarki i jest usuwane po zamknięciu karty lub okna; jednak przetrwa ponowne ładowanie strony. Jeśli dane, które przechowujesz, muszą być dostępne na bieżąco, lepszym rozwiązaniem jest localStorage niż sessionStorage . Pliki cookie służą głównie do odczytu po stronie serwera, podczas gdy localStorage można odczytać tylko po stronie klienta. Więc pytanie brzmi, kto w Twojej aplikacji potrzebuje tych danych — klient czy serwer? |
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()); } }
Czy nasz kod uwierzytelniający ma teraz sens? Wrócimy do zajęć Subject
później, ale teraz wróćmy na chwilę do konfiguracji routingu. Spójrz na tę linię:
{ path: 'logout', component: LogoutComponent},
To jest nasz komponent do wylogowania się z witryny i jest to po prostu prosta klasa do wyczyszczenia localStorage
. Stwórzmy go w folderze components/login
o nazwie 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']); } }
Tak więc za każdym razem, gdy przejdziemy do adresu URL /logout
, localStorage
zostanie usunięty, a witryna przekieruje do strony logowania. Na koniec login.component.ts
w ten sposób:
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']); }); } }
Jak widać, na razie zakodowaliśmy tutaj nasze dane uwierzytelniające. Zauważ, że tutaj nazywamy klasę usług; utworzymy te klasy usług, aby uzyskać dostęp do naszego zaplecza w następnej sekcji.
Na koniec musimy wrócić do pliku app.component.ts
, układu strony. Tutaj, jeśli użytkownik jest uwierzytelniony, pokaże sekcje menu i nagłówka, ale jeśli nie, układ zmieni się i pokaże tylko naszą stronę logowania.

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(); } }
Pamiętasz klasę Subject
w naszej klasie pomocniczej? To jest Observable
. Observable
s zapewniają obsługę przekazywania komunikatów między wydawcami a subskrybentami w Twojej aplikacji. Za każdym razem, gdy zmienia się token uwierzytelniania, właściwość authentication
zostanie zaktualizowana. Przeglądając plik app.component.html
, prawdopodobnie będzie to miało teraz większy sens:
<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>
Usługi
W tym momencie przechodzimy do różnych stron, uwierzytelniamy naszą stronę klienta i renderujemy bardzo prosty układ. Ale jak możemy uzyskać dane z zaplecza? Zdecydowanie zalecam wykonywanie wszystkich dostępów zaplecza w szczególności z klas usług . Nasza pierwsza usługa będzie znajdować się w folderze services
o nazwie 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) ); } }
Pierwsze wywołanie zaplecza to wywołanie POST do interfejsu API tokena. Token API nie potrzebuje ciągu tokena w nagłówku, ale co się stanie, jeśli wywołamy inny punkt końcowy? Jak widać tutaj, TokenService
(i ogólnie klasy usług) dziedziczą po klasie BaseService
. Rzućmy okiem na to:
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); } }
Tak więc za każdym razem, gdy wykonujemy wywołanie HTTP, implementujemy nagłówek żądania po prostu za pomocą super.header
. Jeśli token jest w localStorage
, zostanie dołączony wewnątrz nagłówka, ale jeśli nie, po prostu ustawimy format JSON. Inną rzeczą, którą możemy tutaj zobaczyć, jest to, co się stanie, jeśli uwierzytelnienie się nie powiedzie.
Komponent logowania wywoła klasę usługi, a klasa usługi wywoła backend. Gdy już będziemy mieć token, klasa pomocnicza będzie zarządzać tokenem, a teraz jesteśmy gotowi do pobrania listy użytkowników z naszej bazy danych.
Aby uzyskać dane z bazy danych, najpierw upewnij się, że w naszej odpowiedzi dopasowujemy klasy modeli do modeli widoku zaplecza.
W user.ts
:
export class User { id: number; name: string; }
I możemy teraz stworzyć plik 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)); }
Zaplecze
Szybki start
Witamy w pierwszym kroku naszej aplikacji Web API Core 2. Pierwszą rzeczą, jakiej potrzebujemy, jest utworzenie aplikacji internetowej ASP.Net Core, którą nazwiemy SeedAPI.Web.API
.
Pamiętaj, aby wybrać pusty szablon, aby rozpocząć czysty start, jak widać poniżej:
To wszystko, rozwiązanie tworzymy zaczynając od pustej aplikacji webowej. Teraz nasza architektura będzie taka, jak wymieniliśmy poniżej, więc będziemy musieli tworzyć różne projekty:
Aby to zrobić, dla każdego z nich kliknij prawym przyciskiem myszy rozwiązanie i dodaj projekt „Biblioteka klas (.NET Core)”.
Architektura
W poprzednim rozdziale stworzyliśmy osiem projektów, ale do czego one służą? Oto prosty opis każdego z nich:
-
Web.API
: To jest nasz projekt startowy i miejsce, w którym tworzone są punkty końcowe. Tutaj skonfigurujemy JWT, zależności wstrzykiwania i kontrolery. -
ViewModels
: Tutaj wykonujemy konwersje z typu danych, które kontrolery zwrócą w odpowiedziach do interfejsu. Dobrą praktyką jest dopasowanie tych klas do modeli front-end. -
Interfaces
: będzie to pomocne w implementacji zależności wstrzykiwania. Atrakcyjną zaletą języka z typami statycznymi jest to, że kompilator może pomóc w sprawdzeniu, czy kontrakt, na którym opiera się Twój kod, jest rzeczywiście spełniony. -
Commons
: Wszystkie wspólne zachowania i kod narzędziowy będą tutaj. -
Models
: Dobrą praktyką jest nie dopasowywanie bazy danych bezpośrednio do interfejsuViewModels
, dlatego celemModels
jest tworzenie klas bazy danych encji niezależnych od interfejsu. To pozwoli nam w przyszłości zmienić naszą bazę danych bez konieczności wywierania wpływu na nasz interfejs. Pomaga również, gdy po prostu chcemy dokonać refaktoryzacji. -
Maps
: tutajViewModels
naModels
i na odwrót. Ten krok jest wywoływany między kontrolerami a usługami. -
Services
: Biblioteka do przechowywania całej logiki biznesowej. -
Repositories
: to jedyne miejsce, w którym nazywamy bazę danych.
Referencje będą wyglądać tak:
Uwierzytelnianie oparte na JWT
W tej sekcji przyjrzymy się podstawowej konfiguracji uwierzytelniania tokenem i zagłębimy się w temat bezpieczeństwa.
Aby rozpocząć ustawianie tokenu internetowego JSON (JWT), utwórzmy następną klasę w folderze App_Start
o nazwie JwtTokenConfig.cs
. Kod w środku będzie wyglądał tak:
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(); }); } } }
Wartości parametrów walidacji będą zależeć od wymagań każdego projektu. Prawidłowego użytkownika i odbiorców możemy ustawić czytając plik konfiguracyjny appsettings.json
:
"Jwt": { "Key": "veryVerySecretKey", "Issuer": "http://localhost:50498/" }
Następnie wystarczy wywołać go z metody ConfigureServices
w 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(); }
Teraz jesteśmy gotowi do stworzenia naszego pierwszego kontrolera o nazwie TokenController.cs
. Wartość, którą ustawiliśmy w appsettings.json
na "veryVerySecretKey"
powinna odpowiadać tej, której używamy do utworzenia tokena, ale najpierw stwórzmy LoginViewModel
wewnątrz naszego projektu ViewModels
:
namespace SeedAPI.ViewModels { public class LoginViewModel : IBaseViewModel { public string username { get; set; } public string password { get; set; } } }
I wreszcie kontroler:
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; } } }
Metoda BuildToken
utworzy token z podanym kodem zabezpieczającym. Metoda Authenticate
ma na razie zakodowaną walidację użytkownika, ale będziemy musieli wywołać bazę danych, aby ostatecznie ją zweryfikować.
Kontekst aplikacji
Konfigurowanie Entity Framework jest naprawdę łatwe, ponieważ firma Microsoft uruchomiła wersję Core 2.0 — w skrócie EF Core 2 . Zagłębimy się w model „najpierw kod” przy użyciu identityDbContext
, więc najpierw upewnij się, że zainstalowałeś wszystkie zależności. Możesz użyć NuGet do zarządzania nim:
Korzystając z projektu Models
możemy stworzyć tutaj w folderze Context
dwa pliki ApplicationContext.cs
oraz IApplicationContext.cs
. Będziemy też potrzebować klasy EntityBase
.
Pliki EntityBase
będą dziedziczone przez każdy model jednostki, ale User.cs
jest klasą tożsamości i jedyną jednostką, która będzie dziedziczyć po IdentityUser
. Poniżej znajdują się obie klasy:
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(); } } }
Teraz jesteśmy gotowi do stworzenia ApplicationContext.cs
, który będzie wyglądał tak:
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(); } } } }
Jesteśmy bardzo blisko, ale najpierw będziemy musieli stworzyć więcej klas, tym razem w folderze App_Start
znajdującym się w projekcie Web.API
. Pierwsza klasa to inicjalizacja kontekstu aplikacji, druga to tworzenie przykładowych danych tylko na potrzeby testowania w trakcie rozwoju.
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(); }
Wstrzykiwanie zależności
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.
Projekt Maps
Ten krok to tylko mapowanie ViewModels
do iz modeli baz danych. Musimy stworzyć po jednym dla każdej encji i zgodnie z naszym poprzednim przykładem plik UserMap.cs
będzie wyglądał tak:
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; } } }
Wygląda na to, że po raz kolejny dependency injection działa w konstruktorze klasy, łącząc Mapy z projektem Services.
Projekt Services
Nie ma tu zbyt wiele do powiedzenia: nasz przykład jest naprawdę prosty i nie mamy tutaj logiki biznesowej ani kodu do napisania. Ten projekt okazałby się przydatny w przyszłych zaawansowanych wymaganiach, gdy musimy obliczyć lub wykonać jakąś logikę przed lub po krokach bazy danych lub kontrolera. Idąc za przykładem, klasa będzie wyglądać całkiem goło:
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(); } } }
Projekt Repositories
Przechodzimy do ostatniej części tego samouczka: Musimy tylko wykonać wywołania do bazy danych, więc tworzymy plik UserRepository.cs
, w którym możemy odczytywać, wstawiać lub aktualizować użytkowników w bazie danych.
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; } } } }
Streszczenie
W tym artykule wyjaśniłem, jak stworzyć dobrą architekturę przy użyciu Angular 5 i Web API Core 2. W tym momencie stworzyłeś bazę dla dużego projektu z kodem, który obsługuje duży wzrost wymagań.
Prawda jest taka, że nic nie konkuruje z JavaScript w interfejsie, a co może konkurować z C#, jeśli potrzebujesz wsparcia SQL Server i Entity Framework w zapleczu? Więc ideą tego artykułu było połączenie tego, co najlepsze z dwóch światów i mam nadzieję, że Ci się podobało.
Co dalej?
Jeśli pracujesz w zespole programistów Angulara, prawdopodobnie w interfejsie i zapleczu mogą pracować różni programiści, więc dobrym pomysłem na zsynchronizowanie wysiłków obu zespołów może być integracja Swaggera z Web API 2. Swagger jest świetny narzędzie do dokumentowania i testowania interfejsów API RESTFul. Przeczytaj przewodnik firmy Microsoft: Zacznij korzystać z Swashbuckle i ASP.NET Core.
Jeśli nadal jesteś nowy w Angular 5 i masz problemy z podążaniem dalej, przeczytaj Samouczek Angular 5: Przewodnik krok po kroku do Twojej pierwszej aplikacji Angular 5 autorstwa innego Toptalera, Sergeya Moiseeva.