Angular 5 및 ASP.NET Core

게시 됨: 2022-03-11

Angular의 첫 번째 버전이 클라이언트 측에서 Microsoft를 사실상 죽인 이후로 블로그 게시물을 작성하는 것에 대해 생각했습니다. ASP.Net, Web Forms 및 MVC Razor와 같은 기술은 더 이상 사용되지 않으며 정확히 Microsoft가 아닌 JavaScript 프레임워크로 대체되었습니다. 그러나 Angular의 두 번째 버전 이후로 Microsoft와 Google은 Angular 2를 만들기 위해 협력해 왔으며 이때부터 제가 가장 좋아하는 두 기술이 함께 작동하기 시작했습니다.

이 블로그에서 저는 사람들이 이 두 세계를 결합한 최고의 아키텍처를 만드는 데 도움을 주고 싶습니다. 준비 되었나요? 여기 우리가 간다!

건축에 대하여

RESTful Web API Core 2 서비스를 사용하는 Angular 5 클라이언트를 빌드합니다.

클라이언트 측:

  • 각도 5
  • 앵귤러 CLI
  • 앵귤러 머티리얼

서버 측:

  • .NET C# 웹 API 코어 2
  • 주입 종속성
  • JWT 인증
  • 엔터티 프레임워크 코드 우선
  • SQL 서버

메모

이 블로그 게시물에서는 독자가 이미 TypeScript, Angular 모듈, 구성 요소 및 가져오기/내보내기에 대한 기본 지식을 가지고 있다고 가정합니다. 이 게시물의 목표는 시간이 지남에 따라 코드가 성장할 수 있는 좋은 아키텍처를 만드는 것입니다.

뭐가 필요하세요?

IDE를 선택하여 시작하겠습니다. 물론 이건 제 취향일 뿐이니 편하신 방법으로 사용하시면 됩니다. 제 경우에는 Visual Studio Code와 Visual Studio 2017을 사용하겠습니다.

두 개의 다른 IDE가 필요한 이유는 무엇입니까? Microsoft가 프런트 엔드용 Visual Studio Code를 만들었기 때문에 이 IDE 사용을 멈출 수 없습니다. 어쨌든, 우리는 또한 솔루션 프로젝트 내부에 Angular 5를 통합하는 방법을 볼 것입니다. 이는 단 하나의 F5로 백엔드와 전면을 모두 디버그하는 것을 선호하는 개발자라면 도움이 될 것입니다.

백엔드에 관해서는 개발자를 위한 무료 버전이 있지만 매우 완전한 최신 Visual Studio 2017 버전인 Community를 설치할 수 있습니다.

따라서 이 튜토리얼을 위해 설치해야 하는 항목의 목록은 다음과 같습니다.

  • 비주얼 스튜디오 코드
  • Visual Studio 2017 커뮤니티(또는 모든)
  • Node.js v8.10.0
  • SQL 서버 2017

메모

터미널 또는 콘솔 창에서 node -vnpm -v 를 실행하여 최소 Node 6.9.x 및 npm 3.xx를 실행하고 있는지 확인합니다. 이전 버전은 오류를 생성하지만 최신 버전은 괜찮습니다.

프런트 엔드

빠른 시작

재미를 시작하자! 가장 먼저 해야 할 일은 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 로 사이트를 구축하는 것이 이 환경에 접근하는 가장 가까운 방법입니다. 그런 다음 Visual Studio Code로 angular5-app 폴더를 열고 터미널 bash에서 ng build 를 실행할 수 있습니다.

처음으로 Angular 앱 빌드

dist 라는 새 폴더가 생성되고 IIS 또는 원하는 웹 서버를 사용하여 이를 제공할 수 있습니다. 그런 다음 브라우저에 URL을 입력하고...완료!

새로운 디렉토리 구조

메모

웹 서버를 설정하는 방법을 보여주는 것이 이 자습서의 목적이 아니므로 이미 해당 지식이 있다고 가정합니다.

Angular 5 시작 화면

src 폴더

src 폴더 구조

src 폴더는 다음과 같이 구성되어 있습니다. app 폴더 안에는 각 Angular 구성 요소에 대해 css , ts , spechtml 파일을 생성할 components 가 있습니다. 또한 사이트 구성을 유지하기 위한 config 폴더를 생성하고, directives 에는 모든 사용자 정의 지시문이 포함되며, helpers 에는 인증 관리자와 같은 공통 코드가 포함되고, layout 에는 본문, 헤드 및 측면 패널과 같은 주요 구성 요소가 포함되고, models 은 다음 내용을 유지합니다. 백엔드 뷰 모델과 일치하고 마지막으로 services 에는 백엔드에 대한 모든 호출에 대한 코드가 있습니다.

app 폴더 외부에는 assetsenvironments 과 같이 기본적으로 생성된 폴더와 루트 파일이 유지됩니다.

구성 파일 생성

config 폴더 안에 config.ts 파일을 만들고 AppConfig 클래스를 호출합시다. 여기에서 코드의 다른 위치에서 사용할 모든 값을 설정할 수 있습니다. 예를 들어 API의 URL입니다. 클래스는 매개변수로 키/값 구조와 동일한 값에 대한 액세스를 얻기 위한 간단한 메소드를 수신하는 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의 스타일링이 마음에 든다면 구글에서도 지원하기 때문에 추천합니다.

설치하려면 Visual Studio Code 터미널에서 실행할 수 있는 다음 세 가지 명령만 실행하면 됩니다.

 npm install --save @angular/material @angular/cdk npm install --save @angular/animations npm install --save hammerjs

두 번째 명령은 일부 Material 구성 요소가 Angular Animations에 의존하기 때문입니다. 또한 지원되는 브라우저와 폴리필이 무엇인지 이해하려면 공식 페이지를 읽는 것이 좋습니다.

세 번째 명령은 일부 Material 구성 요소가 제스처에 대해 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";

이제 main.ts 파일에 다음 줄을 추가하여 HammerJS를 가져옵니다.

 import 'hammerjs';

마지막으로 우리가 놓치고 있는 것은 머티리얼 아이콘을 head 섹션 안에 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'; }

하지만 app.component 용 TypeScript 코드는 어떻습니까? 여기에 약간의 미스터리를 남겨두고 잠시 멈췄다가 인증을 구현한 후 다시 돌아올 것입니다.

라우팅

자, 이제 Angular Material이 UI와 간단한 레이아웃으로 페이지 구축을 시작하도록 도와줍니다. 하지만 페이지 사이를 어떻게 이동할 수 있습니까?

간단한 예제를 만들기 위해 데이터베이스의 기존 사용자 목록을 얻을 수 있는 "User"와 일부 통계를 표시할 수 있는 페이지인 "Dashboard"의 두 페이지를 만들어 보겠습니다.

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 {}

간단합니다. @angular/router 에서 RouterModuleRoutes 를 가져오기만 하면 구현하려는 경로를 매핑할 수 있습니다. 여기에서 4개의 경로를 생성합니다.

  • /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에 대해 서버에 직접 요청합니다.

정적 서버는 http://www.mysite.com/ 에 대한 요청을 받으면 정기적으로 index.html을 반환합니다. 그러나 http://www.mysite.com/users/42 를 거부하고 index.html을 대신 반환하도록 구성하지 않는 한 404 - 찾을 수 없음 오류를 반환합니다.

이 문제를 해결하는 것은 매우 간단합니다. 서비스 공급자 파일 구성을 생성하기만 하면 됩니다. 여기서는 IIS로 작업하고 있으므로 이 환경에서 수행하는 방법을 보여주지만 개념은 Apache 또는 다른 웹 서버와 유사합니다.

그래서 web.config 라는 src 폴더 안에 다음과 같은 파일을 만듭니다.

 <?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 는 브라우저 세션 동안에만 사용할 수 있으며 탭이나 창이 닫힐 때 삭제됩니다. 그러나 페이지를 다시 로드해도 살아남습니다. 저장 중인 데이터를 지속적으로 사용할 수 있어야 하는 경우 localStoragesessionStorage 보다 선호됩니다. 쿠키는 주로 서버 측 읽기용인 반면 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 클래스를 기억하십니까? 이것은 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>

서비스

이 시점에서 우리는 다른 페이지로 이동하고 클라이언트 측을 인증하고 매우 간단한 레이아웃을 렌더링합니다. 그러나 어떻게 백엔드에서 데이터를 얻을 수 있습니까? 특히 서비스 클래스에서 모든 백엔드 액세스를 수행하는 것이 좋습니다. 첫 번째 서비스는 token.service.ts 라는 services 폴더 안에 있습니다.

 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) ); } }

백엔드에 대한 첫 번째 호출은 토큰 API에 대한 POST 호출입니다. 토큰 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)); }

뒷머리

빠른 시작

Web API Core 2 애플리케이션의 첫 번째 단계에 오신 것을 환영합니다. 가장 먼저 필요한 것은 SeedAPI.Web.API 라고 하는 ASP.Net Core 웹 애플리케이션을 만드는 것입니다.

새 파일 만들기

아래에서 볼 수 있는 것처럼 깨끗한 시작을 위해 빈 템플릿을 선택해야 합니다.

빈 템플릿 선택

그게 다야, 우리는 빈 웹 애플리케이션으로 시작하는 솔루션을 만듭니다. 이제 아키텍처는 아래 목록과 같으므로 다른 프로젝트를 만들어야 합니다.

우리의 현재 아키텍처

이렇게 하려면 각각에 대해 솔루션을 마우스 오른쪽 버튼으로 클릭하고 "클래스 라이브러리(.NET Core)" 프로젝트를 추가하기만 하면 됩니다.

"클래스 라이브러리(.NET Core)" 추가

아키텍처

이전 섹션에서 우리는 8개의 프로젝트를 만들었습니다. 하지만 그것들은 무엇을 위한 것입니까? 각각에 대한 간단한 설명은 다음과 같습니다.

  • Web.API : 이것은 우리의 시작 프로젝트이자 끝점이 생성되는 곳입니다. 여기에서 JWT, 주입 종속성 및 컨트롤러를 설정합니다.
  • ViewModels : 여기에서 컨트롤러가 프런트 엔드에 대한 응답으로 반환할 데이터 유형에서 변환을 수행합니다. 이러한 클래스를 프런트 엔드 모델과 일치시키는 것이 좋습니다.
  • Interfaces : 이것은 주입 의존성을 구현하는데 도움이 될 것입니다. 정적으로 형식화된 언어의 강력한 이점은 컴파일러가 코드가 의존하는 계약이 실제로 충족되는지 확인하는 데 도움이 될 수 있다는 것입니다.
  • Commons : 모든 공유 동작 및 유틸리티 코드가 여기에 있습니다.
  • Models : 데이터베이스를 프론트 엔드를 향한 ViewModels 와 직접 일치시키지 않는 것이 좋습니다. 따라서 Models 의 목적은 프론트 엔드와 독립적으로 엔터티 데이터베이스 클래스를 만드는 것입니다. 그래야 앞으로 프론트 엔드에 영향을 주지 않고 데이터베이스를 변경할 수 있습니다. 단순히 리팩토링을 하고 싶을 때도 도움이 됩니다.
  • Maps : 여기에서 ViewModelsModels 에 매핑하거나 그 반대로 매핑합니다. 이 단계는 컨트롤러와 서비스 간에 호출됩니다.
  • Services : 모든 비즈니스 로직을 저장하는 라이브러리.
  • Repositories : 이것은 우리가 데이터베이스를 호출하는 유일한 장소입니다.

참조는 다음과 같습니다.

참조 다이어그램

JWT 기반 인증

이 섹션에서는 토큰 인증의 기본 구성을 살펴보고 보안 주제에 대해 좀 더 자세히 알아보겠습니다.

JSON 웹 토큰(JWT) 설정을 시작하려면 JwtTokenConfig.cs라는 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/" }

그런 다음 startup.csConfigureServices 메서드에서 호출하기만 하면 됩니다.

 // 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" 로 설정한 값은 토큰을 생성하는 데 사용하는 값과 일치해야 하지만 먼저 ViewModels 프로젝트 내부에 LoginViewModel 을 생성해 보겠습니다.

 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 메서드에는 현재 하드 코딩된 사용자 유효성 검사가 있지만 결국에는 유효성을 검사하기 위해 데이터베이스를 호출해야 합니다.

애플리케이션 컨텍스트

Microsoft가 Core 2.0 버전(약칭 EF Core 2 )을 출시한 이후로 Entity Framework를 설정하는 것은 정말 쉽습니다. 우리는 identityDbContext 를 사용하는 코드 우선 모델에 대해 깊이 있게 다룰 것이므로 먼저 모든 종속성을 설치했는지 확인하십시오. NuGet을 사용하여 관리할 수 있습니다.

종속성 가져오기

Models 프로젝트를 사용하여 여기 Context 폴더 안에 ApplicationContext.csIApplicationContext.cs 라는 두 개의 파일을 만들 수 있습니다. 또한 EntityBase 클래스가 필요합니다.

클래스

EntityBase 파일은 각 엔터티 모델에서 상속되지만 User.cs 는 ID 클래스이며 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(); } } } }

우리는 정말 가깝지만 먼저 Web.API 프로젝트에 있는 App_Start 폴더에 더 많은 클래스를 만들어야 합니다. 첫 번째 클래스는 애플리케이션 컨텍스트를 초기화하는 것이고 두 번째 클래스는 개발 중 테스트를 목적으로 샘플 데이터를 생성하는 것입니다.

 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와 경쟁할 수 있는 것은 없으며 백 엔드에서 SQL Server 및 Entity Framework의 지원이 필요한 경우 C#과 경쟁할 수 있는 것은 무엇입니까? 따라서 이 기사의 아이디어는 두 세계의 장점을 결합하는 것이었으며 여러분이 그것을 즐겼기를 바랍니다.

무엇 향후 계획?

Angular 개발자 팀에서 작업하는 경우 프런트 엔드와 백엔드에서 작업하는 다른 개발자가 있을 수 있으므로 두 팀의 노력을 동기화하는 좋은 아이디어는 Swagger를 Web API 2와 통합할 수 있습니다. Swagger는 훌륭합니다. RESTFul API를 문서화하고 테스트하는 도구입니다. Microsoft 가이드 읽기: Swashbuckle 및 ASP.NET Core 시작하기.

여전히 Angular 5를 처음 접하고 따라 하는 데 문제가 있는 경우 동료 Toptaler Sergey Moiseev가 작성한 Angular 5 자습서: 첫 번째 Angular 5 앱에 대한 단계별 가이드를 읽어보세요.