本文概述
自从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#Web 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版本, 该版本为开发人员提供免费版本, 但非常完整:社区。
因此, 这里是本教程需要安装的东西的列表:
- Visual Studio程式码
- Visual Studio 2017社区(或任何)
- Node.js v8.10.0
- SQL Server 2017
注意
通过在终端或控制台窗口中运行节点-v和npm -v来验证你至少在运行节点6.9.x和npm3.x.x。较旧的版本会产生错误, 但是较新的版本可以。 |
前端
快速开始
让乐趣开始!我们需要做的第一件事是全局安装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:
将创建一个名为dist的新文件夹, 我们可以使用IIS或你喜欢的任何Web服务器为其提供服务。然后, 你可以在浏览器中键入URL, 然后…完成!
注意
本教程的目的不是显示如何设置Web服务器, 因此我假设你已经知道这一点。 |
src文件夹
我的src文件夹的结构如下:在app文件夹中, 我们具有一些组件, 将在其中为每个Angular组件创建css, ts, spec和html文件。我们还将创建一个config文件夹以保留站点配置, 指令将具有所有自定义指令, 帮助程序将包含身份验证管理器之类的通用代码, 布局将包含主体, 头部和侧面板之类的主要组件, 模型将保留与后端视图模型匹配, 最后服务将具有所有对后端的调用的代码。
在app文件夹之外, 我们将保留默认创建的文件夹, 例如资产和环境, 以及根文件。
创建配置文件
让我们在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的样式, 我建议你使用它, 因为Google也支持它。
要安装它, 我们只需要运行以下三个命令, 就可以在Visual Studio Code终端上执行这些命令:
npm install --save @angular/material @angular/cdk
npm install --save @angular/animations
npm install --save hammerjs
第二个命令是因为某些”材质”组件依赖于”角度动画”。我还建议阅读官方页面, 以了解支持哪些浏览器以及什么是polyfill。
第三个命令是因为某些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 "[email protected]/material/prebuilt-themes/deeppurple-amber.css";
现在通过在main.ts文件中添加以下行来导入HammerJS:
import 'hammerjs';
最后, 我们所缺少的只是在头部内部将Material图标添加到index.html中:
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
布局
在此示例中, 我们将创建一个简单的布局, 如下所示:
想法是通过单击标题上的某些按钮来打开/隐藏菜单。 Angular Responsive将为我们完成其余的工作。为此, 我们将创建一个布局文件夹, 并将默认创建的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>
基本上, 我们将在组件中具有一个身份验证属性, 如果用户未登录, 该属性将允许我们删除标题和菜单, 而是显示一个简单的登录页面。
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和简单的布局来开始构建页面。但是我们如何在页面之间导航?
为了创建一个简单的示例, 让我们创建两个页面:”用户”和”仪表板”, 在这里我们可以获取数据库中现有用户的列表, 在该页面中, 我们可以显示一些统计信息。
在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导入RouterModule和Routes, 我们就可以映射要实现的路径。在这里, 我们创建了四个路径:
- / dashboard:我们的主页
- / login:用户可以在其中进行身份验证的页面
- / logout:注销用户的简单路径
- / users:我们的第一页, 我们要从后端列出用户
请注意, 默认情况下, 信息中心是我们的页面, 因此, 如果用户键入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并返回404-Not Found错误, 除非将其配置为返回index.html。
要解决此问题非常简单, 我们只需要创建服务提供者文件配置即可。由于我在这里使用IIS, 因此我将向你展示如何在此环境中进行操作, 但是对于Apache或任何其他Web服务器, 其概念是相似的。
因此, 我们在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, 你还可以使用Cookie或sessionStorage, 并且该决定将取决于我们要实现的行为。顾名思义, sessionStorage仅在浏览器会话期间可用, 并在关闭选项卡或窗口时删除;但是, 它确实可以在页面重新加载后幸存下来。如果需要不断提供存储的数据, 则localStorage优于sessionStorage。 Cookies主要用于读取服务器端, 而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}, _
_~~~_
This is our component to log out of the site, and it's just a simple class to clean out the `localStorage`. Let's create it under the `components/login` folder with the name of `logout.component.ts`:
~~~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类吗?这是可观察的。 Observables支持在应用程序的发布者和订阅者之间传递消息。每次身份验证令牌更改时, 身份验证属性都会更新。查看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)
);
}
}
后端的第一个调用是对令牌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应用程序的第一步。我们需要做的第一件事是创建一个ASP.Net Core Web应用程序, 我们将其称为SeedAPI.Web.API。
请确保选择”清空”模板, 这样你可以从头开始, 如下所示:
就是这样, 我们从一个空的Web应用程序开始创建解决方案。现在, 我们的架构将如下所示, 因此必须创建不同的项目:
为此, 对于每个解决方案, 只需右键单击解决方案, 然后添加”类库(.NET Core)”项目。
架构
在上一节中, 我们创建了八个项目, 但是它们的用途是什么?这是每个简单的描述:
- Web.API:这是我们的启动项目, 在此创建端点。在这里, 我们将设置JWT, 注入依赖项和控制器。
- ViewModels:在这里, 我们根据控制器将在响应中返回给前端的数据类型执行转换。将这些类与前端模型匹配是一个好习惯。
- 接口:这将有助于实现注入依赖项。静态类型语言的引人注目的好处是, 编译器可以帮助验证你的代码所依赖的合同是否确实得到满足。
- 公用:所有共享的行为和实用程序代码都在这里。
- 模型:最好不要将数据库直接与面向前端的ViewModels匹配, 因此, Models的目的是创建独立于前端的实体数据库类。这将使我们将来可以更改数据库, 而不必影响前端。当我们只想进行一些重构时, 它也有帮助。
- 映射:这是将ViewModels映射到Models的过程, 反之亦然。在控制器和服务之间调用此步骤。
- 服务:用于存储所有业务逻辑的库。
- 存储库:这是我们称为数据库的唯一位置。
这些引用将如下所示:
基于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/"
}
然后, 我们只需要从startup.cs中的ConfigureServices方法调用它:
// 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_)以来, 设置实体框架确实非常容易。我们将使用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();
}
}
}
}
我们真的很接近, 但是首先, 我们需要在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" });
}
}
}
我们从启动文件中调用它们:
// 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();
}
依赖注入
使用依赖注入在不同项目之间移动是一个好习惯。这将帮助我们在控制器和映射器, 映射器和服务以及服务和存储库之间进行通信。
在文件夹App_Start中, 我们将创建文件DependencyInjectionConfig.cs, 它看起来像这样:
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>();
}
}
}
我们将需要为每个新实体创建一个新的Map, Service和Repository, 并将它们与该文件匹配。然后, 我们只需要从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();
}
最后, 当我们需要从数据库中获取用户列表时, 我们可以使用此依赖项注入创建一个控制器:
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)
{
}
}
}
查看此处如何显示Authorize属性, 以确保前端已登录以及依赖注入在类的构造函数中的工作方式。
我们终于可以调用数据库了, 但是首先, 我们需要了解Map项目。
地图项目
此步骤仅用于将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项目。
服务项目
这里没有太多要说的:我们的示例确实很简单, 并且这里没有业务逻辑或代码。当我们需要在数据库或控制器步骤之前或之后进行计算或执行一些逻辑时, 该项目将对将来的高级需求很有用。在该示例之后, 该类将变得非常裸露:
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();
}
}
}
仓库项目
我们将转到本教程的最后一部分:我们只需要对数据库进行调用, 因此我们创建了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还是很陌生, 但在后续操作上仍然遇到困难, 请阅读srcminier Sergey Moiseev的Angular 5教程:第一个Angular 5应用程序分步指南。
评论前必须登录!
注册