使用 TypeScript 开发 Angular 2 应用程序






4.93/5 (25投票s)
使用 Angular 2 开发 Web 应用程序前端
引言
本文是第二部分,也是对上一篇文章使用 Visual Studio 2015 开发和部署 Angular 2 应用程序的续篇,该文章讨论了如何配置 Visual Studio Professional 2015 进行 Angular 2 开发和生产部署。
上一篇文章还介绍了如何使用 Gulp 为开发和生产发布打包 Angular 2 应用程序,包括将 HTML 模板捆绑并注入 Angular 2 组件。本文将深入探讨两篇文章中包含的示例客户维护应用程序的 Angular 2 和 TypeScript 代码中的一些关键方面。本文将重点关注 Angular 2 的 Release Candidate 4 版本。Release Candidate 5 刚刚发布,所以 Angular 2 的最终版本应该会在年底前发布。当最终版本可用时,我将回顾并更新本文。
概述和目标
为了学习 Angular 2 (Release Candidate 4),本文的示例 Web 应用程序使用 Visual Studio Professional 2015 开发,并将包含以下功能和目标:
- 允许用户注册、登录和更新他们的用户配置文件。
- 允许用户创建、更新和浏览客户数据库。
- 创建一个 Angular 2 前端和一个 Microsoft .NET 后端。
- 使用 TypeScript 开发 Angular 2 应用程序。
- 在需要时开发一些自定义的内部控件和组件。
出于演示目的,本文的项目包含了 Microsoft Web API 架构和底层结构。项目解决方案将集成前端和后端代码,Web API 使用与 Visual Studio 2015 集成的 IIS Express 来接受和响应 RESTful Web 服务请求。
本文的示例应用程序将使用Microsoft ASP.NET 4。已经发布了一个新版本的 .ASP.NET,名为ASP.NET Core。ASP.NET Core(以前称为 ASP.NET 5)是对 ASP.NET 的重大重新设计,并在架构上进行了许多更改,使其成为一个更精简、更模块化的框架。ASP.NET Core 不再基于System.Web.dll。我认为这是 Microsoft 对 Node.js 普及及其轻量级占用空间和开放式架构的回应。竞争是好事。也许在未来的文章中,我会将 Angular 2 与 ASP.NET Core 集成,但在此之前,还有很多东西需要学习。
安装和运行示例应用程序
下载并解压附件的源代码后,要运行示例应用程序,您需要在项目根文件夹中从命令行运行“npm install
”。这假设您已经在计算机上安装了 NodeJs。node_modules文件夹很大,因此我只包含了编译应用程序所需的最小节点模块。编译并启动应用程序后,您可以使用以下用户名和密码登录:
用户名:bgates@microsoft.com
密码:microsoft
您也可以选择注册自己的登录名和密码。
TypeScript
整个客户维护示例应用程序都是用 TypeScript 编写的。或者,您也可以使用纯 JavaScript 编写 Angular 2 应用程序,但我出于各种原因选择了 TypeScript。TypeScript 通过类型、类和接口增强了 JavaScript,模仿了强类型语言。如果您是 C# 或 Java 开发人员,您会喜欢它的语法外观和感觉,因为它非常接近这些强类型语言的语法。
有了 Visual Studio 这样优秀的 IDE,您将获得强大的 IntelliSense
和代码自动完成功能,从而提升开发体验。此外,当您在 Visual Studio 中编译和构建应用程序时,所有 TypeScript 代码也会被编译。如果您的代码中存在任何语法和/或类型错误,Visual Studio 的构建将失败。所有这些对于我们习惯于为服务器端开发获得的客户端 Web 开发来说都是一个巨大的改进。
// customer-maintenance.component.ts
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Address } from '../entities/address.entity';
import { Customer } from '../entities/customer.entity';
import { AlertBoxComponent } from '../shared/alertbox.component';
import { CustomerService } from '../services/customer.service';
import { HttpService } from '../services/http.service';
import { AlertService } from '../services/alert.service';
import { SessionService } from '../services/session.service';
import { AddressComponent } from '../shared/address.component';
@Component({
templateUrl: 'application/customer/customer-maintenance.component.html',
providers: [AlertService],
directives: [AlertBoxComponent, AddressComponent]
})
export class CustomerMaintenanceComponent implements OnInit {
public title: string = 'Customer Maintenance';
public customerID: number;
public customerCode: string;
public companyName: string;
public phoneNumber: string;
public address: Address;
public showUpdateButton: Boolean;
public showAddButton: Boolean;
public customerCodeInputError: Boolean;
public companyNameInputError: Boolean;
public messageBox: string;
public alerts: Array<string =[];
constructor(private route ActivatedRoute,
private customerService CustomerService,
private sessionService SessionService,
private alertService AlertService) {
}
private ngOnInit() {
this.showUpdateButton=false;
this.showAddButton=false;
this.address=new Address();
this.route.params.subscribe(params=> {
let id: string = params['id'];
if (id != undefined) {
this.customerID = parseInt(id);
let customer = new Customer();
customer.customerID = this.customerID;
this.customerService.getCustomer(customer)
.subscribe(
response => this.getCustomerOnSuccess(response),
response => this.getCustomerOnError(response));
}
else {
this.customerID = 0;
this.showAddButton = true;
this.showUpdateButton = false;
}
});
}
private getCustomerOnSuccess(response: Customer) {
this.customerCode = response.customerCode;
this.companyName = response.companyName;
this.phoneNumber = response.phoneNumber;
this.address.addressLine1 = response.addressLine1;
this.address.addressLine2 = response.addressLine2;
this.address.city = response.city;
this.address.state = response.state;
this.address.zipCode = response.zipCode;
this.showUpdateButton = true;
this.showAddButton = false;
}
private getCustomerOnError(response: Customer) {
this.alertService.renderErrorMessage(response.returnMessage);
this.messageBox = this.alertService.returnFormattedMessage();
this.alerts = this.alertService.returnAlerts();
}
public updateCustomer(): void {
let customer = new Customer();
customer.customerID = this.customerID;
customer.customerCode = this.customerCode;
customer.companyName = this.companyName;
customer.phoneNumber = this.phoneNumber;
customer.addressLine1 = this.address.addressLine1;
customer.addressLine2 = this.address.addressLine2;
customer.city = this.address.city;
customer.state = this.address.state;
customer.zipCode = this.address.zipCode;
this.clearInputErrors();
this.customerService.updateCustomer(customer).subscribe(
response => this.updateCustomerOnSuccess(response),
response => this.updateCustomerOnError(response));
}
private updateCustomerOnSuccess(response: Customer) {
if (this.customerID == 0) {
this.customerID = response.customerID;
this.showAddButton = false;
this.showUpdateButton = true;
}
this.alertService.renderSuccessMessage(response.returnMessage);
this.messageBox = this.alertService.returnFormattedMessage();
this.alerts = this.alertService.returnAlerts();
}
private updateCustomerOnError(response: Customer) {
this.alertService.renderErrorMessage(response.returnMessage);
this.messageBox = this.alertService.returnFormattedMessage();
this.alerts = this.alertService.returnAlerts();
}
private clearInputErrors() {
this.customerCodeInputError = false;
this.companyNameInputError = false;
}
}
上面示例代码中的大多数 TypeScript 代码都可以视为“糖”。例如,您可以将属性和方法装饰为private
或public
,并为它们指定强类型。但是,当代码编译成 JavaScript 时,这些 TypeScript 约定会从生成的 JavaScript 中删除。它主要提供了更好的开发体验和更严格的代码实践。对于示例应用程序而言,作为编码标准,我将所有被 HTML 模板访问的组件方法设为public
,而将组件内部调用的所有方法设为private
。
启动和注入
要让您的应用程序启动并运行,您需要做的第一件事就是引导
应用程序。
在 Angular 2 中引导应用程序与 Angular 1 的方式大不相同,在 Angular 1 中,我们通过在 HTML 的 body 标签中添加“ng-app
”来启动应用程序。Angular 2 中的引导使用‘bootstrap
’函数。
在下面的示例 main.ts 代码中,Angular 2 的 bootstrap 方法接受您的初始根应用程序组件作为参数——在本例中是 ApplicationComponent
——第二个参数用于应用程序所需的任何额外提供者。组件在 Angular 2 中本质上是类。
对 typings/browser.d.ts 的引用允许 Visual Studio 编译和理解 TypeScript 类型定义,并提供正确的 IntelliSense。
// main.ts
///<reference path="../typings/browser.d.ts" />
import { bootstrap } from '@angular/platform-browser-dynamic';
import { HTTP_BINDINGS } from '@angular/http';
import { ApplicationComponent } from './application.component';
import { applicationRouterProviders } from './application-routes';
import { enableProdMode } from '@angular/core';
enableProdMode();
bootstrap(ApplicationComponent, [applicationRouterProviders]);
在开发过程中,主引导代码通过 SystemJS
在默认页面中启动。部署到生产环境时,整个应用程序将被捆绑成 CommonJS
格式,并在捆绑的 JavaScript 末尾添加一个 micro-loader
。这在我之前的 Angular 2 文章中有详细描述。
<!-- index.cshtml -->
<script src="~/systemjs.config.js"></script>
<script>
System.import('application/main.js').catch(function (err) { console.error(err); });
</script>
<codeproject-application title="@title"
currentRoute="@currentRoute" version="@version">
<div>
...loading
</div>
</codeproject-application>
根 ApplicationComponent
引用主页面组件,该组件提供了应用程序的结构和布局,包括页眉、菜单栏、内容区域和页脚。
示例应用程序使用 web.config 文件中的配置设置。在下面的示例代码中,应用程序版本、标题和当前路由值被注入到 Angular 2 应用程序中并在构造函数中引用。这些值通过主页面的输入参数注入到主页面中。
// ApplicationComponent.ts
import { Component, ElementRef, ApplicationRef } from '@angular/core';
import { MasterComponent } from './shared/master.component';
import 'rxjs/Rx';
@Component({
selector: 'codeproject-application',
template: '<master [currentRoute]="currentRoute"
[title]="title"
[version]="version">
</master>',
directives: [MasterComponent]
})
export class ApplicationComponent {
public title: string;
public currentRoute: string;
public version: string;
constructor(private elementRef: ElementRef) {
let native = this.elementRef.nativeElement;
this.title = native.getAttribute("title");
this.currentRoute = native.getAttribute("currentRoute");
this.version = native.getAttribute("version");
}
}
当您需要告诉 Angular 2 应用程序所有 RESTFul Web 服务调用的服务器 URL 时,将值注入 Angular 2 会特别有用。
应用程序路由
每个 Angular 2 应用程序都使用路由来导航页面。在示例应用程序中,创建了一个单独的 TypeScript 文件来引用每个路由。每个路由都被配置并链接到一个 Angular 2 组件。
// application-routes.ts
import { provideRouter, RouterConfig } from "@angular/router";
import { AboutComponent } from './home/about.component';
import { RegisterComponent } from './home/register.component';
import { LoginComponent } from './home/login.component';
import { ContactComponent } from './home/contact.component';
import { MasterComponent } from './shared/master.component';
import { HomeComponent } from './home/home.component';
import { ImportCustomersComponent } from './home/import-customers.component';
import { CustomerInquiryComponent } from './customer/customer-inquiry.component';
import { CustomerMaintenanceComponent } from './customer/customer-maintenance.component';
import { UserProfileComponent } from './user/user-profile.component';
import { SessionService } from './services/session.service';
import { authorizationProviders } from "./authorization-providers";
import { AuthorizationGuard } from "./authorization-guard";
const routes: RouterConfig = [
{ path: '', component: HomeComponent },
{ path: 'home/about', component: AboutComponent },
{ path: 'home/contact', component: ContactComponent },
{ path: 'home/home', component: HomeComponent },
{ path: 'home/register', component: RegisterComponent },
{ path: 'home/login', component: LoginComponent },
{ path: 'home/importcustomers', component: ImportCustomersComponent },
{ path: 'customer/customerinquiry', component: CustomerInquiryComponent,
canActivate: [AuthorizationGuard] },
{ path: 'customer/customermaintenance', component: CustomerMaintenanceComponent,
canActivate: [AuthorizationGuard] },
{ path: 'customer/customermaintenance/:id', component: CustomerMaintenanceComponent,
canActivate: [AuthorizationGuard] },
{ path: 'user/profile', component: UserProfileComponent,
canActivate: [AuthorizationGuard] }
];
export const applicationRouterProviders = [
provideRouter(routes),
authorizationProviders
];
保护路由
在 Angular 2 应用程序中,您可能希望做的一件事是保护某些路由,使其在用户未进行身份验证时无法访问。上面的应用程序路由代码引用了一个名为 canActivate
的属性。此属性引用一个名为 AuthorizationGuard
的类。当访问受保护的路由时,AuthorizationGuard
组件将被执行,并检查用户是否已进行身份验证。如果用户未进行身份验证,canActivate
方法会将用户路由到默认路由,最终将用户发送到登录页面。
// Authorization-Guard.ts
import { Injectable, Component } from "@angular/core";
import { CanActivate, Router } from "@angular/router";
import { SessionService } from "./services/session.service";
import { User } from "./entities/user.entity";
@Injectable()
export class AuthorizationGuard implements CanActivate {
constructor(private _router: Router, private sessionService: SessionService) { }
public canActivate() {
if (this.sessionService.isAuthenicated==true) {
return true;
}
this._router.navigate(['/']);
return false;
}
}
会话状态
对于示例应用程序,需要维护用户在其登录会话生命周期内的会话信息。如今,大多数 Web 应用程序是完全无状态的,同时使用集成了 RESTful API 服务的服务器端架构。为了维护用户信息,例如姓名和电子邮件地址信息,需要一个 Angular 2 服务。在 Angular 1 中,我们可以通过 factory 或 service 来处理。在 Angular 2 中,情况变得更简单了。我们只需要创建一个充当服务的组件类,该类包含应用程序所需的属性和方法。
// session.service.ts
import { Injectable, EventEmitter } from '@angular/core';
import { User } from '../entities/user.entity';
@Injectable()
export class SessionService {
public firstName: string;
public lastName: string;
public emailAddress: string;
public addressLine1: string;
public addressLine2: string;
public city: string;
public state: string;
public zipCode: string;
public userID: number;
public isAuthenicated: Boolean;
public sessionEvent: EventEmitter<any>;
public apiServer: string;
public version: string;
constructor() {
this.sessionEvent = new EventEmitter();
}
public authenticated(user: User) {
this.userID = user.userID;
this.firstName = user.firstName;
this.lastName = user.lastName;
this.emailAddress = user.emailAddress;
this.addressLine1 = user.addressLine1;
this.addressLine2 = user.addressLine2;
this.city = user.city;
this.state = user.state;
this.zipCode = user.zipCode;
this.isAuthenicated = true;
this.sessionEvent.emit(user);
}
public logout() {
this.userID = 0;
this.firstName = "";
this.lastName = "";
this.emailAddress = "";
this.addressLine1 = "";
this.addressLine2 = "";
this.city = "";
this.state = "";
this.zipCode = "";
this.isAuthenicated = false;
}
}
提供者和单例
OOP 中的一个常见模式是单例模式,它允许我们在整个应用程序中只有一个类的实例。要创建可以以有状态方式维护用户会话信息的 Angular 2 服务,需要为该服务提供并将其创建为一个单例服务组件。
由于 SessionService
类需要被整个应用程序访问,因此 SessionService
在 MasterComponent
的应用程序树的顶部被提供。
提供者通常是单例(一个实例)对象,其他对象可以通过依赖注入 (DI) 访问它们。如果您计划多次使用某个对象,例如此应用程序中的 SessionService
在不同的组件中,您可以请求该服务的同一个实例并重用它。您通过 DI 来实现这一目的,即提供 DI 为您创建的同一对象的引用。
在 MasterComponent
的示例代码片段中,@Component
注解的 provider 属性会在括号之间的列表中创建一个所有对象的实例。
请注意不要创建另一个通过提供者引用单例对象的组件。单例只需要在应用程序树的顶部提供一次。如果您再次为同一个类提供,就会创建一个新的对象实例,并且您将丢失对任何所需信息的引用。
// master.component.ts
@Component({
selector: 'master',
templateUrl: 'application/shared/master.component.html',
directives: [ROUTER_DIRECTIVES],
providers: [HTTP_PROVIDERS, UserService, CustomerService, HttpService, BlockUIService]
})
依赖注入一直是 Angular 最重要的特性和卖点之一。它允许我们在应用程序的不同组件中注入依赖项。
在 Angular 2 中,依赖注入通过组件类的构造函数来完成。组件所需的任何服务或组件都通过在组件的构造函数方法中添加依赖项来注入到组件中。依赖注入使我们的代码更具可测试性,而可测试的代码使我们的代码更具可重用性,反之亦然。
constructor(private customerService: CustomerService,
private sessionService: SessionService ) { }
请记住,Angular 2 是以树状结构工作的。一个应用程序始终会有一个根组件,其中包含所有其他组件。子组件也可以包含提供者,这些提供者列出了其子组件可能注入的组件。子子组件可以访问由其父组件作为单例组件提供的所有注入的组件。
块 UI
使用像 Angular 2 这样完全重写的框架的预发布版本会遇到一些挑战,即您在先前版本中习惯的某些功能在新版本中尚不可用。我想在示例应用程序中包含的一些功能是我在 Angular 1 中使用的UI 阻塞
功能。
我在 Angular 1 应用程序中的 UI 阻塞功能会在整个页面上创建一个浅色滤镜,以阻止用户交互,并在执行 HTTP RESTful 服务调用时显示“请稍候”消息。不幸まして,我在这篇文章中编写代码时,我在 Angular 1 中使用的 Block UI 尚不适用于 Angular 2。
经过一番研究,我找到了一个创建自定义块 UI 解决方案的方法。首先,我在 index 默认页面中添加了一些 HTML 和 CSS,作为一个容器DIV
,用于一个全页面阻塞滤镜,其中显示了一个加载中的旋转圆圈和一个“请稍候”的消息。当主页面组件中的 blockUI
属性设置为 true
时,此 HTML 将添加到 DOM 中。
<!-- index.cshtml -->
<div *ngIf="blockUI">
<div class="in modal-backdrop spinner-overlay"></div>
<div class="spinner-message-container"
aria-live="assertive" aria-atomic="true">
<div class="loading-message">
<img src="application/content/images/loading-spinner-grey.gif"
class="rotate90">
<span> please wait...</span>
</div>
</div>
</div>
为了让示例应用程序能够从应用程序的任何地方切换自定义Block
UI 功能,需要创建一个单例BlockUI
服务。
Block
UI 服务使用Angular 2 事件发射器。Angular 2 组件可以通过新的EventEmitter
组件发出自定义事件。Angular 2 事件发射器允许您广播事件(发出)并将数据传递给订阅该事件的组件,这使得订阅者可以监听触发的事件并采取行动。
// blockui.service.ts
import { Injectable, EventEmitter } from '@angular/core';
@Injectable()
export class BlockUIService {
public blockUIEvent: EventEmitter<any>;
constructor() {
this.blockUIEvent = new EventEmitter();
}
public startBlock() {
this.blockUIEvent.emit(true);
}
public stopBlock() {
this.blockUIEvent.emit(false);
}
}
Block
UI 服务启动一个新的 Event Emitter,并在执行 startBlock
方法时发出事件并发送值为 true
的值,在执行 stopBlock
时发出值为 false
的值。
// master-component.ts
constructor(
private sessionService: SessionService,
private applicationRef: ApplicationRef,
private userService: UserService,
private blockUIService: BlockUIService,
private router: Router) {
/// bug fix when hitting the back button in Internet Explorer
router.events.subscribe((uri) => {
applicationRef.zone.run(() => applicationRef.tick());
});
}
public ngOnInit() {
this.sessionService.sessionEvent.subscribe(user => this.onAuthenication(user));
this.blockUIService.blockUIEvent.subscribe(event => this.blockUnBlockUI(event));
this.blockUIService.blockUIEvent.emit({
value: true
});
let user: User = new User();
this.userService.authenicate(user).subscribe(
response => this.authenicateOnSuccess(response),
response => this.authenicateOnError(response));
}
private blockUnBlockUI(event) {
this.blockUI = event.value;
}
private authenicateOnSuccess(response: User) {
this.blockUIService.blockUIEvent.emit({
value: false
});
}
在上面的主页面组件中,引用了 Angular 2 OnInit
事件并在组件启动时执行。当主页面组件启动时,它订阅 blockUI
事件发射器。订阅事件发射器时,您需要指定事件触发时要调用的方法。在上面的示例中,当触发块 UI 事件时,将调用 blockUnblockUI
方法,并将 blockUI
属性设置为 true
或 false
的值,从而显示或移除阻塞 UI 滤镜和“请稍候”消息。
题外话:在上面的代码片段中,您会看到对执行命令的代码的引用。
applicationRef.zone.run(() => applicationRef.tick());
需要此代码作为补丁来修复在 Internet Explorer 中按后退按钮时的一个错误。后退按钮无法返回到之前的路由。希望在最终版本中会修复这个问题。
HTTP 服务
本文示例应用程序的另一个有用的单例服务包括一个 HttpService
组件,用于进行 HTTP RESTful 服务调用。我不想在整个应用程序中直接调用Angular 2 Http 组件,而是希望将功能集中起来,以便可以自定义 HTTP 调用并轻松重用。
Angular2 http.post
方法需要一个 url 和一个 body,两者都是字符串,然后是可选的 options
对象。在下面的自定义 HttpService
中,修改了 headers
属性,指定请求为 json 请求。
此外,示例应用程序使用 JSON Web Token 来存储身份验证信息,该信息也添加到 header 属性中。JSON Web Token 是在服务器端生成的,存储在浏览器的本地存储中,以便可以持久化并检索用于后续请求。
http.post
还返回一个Observable。Angular 2 Observable 提供了一种新的运行异步请求的模式。Angular 2 HTTP 组件默认返回 Observable
而不是 Promise
。Observable 比 Promise 提供了更多的功能和灵活性。通过使用 Observable,我们可以提高代码的可读性和可维护性,因为 Observable 可以优雅地响应涉及多个已发出值的更复杂场景,而不是仅限于一次性的单个值,并且还具有取消 observable 请求的能力。
在下面的示例代码片段中,使用 map 函数在订阅 observable 时从响应中提取 JSON 对象。每次调用此自定义服务的 HttpPost
时,都会调用 blockUI
服务,从而触发 blockUI
滤镜。
当响应返回时,将提取 json 主体并返回给调用函数,然后从 UI 中移除 UI 阻塞滤镜。
// http.service.ts
import { Injectable } from '@angular/core';
import { Http, Response } from '@angular/http';
import { Observable } from 'rxjs/Observable';
import { Headers, RequestOptions } from '@angular/http';
import { SessionService } from '../services/session.service';
import { BlockUIService } from './blockui.service';
@Injectable()
export class HttpService {
constructor(private http: Http, private blockUIService: BlockUIService) {}
public httpPost(object: any, url: string): Observable<any> {
this.blockUIService.blockUIEvent.emit({
value: true
});
let body = JSON.stringify(object);
let headers = new Headers();
headers.append('Content-Type', 'application/json');
headers.append('Accept', 'q=0.8;application/json;q=0.9');
if (typeof (Storage) !== "undefined") {
let token = localStorage.getItem("CodeProjectAngular2Token");
headers.append('Authorization', token);
}
let options = new RequestOptions({ headers: headers });
return this.http.post(url, body, options).map((response) =>
this.parseResponse(response, this.blockUIService, true))
.catch((err) => this.handleError(err, this.blockUIService, true));
}
public httpPostWithNoBlock(object: any, url: string): Observable<any> {
let body = JSON.stringify(object);
let headers = new Headers();
headers.append('Content-Type', 'application/json');
headers.append('Accept', 'q=0.8;application/json;q=0.9');
if (typeof (Storage) !== "undefined") {
let token = localStorage.getItem("CodeProjectAngular2Token");
headers.append('Authorization', token);
}
let options = new RequestOptions({ headers: headers });
return this.http.post(url, body, options).map((response) =>
this.parseResponse(response, this.blockUIService, false))
.catch((err) => this.handleError(err, this.blockUIService, false));
}
private handleError(error: any, blockUIService: BlockUIService, blocking: Boolean) {
let body = error.json();
if (blocking) {
blockUIService.blockUIEvent.emit({
value: false
});
}
return Observable.throw(body);
}
private parseResponse(response: Response,
blockUIService: BlockUIService, blocking: Boolean) {
let authorizationToken = response.headers.get("Authorization");
if (authorizationToken != null) {
if (typeof (Storage) !== "undefined") {
localStorage.setItem("CodeProjectAngular2Token", authorizationToken);
}
}
if (blocking) {
blockUIService.blockUIEvent.emit({
value: false
});
}
let body = response.json();
return body;
}
}
在上面的代码片段中还有一个值得注意的地方是附加到 http 请求头的代码行:'Accept', 'q=0.8;application/json;q=0.9'
。这解决了在 Firefox 中运行应用程序时解析 json 响应时的一个问题。在 Firefox 中,如果没有在请求中添加此 header,JSON 解析就会失败。
使用 HTTP 服务
我希望做的另一件事是,将所有 http 调用及其相关的 Web API 端点 URL 保存在单独的服务中。在下面的示例中,我创建了一个 CustomerService
,它调用 HttpService
并传入特定请求的 URL 路由。这使得 URL 路由对更高级别的页面组件隐藏,以便可以更通用地执行 RESTful 服务调用。这更多的是一种偏好,而不是其他任何东西,它提供了额外的抽象层。所有需要客户数据的客户相关功能都将引用并执行 CustomerService
中的方法以从服务器获取数据。
// Customer.service.ts
import { Customer } from '../entities/customer.entity';
import { Injectable } from '@angular/core';
import { Http, Response } from '@angular/http';
import { Observable } from 'rxjs/Observable';
import { Headers, RequestOptions } from '@angular/http';
import { HttpService } from './http.service';
@Injectable()
export class CustomerService {
constructor(private httpService: HttpService) { }
public importCustomers(customer: Customer): Observable<any> {
let url = "api/customers/importcustomers";
return this.httpService.httpPost(customer, url);
}
public getCustomers(customer: Customer): Observable<any> {
let url = "api/customers/getcustomers";
return this.httpService.httpPost(customer, url);
}
public getCustomer(customer: Customer): Observable<any> {
let url = "api/customers/getcustomer";
return this.httpService.httpPost(customer, url);
}
public updateCustomer(customer: Customer): Observable<any> {
let url = "api/customers/updatecustomer";
return this.httpService.httpPost(customer, url);
}
}
自定义数据网格
每个 Web 应用程序都需要一些数据网格功能。在撰写本文时,Angular 2 还没有真正出色的数据网格功能。为了更多地了解 Angular 2,我决定创建自己的数据网格——支持分页、排序以及行的过滤和选择。
您在 Angular 1 中看到的大多数数据网格都需要您创建一个列集合,其中包含有关列名、列标题和宽度等信息,用于在控制器中格式化和显示网格数据。这就是我在示例应用程序中需要数据网格功能的组件所采用的方法。
下面的数据网格组件的 HTML 模板包含三个部分。第一部分将遍历列信息集合,显示带有向上和向下箭头支持的列标题,以便按特定列对网格进行排序。当指定要显示在数据网格中的列时,可以选择启用排序功能。
HTML 模板的第二部分遍历绑定到网格的数据行集合。网格还提供了额外的数据格式化。*ngIf
指令可以轻松显示各种不同格式的单元格数据,或在单元格中包含可点击的行按钮。
HTML 模板的最后一部分提供了数据分页功能,包括传统的首页、上一页、下一页和末页按钮,以及有关总行数和要显示页面的其他信息。
<!--- datagrid.component.html -->
<table class="table table-striped">
<thead>
<tr>
<td *ngFor="let col of columns"
[ngStyle]="{'width': col.cellWidth, 'text-align': col.textAlign}">
<div *ngIf="col.disableSorting==false">
<b><a (click)="sortData(col.name)">{{col.description}}</a></b>
<span *ngIf="col.name == sortColumn && sortAscending == true">
<i class="glyphicon glyphicon-arrow-down"></i>
</span>
<span *ngIf="col.name == sortColumn && sortDesending == true">
<i class="glyphicon glyphicon-arrow-up"></i>
</span>
</div>
<div *ngIf="col.disableSorting==true">
<b>{{col.description}}</b>
</div>
</td>
</tr>
</thead>
<tbody>
<tr *ngFor="let row of rows; let i = index">
<td *ngFor="let col of columns"
[ngStyle]="{'width': col.width, 'text-align': col.textAlign}">
<div *ngIf="col.hyperLink == false &&
col.singleButton == false && col.multiButton == false">
<span class="table-responsive-custom"><b>
{{col.description}}: </b></span>
<div *ngIf="col.formatDate == true && col.formatDateTime == false">
{{row[col.name] | date:"MM/dd/yyyy"}}
</div>
<div *ngIf="col.formatDateTime == true && col.formatDate == false">
{{row[col.name] | date:"MM/dd/yyyy hh:mm AMPM" }}
</div>
<div *ngIf="col.formatDate == false && col.formatDateTime == false">
{{row[col.name]}}
</div>
</div>
<div *ngIf="col.hyperLink == true">
<span class="table-responsive-custom" style="width:100%">
<b>{{col.description}}: </b>
</span>
<div style="text-decoration: underline; cursor:pointer;"
(click)="selectedRow(i)">
<div *ngIf="col.formatDate == true && col.formatDateTime == false">
{{row[col.name] | date:"MM/dd/yyyy"}}
</div>
<div *ngIf="col.formatDateTime == true && col.formatDate == false">
{{row[col.name] | date:"MM/dd/yyy hh:mm AMPM" }}
</div>
<div *ngIf="col.formatDate == false && col.formatDateTime == false">
{{row[col.name]}}
</div>
</div>
</div>
<div *ngIf="col.singleButton == true">
<span class="table-responsive-custom" style="width:100%">
<b>{{col.description}}: </b>
</span>
<button class="btn btn-primary" (click)="buttonClicked(col.buttonText,i)">
<b>{{col.buttonText}}</b>
</button>
</div>
<div *ngIf="col.multiButton == true">
<span class="table-responsive-custom" style="width:100%">
<b>{{col.description}}: </b>
</span>
<div *ngFor="let button of col.buttons" style="float:left">
<button class="btn btn-primary"
(click)="buttonClicked(button.ButtonText,i)">
<b>{{button.ButtonText}} </b>
</button>
</div>
</div>
</td>
</tr>
</tbody>
</table>
<div style="float:left">
<button class="btn ui-grid-pager-control" (click)="buttonFirstPage()"
[disabled]="disableFirstPageButton ||
(totalPages == 1 && this.currentPageNumber == 1)">
<div class="first-triangle"><div class="first-bar"></div></div>
</button>
<button class="btn ui-grid-pager-control" (click)="buttonPreviousPage()"
[disabled]="disablePreviousPageButton ||
(totalPages == 1 && this.currentPageNumber == 1)">
<div class="first-triangle prev-triangle"></div>
</button>
{{currentPageNumber}} / {{totalPages}}
<button class="btn ui-grid-pager-control" (click)="buttonNextPage()"
[disabled]="disableNextPageButton ||
(totalPages == 1 && this.currentPageNumber == 1)">
<div class="last-triangle"></div>
</button>
<button class="btn ui-grid-pager-control" (click)="buttonLastPage()"
[disabled]="disableLastPageButton ||
(totalPages == 1 && this.currentPageNumber == 1)">
<div class="last-triangle"><div class="last-bar"></div></div>
</button>
<select class="ui-grid-pager-row-count-picker" [(ngModel)]="pageSize"
(change)="pageSizeChanged($event.target.value)">
<option *ngFor="let pageSizeDefault of pageSizes"
value="{{pageSizeDefault}}">
{{pageSizeDefault}}
</option>
</select>
items per page
</div>
<!--<br class="grid-pager-break" style="clear:both;" />-->
<div class="grid-pager-responsive-custom">
{{itemNumberBegin}} -
{{itemNumberEnd}} of {{totalRows}}
items
</div>
数据网格组件
DataGrid
组件接受两个输入参数,一个用于要在网格中显示的列的集合,另一个用于行的集合。DataGrid
组件还使用一个事件发射器,该发射器触发分页和排序事件。还支持其他事件,用于行选择以及为任何单元格行配置的按钮的点击事件。
为了使数据网格可供使用,设置了一个选择器属性,允许将数据网格包含在使用的 HTML 模板中。数据网格还包含自己的 CSS 类文件。允许在组件级别拥有单独的 CSS 文件是 Angular 2 的另一个改进。
// datagrid.component.ts
import { Component, EventEmitter, Injectable, Output, Input, OnChanges, OnInit, Host}
from '@angular/core';
import { DataGridColumn, DataGridSorter, DataGridButton, DataGridSortInformation,
DataGridEventInformation } from './datagrid.core';
import { TransactionalInformation } from '../../entities/transactionalinformation.entity';
@Component({
selector: 'datagrid',
styleUrls: ['application/shared/datagrid/datagrid.css'],
inputs: ['rows: rows', 'columns: columns'],
templateUrl: 'application/shared/datagrid/datagrid.component.html'
})
@Injectable()
export class DataGrid implements OnInit {
public columns: Array<DataGridColumn>;
public rows: Array<any>;
public sorter: DataGridSorter;
public pageSizes = [];
public sortColumn: string;
public sortDesending: Boolean;
public sortAscending: Boolean;
@Output() datagridEvent;
@Input() pageSize: number;
public disableFirstPageButton: Boolean;
public disablePreviousPageButton: Boolean;
public disableNextPageButton: Boolean;
public disableLastPageButton: Boolean;
public pageSizeForGrid: number;
public currentPageNumber: number;
public totalRows: number;
public totalPages: number;
public itemNumberBegin: number;
public itemNumberEnd: number;
constructor() {
this.sorter = new DataGridSorter();
this.datagridEvent = new EventEmitter();
this.disableNextPageButton = false;
this.disableLastPageButton = false;
this.disableFirstPageButton = false;
this.disablePreviousPageButton = false;
this.disableFirstPageButton = true;
this.disablePreviousPageButton = true;
this.pageSizes.push(5);
this.pageSizes.push(10);
this.pageSizes.push(15);
this.pageSizeForGrid = 15;
this.sortColumn = "";
this.sortAscending = false;
this.sortDesending = false;
}
public ngOnInit() {}
public databind(transactionalInformation: TransactionalInformation) {
this.currentPageNumber = transactionalInformation.currentPageNumber;
this.totalPages = transactionalInformation.totalPages;
this.totalRows = transactionalInformation.totalRows;
this.itemNumberBegin = ((this.currentPageNumber - 1) * this.pageSize) + 1;
this.itemNumberEnd = this.currentPageNumber * this.pageSize;
if (this.itemNumberEnd > this.totalRows) {
this.itemNumberEnd = this.totalRows;
}
this.disableNextPageButton = false;
this.disableLastPageButton = false;
this.disableFirstPageButton = false;
this.disablePreviousPageButton = false;
if (this.currentPageNumber == 1) {
this.disableFirstPageButton = true;
this.disablePreviousPageButton = true;
}
if (this.currentPageNumber == this.totalPages) {
this.disableNextPageButton = true;
this.disableLastPageButton = true;
}
}
public sortData(key) {
let sortInformation: DataGridSortInformation = this.sorter.sort(key, this.rows);
if (this.sortColumn != key) {
this.sortAscending = true;
this.sortDesending = false;
this.sortColumn = key;
}
else {
this.sortAscending = !this.sortAscending;
this.sortDesending = !this.sortDesending;
}
let eventInformation = new DataGridEventInformation();
eventInformation.EventType = "Sorting";
eventInformation.Direction = sortInformation.Direction;
eventInformation.SortDirection = sortInformation.SortDirection;
eventInformation.SortExpression = sortInformation.Column;
this.datagridEvent.emit({
value: eventInformation
});
}
public selectedRow(i: number) {
let eventInformation = new DataGridEventInformation();
eventInformation.EventType = "ItemSelected";
eventInformation.ItemSelected = i;
this.datagridEvent.emit({
value: eventInformation
});
}
public buttonClicked(buttonName: string, i: number) {
let eventInformation = new DataGridEventInformation();
eventInformation.EventType = "ButtonClicked";
eventInformation.ButtonClicked = buttonName;
eventInformation.ItemSelected = i;
this.datagridEvent.emit({
value: eventInformation
});
}
public pageSizeChanged(newPageSize) {
let eventInformation = new DataGridEventInformation();
eventInformation.EventType = "PageSizeChanged";
this.pageSize = parseInt(newPageSize) + 0;
eventInformation.PageSize = this.pageSize;
this.datagridEvent.emit({
value: eventInformation
});
}
public buttonNextPage() {
let currentPageNumber = this.currentPageNumber + 1;
let eventInformation = new DataGridEventInformation();
eventInformation.EventType = "PagingEvent";
eventInformation.CurrentPageNumber = currentPageNumber;
this.datagridEvent.emit({
value: eventInformation
});
}
public buttonPreviousPage() {
this.currentPageNumber = this.currentPageNumber - 1;
let eventInformation = new DataGridEventInformation();
eventInformation.EventType = "PagingEvent";
eventInformation.CurrentPageNumber = this.currentPageNumber;
this.datagridEvent.emit({
value: eventInformation
});
}
public buttonFirstPage() {
this.currentPageNumber = 1;
let eventInformation = new DataGridEventInformation();
eventInformation.EventType = "PagingEvent";
eventInformation.CurrentPageNumber = this.currentPageNumber;
this.datagridEvent.emit({
value: eventInformation
});
}
public buttonLastPage() {
this.currentPageNumber = this.totalPages;
let eventInformation = new DataGridEventInformation();
eventInformation.EventType = "PagingEvent";
eventInformation.CurrentPageNumber = this.currentPageNumber;
this.datagridEvent.emit({
value: eventInformation
});
}
}
使用数据网格
示例应用程序的客户查询页面将使用数据网格组件。Customer Inquiry HTML 模板中唯一需要的是在模板中添加 <datagrid>
选择器标签,并从 Customer Inquiry 组件的属性中分配行和列的输入参数。
// customer-inquiry.component.html
<h4 class="page-header">{{title}}</h4>
<div class="form-horizontal" style="margin-bottom:25px;">
<div style="width:20%; float:left; padding-right:1px;">
<input type="text" class="form-control" placeholder="Customer Code"
[(ngModel)]="customerCode" (ngModelChange)="customerCodeChanged($event)" />
</div>
<div style="width:20%; float:left; padding-right:1px;">
<input type="text" class="form-control" placeholder="Company Name"
[(ngModel)]="companyName" (ngModelChange)="companyNameChanged($event)" />
</div>
<div style="float:left; padding-right:1px; padding-left:5px;">
<button class="btn btn-primary" (click)="reset()">
<b>Reset Search</b>
</button>
</div>
<div style="float:left; padding-right:1px; padding-left:5px;">
<button class="btn btn-primary" (click)="search()">
<b>Submit Search</b>
</button>
</div>
<div style="float:right; padding-left:5px;">
<label><input type="checkbox"
[(ngModel)]="autoFilter"> Auto Filtering Search</label>
</div>
</div>
<br clear="all" />
<datagrid [rows]="customers"
[columns]="columns"
[pageSize]="pageSize"
(datagridEvent)="datagridEvent($event)">
</datagrid>
<br style="clear:both;" />
<div>
<alertbox [alerts]="alerts" [messageBox]="messageBox"></alertbox>
</div>
在 CustomerInquiryComponent
中,通过 @Component
指令的 directives 属性引用 DataGrid
。在 OnInit
事件中,将使用要在数据网格中显示的列来填充列集合。columns 数组是 DataGridColumn
对象的集合,它允许您指定任何额外的格式。
CustomerInquiryComponents
通过数据网格组件的 output 参数绑定到 DataGrid
事件发射器。由于它绑定到数据网格组件,因此在使用的 CustomerInquiry
组件中执行 datagridEvent(event)
。这是通过事件发射器在使用的组件中执行代码的一个示例,而无需使用的组件订阅事件发射器。
当事件被发送回 CustomerInquiry
组件时,组件会执行必要的方法来完成分页、排序和过滤,并通过 http 调用回服务器以使用新数据重新填充数据网格。
当数据从服务器请求返回时,数据将重新绑定回数据网格,并且分页器将使用新的页面信息进行更新。下面的 customer-inquiry.component.ts 代码通过 @ViewChild
注解访问数据网格组件方法 databind
来更新数据网格分页器。
由于 Angular 2 中的所有组件都是类,您可能希望从父组件调用这些类的方法或访问这些类的属性。这需要访问子组件。要访问组件及其方法,请使用 @ViewChild
注解。
Customer Inquiry 组件还支持类型自动完成功能,即当用户键入客户名称信息时,数据网格会实时过滤。这通过使用 Angular 2 setTimeout()
回调函数来支持,该函数允许在按键之间有短暂的延迟,然后再执行服务器调用以获取数据。
// customer-inquiry.component.ts
import { Component, OnInit, EventEmitter, Output, ViewChild } from '@angular/core';
import { Router } from '@angular/router';
import { DataGridColumn, DataGridButton, DataGridEventInformation }
from '../shared/datagrid/datagrid.core';
import { DataGrid } from '../shared/datagrid/datagrid.component';
import { AlertService } from '../services/alert.service';
import { CustomerService } from '../services/customer.service';
import { AlertBoxComponent } from '../shared/alertbox.component';
import { Customer } from '../entities/customer.entity';
import { TransactionalInformation } from '../entities/transactionalinformation.entity';
export var debugVersion = "?version=" + Date.now();
@Component({
templateUrl: 'application/customer/customer-inquiry.component.html' + debugVersion,
directives: [DataGrid, AlertBoxComponent],
providers: [AlertService]
})
export class CustomerInquiryComponent implements OnInit {
@ViewChild(DataGrid) datagrid: DataGrid;
public title: string = 'Customer Inquiry';
public customers: Customer[];
public columns = [];
public alerts: Array<string> = [];
public messageBox: string;
public totalRows: number;
public currentPageNumber: number = 1;
public totalPages: number;
public pageSize: number;
public companyName: string;
public customerCode: string;
private sortDirection: string;
private sortExpression: string;
public autoFilter: Boolean;
public delaySearch: Boolean;
public runningSearch: Boolean;
constructor(private alertService: AlertService, private customerService: CustomerService,
private router: Router) {
this.currentPageNumber = 1;
this.autoFilter = false;
this.totalPages = 0;
this.totalRows = 0;
this.pageSize = 15;
this.sortDirection = "ASC";
this.sortExpression = "CompanyName";
}
public ngOnInit() {
this.columns.push(new DataGridColumn('customerCode', 'Customer Code',
'[{"width": "20%" , "disableSorting": false}]'));
this.columns.push(new DataGridColumn('companyName', 'Company Name',
'[{"width": "30%" ,
"hyperLink": true, "disableSorting": false}]'));
this.columns.push(new DataGridColumn('city', 'City',
'[{"width": "20%" , "disableSorting": false}]'));
this.columns.push(new DataGridColumn('zipCode', 'Zip Code',
'[{"width": "15%" , "disableSorting": false}]'));
this.columns.push(new DataGridColumn('dateUpdated', 'Date Updated',
'[{"width": "15%" ,
"disableSorting": false, "formatDate": true}]'));
this.executeSearch();
}
private executeSearch(): void {
if (this.runningSearch == true) return;
let miliseconds = 500;
if (this.delaySearch == false) {
miliseconds = 0;
}
this.runningSearch = true;
setTimeout(() => {
let customer = new Customer();
customer.customerCode = this.customerCode;
customer.companyName = this.companyName;
customer.pageSize = this.pageSize;
customer.sortDirection = this.sortDirection;
customer.sortExpression = this.sortExpression;
customer.currentPageNumber = this.currentPageNumber;
this.customerService.getCustomers(customer)
.subscribe(
response => this.getCustomersOnSuccess(response),
response => this.getCustomersOnError(response));
}, miliseconds)
}
private getCustomersOnSuccess(response: Customer): void {
let transactionalInformation = new TransactionalInformation();
transactionalInformation.currentPageNumber = this.currentPageNumber;
transactionalInformation.pageSize = this.pageSize;
transactionalInformation.totalPages = response.totalPages;
transactionalInformation.totalRows = response.totalRows;
transactionalInformation.sortDirection = this.sortDirection;
transactionalInformation.sortExpression = this.sortExpression;
this.customers = response.customers;
this.datagrid.databind(transactionalInformation);
this.alertService.renderSuccessMessage(response.returnMessage);
this.messageBox = this.alertService.returnFormattedMessage();
this.alerts = this.alertService.returnAlerts();
this.runningSearch = false;
}
private getCustomersOnError(response): void {
this.alertService.renderErrorMessage(response.returnMessage);
this.messageBox = this.alertService.returnFormattedMessage();
this.alerts = this.alertService.returnAlerts();
this.runningSearch = false;
}
public datagridEvent(event) {
let datagridEvent: DataGridEventInformation = event.value;
if (datagridEvent.EventType == "PagingEvent") {
this.pagingCustomers(datagridEvent.CurrentPageNumber);
}
else if (datagridEvent.EventType == "PageSizeChanged") {
this.pageSizeChanged(datagridEvent.PageSize);
}
else if (datagridEvent.EventType == "ItemSelected") {
this.selectedCustomer(datagridEvent.ItemSelected);
}
else if (datagridEvent.EventType == "Sorting") {
this.sortCustomers(datagridEvent.SortDirection, datagridEvent.SortExpression);
}
}
private selectedCustomer(itemSelected: number) {
let rowSelected = itemSelected;
let row = this.customers[rowSelected];
let customerID = row.customerID;
this.router.navigate(['/customer/customermaintenance', { id: customerID }]);
}
private sortCustomers(sortDirection: string, sortExpression: string) {
this.sortDirection = sortDirection;
this.sortExpression = sortExpression;
this.currentPageNumber = 1;
this.delaySearch = false;
this.executeSearch();
}
private pagingCustomers(currentPageNumber: number) {
this.currentPageNumber = currentPageNumber;
this.delaySearch = false;
this.executeSearch();
}
private pageSizeChanged(pageSize: number) {
this.pageSize = pageSize;
this.currentPageNumber = 1;
this.delaySearch = false;
this.executeSearch();
}
public reset(): void {
this.customerCode = "";
this.companyName = "";
this.currentPageNumber = 1;
this.delaySearch = false;
this.executeSearch();
}
public search(): void {
this.currentPageNumber = 1;
this.delaySearch = false;
this.executeSearch();
}
public companyNameChanged(newValue): void {
if (this.autoFilter == false) return;
if (newValue == "") return;
this.companyName = newValue;
this.currentPageNumber = 1;
this.delaySearch = true;
setTimeout(() => {
this.executeSearch();
}, 500)
}
public customerCodeChanged(newValue): void {
if (this.autoFilter == false) return;
if (newValue == "") return;
this.customerCode = newValue;
this.currentPageNumber = 1;
this.delaySearch = true;
setTimeout(() => {
this.executeSearch();
}, 500)
}
}
在整个示例应用程序中,我在 templateUrl
属性的 URL 末尾添加了一个日期时间戳。这有助于在开发应用程序时打破浏览器缓存。我在开发此应用程序时使用了浏览器同步功能,该功能会自动监视我的 TypeScript 和 HTML 文件中的更改,然后自动刷新浏览器。稍后在生产构建中,这个日期时间戳会通过 Gulp 任务移除。
export var debugVersion = "?version=" + Date.now();
@Component({
templateUrl: 'application/customer/customer-inquiry.component.html' + debugVersion,
directives: [DataGrid, AlertBoxComponent],
providers: [AlertService]
})
实体
最后,对于这个应用程序,我需要创建表示服务器返回的所有数据格式的类。我为每个实体创建了单独的实体类。在 Angular 2 客户端,您可以将这些实体视为视图模型
实体,因为它们不一定与服务器端数据模型格式匹配。视图模型类通常比其服务器端对应项具有更多或更少的数据属性。
TypeScript 的一个很酷之处在于,您可以使用extends
子句通过继承来扩展类。TransactionalInformation
实体包含来自服务器的通用信息,应用程序中的所有实体都需要引用这些信息。User 类继承了TransactionalInformation
类,因此属性被包含在User
类中。
import { TransactionalInformation } from './transactionalinformation.entity';
export class User extends TransactionalInformation {
public userID: number;
public firstName: string;
public lastName: string;
public emailAddress: string;
public addressLine1: string;
public addressLine2: string;
public city: string;
public state: string;
public zipCode: string;
public password: string;
public passwordConfirmation: string;
public dateCreated: Date;
public dateUpdated: Date;
}
结论
我想说的是,在用 TypeScript 开发了 Angular 2 应用程序之后,我不得不说我同时喜爱 Angular 2 和 TypeScript。这似乎是客户端开发的完美乌托邦。Angular 2 是从头开始重写的,感觉比它的前身 Angular 1 更干净、更简单。使用 TypeScript 是一个额外的优势,它提供了更好的开发体验和更美观的代码库。如果您有 Angular 1 的经验,您会发现学习 Angular 2 的曲线非常小。在部署 Angular 2 应用程序时,您还会注意到 Angular 2 的运行速度比 Angular 1 更流畅、更高效,页面加载速度更快,数据绑定更有效。我期待 Angular 2 的最终版本。
历史
- 2016 年 8 月 13 日:初始版本