Angular 2 和 .NET Core 从零开始开发 Web 应用程序 第 2 部分:实现前端部分
一篇很棒的文章,教你如何使用 Angular2 和 .NET CORE WEB API 从零开始创建 Web 应用程序
目录
引言
在准备好服务器端之后,我们将开始使用 Angular 2 框架构建客户端(前端)部分。
我们的应用程序是一个由四个模块组成的单页应用程序
- home:主页,用于向客户展示可用产品。
- Login:认证页面,允许用户登录应用程序并访问后端管理界面。
- Subscription:订阅页面,提供一个表单来创建新的用户帐户。
- Product Management:此模块提供一个表单,用于将新产品插入数据库。
在本文中,我参考了以下链接
- Angular2 文档
- Angular2 material
- Angular2 cookies
- 使用 Angular 2 和 Web API 在 ASP.NET Core MVC 中进行 CRUD 操作
背景
为了更好地理解此演示,建议您对以下内容有很好的了解:
- C#、JavaScript 和 HTML 编程
- Angular 2
- MVC 架构
- 数据绑定
- 实体框架
- Visual Studio Code(可选)
必备组件
Using the Code
A) 创建和配置 Angular 2 项目
首先,使用 CMD 创建一个名为“angular2fromscratch
”的新文件夹(新项目),您可以在其中从 Angular 2 官方文档下载 Angular 2 项目。
之后,您应该创建相同的配置文件(基于 Angular 2 官方文档)
- package.json
- tsconfig.json
- typings.json
接下来,您应该使用 npm
安装 TypeScript、typings、webpack、cookies 服务和 angular2-material
Npm install –g typescript
Npm install -g typings
Npm install angular2-cookie
npm install @angular2-material/sidenav
npm install @angular2-material/input
npm install @angular2-material/button
npm install @angular2-material/core
npm install @angular2-material/card
npm install @angular2-material/icon
npm install @angular2-material/toolbar
npm install @angular2-material/progress-circle
npm install @angular2-material/sidenav
最终的 treeview
应该如下所示
在开始实施之前,您应该对 systemjs.config.js 文件进行一些更改,以定义 Angular 2 包的别名。
(function (global) {
System.config({
paths: {
// paths serve as alias
'npm:': 'node_modules/'
},
// map tells the System loader where to look for things
map: {
// our app is within the app folder
app: 'app',
// angular bundles
'@angular/core': 'npm:@angular/core/bundles/core.umd.js',
'@angular/common': 'npm:@angular/common/bundles/common.umd.js',
'@angular/compiler': 'npm:@angular/compiler/bundles/compiler.umd.js',
'@angular/platform-browser':
'npm:@angular/platform-browser/bundles/platform-browser.umd.js',
'@angular/platform-browser-dynamic':
'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js',
'@angular/http': 'npm:@angular/http/bundles/http.umd.js',
'@angular/router': 'npm:@angular/router/bundles/router.umd.js',
'@angular/forms': 'npm:@angular/forms/bundles/forms.umd.js',
'@angular2-material/input': 'npm:@angular2-material/input/input.umd.js',
'@angular2-material/core': 'npm:@angular2-material/core/core.umd.js',
'@angular2-material/card': 'npm:@angular2-material/card/card.umd.js',
'@angular2-material/button': 'npm:@angular2-material/button/button.umd.js',
'@angular2-material/icon': 'npm:@angular2-material/icon/icon.umd.js',
'@angular2-material/toolbar': 'npm:@angular2-material/toolbar/toolbar.umd.js',
'@angular2-material/progress-circle':
'npm:@angular2-material/progress-circle/progress-circle.umd.js',
'@angular2-material/sidenav': 'npm:@angular2-material/sidenav/sidenav.umd.js',
'angular2-cookie': 'npm:angular2-cookie',
// other libraries
'rxjs': 'npm:rxjs',
'angular-in-memory-web-api': 'npm:angular-in-memory-web-api',
},
// packages tells the System loader how to load when no filename and/or no extension
packages: {
app: {
main: './main.js',
defaultExtension: 'js'
},
rxjs: {
defaultExtension: 'js'
},
'angular-in-memory-web-api': {
main: './index.js',
defaultExtension: 'js'
},
'angular2-cookie': {
main: './core.js',
defaultExtension: 'js'
},
}
});
})(this);
B) 设置 Angular 2 项目
在 app 文件夹中,创建以下文件:
- app.module.ts
此文件用于:
- 使用路由器模块定义路由:“
RouterModule
” - 通过关键字导入所需的 Angular 2 模块:“
imports
” - 通过关键字声明组件:“
declarations
” - 通过关键字声明服务:“
providers
” - 通过关键字指定要包含在 index.html 文件中的根组件:“
bootstrap
”
import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { FormsModule } from '@angular/forms'; import { RouterModule, Routes } from '@angular/router'; import { MdInputModule } from '@angular2-material/input'; import { MdCardModule } from '@angular2-material/card'; import { MdButtonModule } from '@angular2-material/button'; import { MdIconModule } from '@angular2-material/icon'; import { MdToolbarModule } from '@angular2-material/toolbar'; import { MdIconRegistry } from '@angular2-material/icon'; import { CookieService } from 'angular2-cookie/services/cookies.service'; import { AppComponent } from './app.component'; import { loginComponenent } from './app.loginComponenent'; import { homeComponenent } from './app.homeComponenent'; import { subscriptionComponenent } from './app.subscriptionComponenent'; import { productManageComponent } from './app.productManageComponent'; import { PageNotFoundComponent } from './app.pageNotFoundComponent'; import { MdProgressCircleModule } from '@angular2-material/progress-circle'; import { MdSidenavModule } from '@angular2-material/sidenav'; import { AuthentificationService } from './services/authentificationService'; import { ManageProductService } from './services/manageProductService' const appRoutes: Routes = [ { path: 'home', component: homeComponenent }, { path: 'login', component: loginComponenent }, { path: 'subscription', component: subscriptionComponenent }, { path: 'productManagement', component: productManageComponent }, { path: '', redirectTo: '/home', pathMatch: 'full'}, { path: '**', component: PageNotFoundComponent } ]; @NgModule({ imports: [ FormsModule , BrowserModule,MdSidenavModule, MdProgressCircleModule, MdInputModule, MdToolbarModule, MdCardModule, MdButtonModule, MdIconModule, RouterModule.forRoot(appRoutes)], declarations: [ AppComponent, loginComponenent, homeComponenent, subscriptionComponenent, productManageComponent, PageNotFoundComponent], providers: [ CookieService, MdIconRegistry, AuthentificationService, ManageProductService ], bootstrap: [ AppComponent ] }) export class AppModule { }
- 使用路由器模块定义路由:“
- app.component.ts
此文件用作 app.html 模板的代码隐藏文件,您将在其中实现:
constructor
:用于初始化我们的主页面(app.html),并声明路由器监听器,根据当前会话值(会话由 cookies 管理)控制用户导航。logOut
事件:此事件用于通过清除 cookies 释放当前会话,并将用户重定向到主页。
import { Component } from '@angular/core'; import {MdToolbar} from '@angular2-material/toolbar'; import {CookieService} from 'angular2-cookie/core'; import {Router, NavigationStart, NavigationEnd} from '@angular/router' @Component({ selector: 'my-app', templateUrl: 'app/templates/app.html', }) export class AppComponent { title = 'Angular 2 Application'; showMenu : boolean = false; login : string = ""; constructor(private _cookieService: CookieService, private router : Router){ this.showMenu = false; router.events.subscribe((val) => { // see also if(val instanceof NavigationStart) { this.login = this._cookieService.get("login"); if(this.login != null && this.login != ""){ this.showMenu = true; if(val.url.endsWith("login") || val.url.endsWith("subscription")){ router.navigateByUrl("/home"); } } else{ this.showMenu = false; if(val.url.endsWith("productManagement")){ router.navigateByUrl("/home"); } } } }); } public logOut(right){ this.showMenu = false; right.close(); this._cookieService.remove("login"); location.reload(); //this.router.navigateByUrl("/home"); } }
- app.homeComponent.ts
构成我们
home
模板的代码隐藏文件,您将在其中实现:ngOnInit
事件:在每次页面初始化时刷新产品列表Remove
事件:通过调用 “_service
变量”的“Delete
方法”删除现有产品(通过其“唯一键”识别)loadProducts event
:通过调用_service
变量的loadProducts
方法从远程服务器加载所有可用产品
import { Component , OnInit} from '@angular/core'; import { Product } from './model/product' import { ManageProductService } from './services/manageProductService' import {CookieService} from 'angular2-cookie/core'; @Component({ templateUrl: 'app/templates/home.html' }) export class homeComponenent extends OnInit { title = 'Products'; showProgress : boolean = false; showError : boolean = false; showEmpty : boolean = false; isLogged : boolean = false; listProduct : Product[]; constructor(private _service : ManageProductService, private _cookieService: CookieService){ super(); console.log("constructor"); } ngOnInit() { this.loadProducts(); this.isLogged = (this._cookieService.get("login")!= null)?true: false; } public Remove(id: number) { var result = confirm("Do you want to continue the suppression process ?"); let _self = this; _self.showError = false; _self.showEmpty = false; if (result == true) { this._service.deleteProduct(id).then(function(){ _self.listProduct = _self.listProduct.filter(value=> { return (value.id != id); }); if(_self.listProduct.length==0){ _self.showEmpty = true; } }).catch(function(){ _self.showError = true; }); } else { } } public loadProducts(){ let _self = this; _self.showError = false; _self.showEmpty = false; _self.showProgress = true; _self._service.loadProducts().then(function(response){ _self.showProgress = false; if(response.length >0){ _self.listProduct = response; }else{ _self.showEmpty = true; } }).catch(function(error: any){ _self.showProgress = false; _self.showError = true; }); } }
- app.loginComponent.ts
构成
login
模板的代码隐藏文件,您将在其中实现:tryToConnect
事件:使用用户凭据(电子邮件、密码)与服务器建立新会话
import { Component } from '@angular/core'; import {Router} from '@angular/router' import { User } from './model/user'; import { AuthentificationService } from './services/authentificationService'; import {CookieService} from 'angular2-cookie/core'; @Component({ templateUrl: 'app/templates/login.html' }) export class loginComponenent { public title = 'Login'; public model : User = new User(); constructor(private _service : AuthentificationService, private _cookieService:CookieService, private router: Router){ } tryToConnect(){ let _self = this; _self._service.Login(_self.model).then(function (response){ _self._cookieService.put("login", _self.model.login); _self.router.navigateByUrl('/home'); }) .catch(function(error : any){ alert("Fail to login"); }); } }
- app.productManageComponent.ts
构成我们
productManagement
模板的代码隐藏文件,您将在其中实现:addNewProduct
事件:当用户点击提交按钮以提交表单数据注册新产品时调用。它将调用_service
变量的addProduct
方法。
import { Component } from '@angular/core'; import { Product } from './model/Product'; import { ManageProductService } from './services/manageProductService'; @Component({ templateUrl: 'app/templates/productManagement.html' }) export class productManageComponent { title = 'Manage Product'; model : Product; constructor(private _service : ManageProductService){ this.model = new Product(); } onChange(event) { var file = event.srcElement.files[0]; this.model.pictureFile = file; } public addNewProduct(){ let _self = this; let formData = new FormData(); formData.append("title",_self.model.title); formData.append("fullDescription",_self.model.fullDescription); formData.append("price",_self.model.price); formData.append("file",_self.model.pictureFile, _self.model.pictureFile.name); _self._service.addProduct(formData).then(function(response){ alert("new product was successfully created"); }).catch(function(error: any){ alert("Server error"); }); } }
- app.subscriptionComponent.ts
构成我们
signup
模板的代码隐藏文件,您将在其中实现:Subscription
事件:当用户尝试提交新用户详细信息时触发。它将调用_service
变量的addProduct
方法。
import { Component } from '@angular/core'; import { User } from './model/user'; import { AuthentificationService } from './services/authentificationService'; import { NgModule } from '@angular/core'; @Component({ templateUrl: 'app/templates/singup.html' }) export class subscriptionComponenent { title = 'Subscription'; public model : User; constructor(private _service : AuthentificationService){ this.model = new User(); } public Subscription(){ let _self = this; this._service.Signup(this.model).then(function(response){ alert("Congratulation, the user with login : "+ _self.model.login + " has been created"); }) .catch(function(error : any){ alert("Fail to create new user"); }); } }
接下来,您应该创建不同的文件夹和文件
1) model 文件夹
- product.ts
用于将 JSON 数据反序列化为
product
对象。export class Product{ id : number; title : string = ''; fullDescription : string = ''; price : number = 0; picture : string; pictureFile : any; }
- user.ts
用于将 JSON 数据反序列化为
user
对象。export class User{ login : string = ''; pwd : string = ''; pwdConfirmation : string = ''; }
2) services 文件夹
- authentificationService.ts
实现确保用户身份验证和用户注册所需的方法
Login
:调用外部 web 服务 "api/Services/Login" 以创建与服务器的会话。Signup
:调用远程 web 服务 "api/Services/Subscription" 以注册用户。
import { Injectable } from '@angular/core'; import { Http, Response, RequestOptions, Headers } from '@angular/http'; import 'rxjs/add/operator/toPromise'; import {User} from '../model/user'; @Injectable() export class AuthentificationService { constructor( private _http : Http){ } Login(model : User): Promise<any> { let _url = "https://:5000/api/Services/Login"; let bodyString = JSON.stringify(model); let headers = new Headers ({ 'Content-Type': 'application/json' }); // ... Set content type // to JSON let options = new RequestOptions({ headers: headers }); return this._http.post(_url, bodyString, options) .toPromise(); } Signup(model : any): Promise<any> { let _url = "https://:5000/api/Services/Subscription"; let bodyString = JSON.stringify(model); let headers = new Headers ({ 'Content-Type': 'application/json' }); // ... Set content // type to JSON let options = new RequestOptions({ headers: headers }); return this._http.post(_url, bodyString, options) .toPromise(); } protected handleErrorPromise(error: any): Promise<void> { try { error = JSON.parse(error._body); } catch (e) { } return Promise.reject(error); } }
- manageProductService.ts
此类提供管理产品的服务,例如:
loadProducts
:通过调用地址为 "api/Services/GetProducts" 的外部 web 服务加载可用产品列表。addProduct
:它调用外部 web 服务 "api/Services/AddProduct" 来创建一个新产品。deleteProduct
:通过调用现有 web 服务 "api/Services/DeleteProduct/" 从数据库中删除特定产品。
import { Injectable } from '@angular/core'; import { Http, Response, RequestOptions, Headers } from '@angular/http'; import { Product} from '../model/Product'; import 'rxjs/add/operator/toPromise'; @Injectable() export class ManageProductService { constructor(private _http: Http) { } loadProducts(): Promise<Product[]> { let _url = "https://:5000/api/Services/GetProducts"; return this._http.get(_url) .toPromise() .then(response => this.extractArray(response)); } addProduct(formData: any): Promise<any>{ let _url = "https://:5000/api/Services/AddProduct"; return new Promise(function (resolve, reject) { let xhr = new XMLHttpRequest(); xhr.open('POST', _url, true); xhr.onload = function (e) { resolve(JSON.parse(xhr.response)); }; xhr.onerror = function (e) { reject(e); } xhr.send(formData); }); } deleteProduct(id: number) { let _url = "https://:5000/api/Services/DeleteProduct/"; return this._http.delete(_url+'?id=' + id).toPromise(); } protected extractArray(res: Response, showprogress: boolean = true) { let data = res.json(); return data || []; } }
3) templates 文件夹
- app.html
这是一个 HTML 模板,用作主页面。它包含:
md-toolbar
:它被视为导航菜单,您可以从中导航到主页、登录和订阅页面。md-sidenav
:右侧导航菜单,显示以下信息:- 当前用户的电子邮件
- 导航按钮:用于重定向到
productManagement
页面 - 注销按钮:用于删除当前会话并重定向到
home
页面
router-outlet
:这是一个占位符,用于根据路由器状态显示特定视图(了解更多关于 router-outlet 的信息)。
<md-toolbar >
<button md-icon-button class="md-raised md-primary" routerLink="/home">
<md-icon >home</md-icon>
</button>
<span>{{title}}</span>
<span flex></span>
<a md-button routerLink="/login" *ngIf="showMenu == false"
routerLinkActive="active">
<i md-icon></i>Login
</a>
<a md-button routerLink="/subscription" *ngIf="showMenu == false"
routerLinkActive="active">
<i md-icon></i>Sign up
</a>
<button md-icon-button *ngIf="showMenu == true"
class="md-raised md-primary" (click)="right.toggle()">
<md-icon class="md-24">list</md-icon>
</button>
</md-toolbar>
<md-sidenav-layout>
<md-sidenav #right align="end" layout-padding>
<h5 style="text-align:center"><md-icon>account_circle</md-icon></h5>
<h6 style="text-align:center"> {{login}}</h6>
<hr>
<div style="text-align:center">
<button md-button routerLink="/productManagement"
class="md-icon-button" style="width: 100%" (click)="right.close()"
routerLinkActive="active">
<h5> Manage</h5>
</button>
</div>
<div style="text-align:center">
<button md-button class="md-icon-button"
style="width: 100%"
(click)="logOut(right)" routerLinkActive="active">
<h5>Logout</h5>
</button>
</div>
</md-sidenav>
<div layout-padding>
<router-outlet></router-outlet>
</div>
</md-sidenav-layout>
- home.html
通过此页面,用户可以看到可用产品的列表及详细信息。如果用户已连接,他将有可能从当前列表中删除某些产品。
<div > <h1>{{title}}</h1> <div class="row"> <md-progress-circle color="primary" mode="indeterminate" *ngIf="showProgress" [style.width]="'40px'" [style.margin]="'0 auto'" ></md-progress-circle> <div *ngIf="showEmpty" class="alert alert-info"> <strong>Info :</strong> Empty Result </div> <div *ngIf="showError" class="alert alert-danger"> <strong>Error :</strong> Problem happened in server side. </div> <div> <div class="row" > <div *ngFor="let obj of listProduct" class="col col-lg-4" style="margin-top:1px; " > <md-card > <md-card-title-group> <img md-card-md-image [src]="obj.picture"> <md-card-title>{{obj.title}}</md-card-title> <md-card-subtitle>Price : {{obj.price}} $</md-card-subtitle> </md-card-title-group> <md-card-actions *ngIf="isLogged == true" style="text-align: center"> <button md-icon-button class="md-primary" (click)="Remove(obj.id)" > <md-icon class="md-24" >delete</md-icon> </button> </md-card-actions> </md-card> <div class="clearfix" ></div> </div> </div> </div>
- login.html
此页面提供了一个连接表单,允许用户输入其凭据(电子邮件和密码)并打开新会话。
<div> <h1>{{title}}</h1> <div > <form> <div> <label class="vSpace"> Login :</label> <md-input type="email" name="login" [(ngModel)]="model.login" placeholder="Email" > </md-input> </div> <div> <label class="vSpace"> Password : </label> <md-input type="password" name="pwd" [(ngModel)]="model.pwd" placeholder="Password" > </md-input> </div> <div> <button type="button" md-raised-button [disabled]="(model.login == '' || model.pwd =='')" (click)="tryToConnect()" class="md-primary" value="submit" > submit </button> </div> </form> </div> </div>
- productManagement.html
此页面为用户提供了通过表单添加新产品的可能性。他必须提供有关产品名称(标题)、描述、价格、缩略图(图片)的信息。
<div> <h1>{{title}}</h1> <div > <form> <div> <label class="vSpace"> Title :</label> <md-input type="text" name="title" [(ngModel)]="model.title" placeholder="title"> </md-input> </div> <div> <label class="vSpace"> Full description :</label> <md-input type="text"name="fullDescription" [(ngModel)]="model.fullDescription" placeholder="Description"> </md-input> </div> <div> <label class="vSpace"> Price ($) : </label> <md-input type="number" name="price" [(ngModel)]="model.price" placeholder="Price" ></md-input> </div> <div> <label class="vSpace"> Piture : </label> <label class="btn btn-default btn-file"> Load File <input type="file" id="file" name="pictureFile" (change)="onChange($event)" class="md-primary" class="hidden"> </label> </div> <div> <button type="button" md-raised-button [disabled]="(model.title == '' || model.fullDescription =='')" (click)="addNewProduct()" class="md-primary" value="submit" > Add </button> </div> </form> </div> </div>
- signup.html
通过此页面,任何匿名用户都可以创建自己的帐户,以获得访问应用程序后端管理界面的权限。
要创建新帐户,用户应填写订阅表单,输入以下信息:登录名、密码。
<div> <h1>{{title}}</h1> <div> <form id="newProductForm" > <div> <label class="vSpace">Login :</label> <md-input type="email" name="login" [(ngModel)]="model.login" placeholder="Email" > </md-input> </div> <div> <label class="vSpace">Password :</label> <md-input type="password" name="pwd" [(ngModel)]="model.pwd" placeholder="Password" > </md-input> </div> <div> <label class="vSpace">Confirm password :</label> <md-input type="password" name="pwdConfirmation" [(ngModel)]="model.pwdConfirmation" placeholder="Confirm password" > </md-input> </div> <div> <button md-raised-button type="button" [disabled]="(model.login == '' || model.pwd =='' || model.pwdConfirmation != model.pwd)" (click)="Subscription()" class="md-primary" value="submit" > submit </button> </div> </form> </div> </div>
要运行演示,您应该使用 CMD
编写以下命令行,但首先请确保您位于应用程序的根目录中
npm start
:将 TS 文件转换为 JavaScript 文件并启动应用程序。
当您尝试通过给定的 URL (https://:3000) 打开应用程序时,您将得到以下结果
要获得此结果,您应该启动您的服务器应用程序(请参阅本文第 1 部分:服务器端实现)。确保您的 Web 服务可以从该地址访问:https://:5000。
参考文献
关注点
希望您喜欢这一系列文章。请尝试下载源代码,我期待您的问题和评论。
历史
- v1 2017 年 1 月 29 日:初始版本