文档结构  
翻译进度:已翻译     翻译赏金:0 元 (?)    ¥ 我要打赏

简介

本文讨论了如何使用 Visual Studio Professional 2015 进行 Angular 2 开发和生产部署,是《使用 Visual Studio 2015 开发和部署 Angular 2 应用》的后续文章

上一篇文章介绍了使用 Gulp 进行Angular 2 应用开发和产品发布打包,包含绑定和注入HTML 模板到 Angular 2 的组件中。 本文将深入探讨 Angular2 和 TypeScript 代码的一些关键点,为两篇文章中都提及的客户维护应用范例服务。本文主要讨论 Angular 2 Candiate 4 版本,Candiate 5 也已经发布了,Angular 2 正式版应该在年内就会发布。正式版发布后,作者会重审并更新本文。 

第 1 段(可获 2.08 积分)



带着学习Angular 2(Release Candiate 4)的目的 ,本文使用Visual Studio Professional 2015开发一个简易的web应用,它包含如下的功能和目标:

  • 允许用户注册、登录和修改用户个人资料
  • 允许用户创建、修改和查看客户数据
  • 使用Angular 2开发前端,微软的 .NET 开发后端
  • 使用TypeScript开发Angular 2 应用  
  • 开发自定义的控件和组件

为实现上图中的功能, 项目中包含了微软的Web API 框架和其它框架 。 项目解决方案将使用Visual Studio 2015内置的IIS Express来实现前端与后台的访问和响应。

第 2 段(可获 1.69 积分)

本文将使用微软的ASP.NET 4 来开发这个简单的应用。新版的 .ASP.NET 改名为 ASP.NET Core.。也就是ASP.NET 5, ASP.NET Core 是对 ASP.NET有重要的重构和一系列架构改变,也更精简和现代化的框架。ASP.NET Core 不再基于 System.Web.dll。我相信这是微软对Node.js的流行、轻量化和开放性的响应。 相互竞争总是好的。也许在后面的文章中我会整合 Angular 2 与ASP.NET Core,当然这其中有很多需要学习了解的。

第 3 段(可获 1.44 积分)

安装和运行示例应用

下载和解压源码后,为了运行示例应用,您需要在项目的根目录运行命令"npm install"。运行这个命令的前提是需要在计算机上安装NodeJs。node_modules 目录比较大,因此源码目录里仅包含了编译应用所需最低限度的 node 模块。 编译和启动程序后,你可以使用如下的用户名和密码登录:

UserName: bgates@microsoft.com
Password: microsoft

当然你也可以自己注册账号。

第 4 段(可获 1.23 积分)

TypeScript

整个客户维护应用使用 TypeScript 编写,你也可以使用纯 JavaScript 来编写 Angular 2 应用,但我选择 TypeScript 有几方面的原因。首先 TypeScript 增强了 JavaScript 的类型、类和接口,模仿强类型语言。如果你是一个 C# 或者 Java 的开发者,你就会喜欢其酷似强类型语言的语法。

通过一个好的集成开发环境,例如 Visual Studio 你可以获得很智能的代码提示和智能完成体验。此外当你在 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;
    }

}

 

第 5 段(可获 1.83 积分)

上面的示例代码中添加了语法糖。例如:使用private或public 修饰属性和方法,给它们添加上数据类型。但当代码编译为JavaScript后,在编译后的JavaScript文件中是没有的。 它主要是提供了更好的开发体验和更严格编码实践。为了示例程序符合编码标准,在HTML模板中访问的组件方法我都加上了public,组件内部方法的方法都加上了private。

第 6 段(可获 1.25 积分)

启动和注入

首先需要做的事情是启动和运行应用。在 Angular 2 中启动应用跟在 Angular 1 中有一些些不同。我们在 HTML 的 body 标签中增加了 ng-app 属性来启动应用,而在 Angular 2 中我们需要使用 bootstrap 函数。

在示例代码 main.ts 中,Angular 2 的 bootstrap 方法将你初始的根应用组件作为参数,在这里是 ApplicationComponent - 然后第二个参数是任何应用需要的额外组件。组件在 Angular 2 中是最最基础的类。

到 typings/browser.d.ts 的引用可以让 Visual Studio 编译和理解 TypeScript 类型以及提供相应的智能代码助理。

// 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]);
第 7 段(可获 1.61 积分)

在开发过程中,主启动代码是在默认页面通过 SystemJS 调用的。当发布到产品环境中,整个应用被绑定到一个 CommonJS 格式,整个 JavaScript 文件底部会增加一个 micro-loader 的东西。这在我之前的 Anguar 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");

    }

}
第 8 段(可获 1.65 积分)

将值注入Angular 2特别方便,当你需要告诉 Angular 2 应用调用 RESTFul web服务的url时,应用会自动注入。

应用程序路由

每一个Angular 2 应用程序都是用了路由来做页面跳转。 在示例程序中, 一个独立的TypeScript文件都引用了一个路由,每一个配置的路由都绑定到了Angular2的组件上。

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

}

 

第 9 段(可获 2.15 积分)

会话状态 

在示例程序中,用户登录后的整个生命周期里都需要维护一个用户的会话信息。采用RESTful API 服务端的多数web应用都是无状态的。为了维护如:用户名、email地址等用户的会话信息,一个Angular2的服务是必要的。  在 Angular 1 中我们使用we factory或者 service来处理,在Angular 2 中更简单。我仅需要创建一个应用需要的包含属性和方法的组件类来作为service。

// 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;

    }

}

 

第 10 段(可获 1.39 积分)

Providers 和单例

面向对象程序设计中的一个常见的模式是单例模式,即允许我们在我们的整个应用中 类只有一个实例。创建一个 Angular 2 service 来维护有状态的用户会话信息必然会使用单例的service组件。

由于 SessionService 类需要在整个应用中使用,所以SessionService 被放在 MasterComponent中。

Providers 通常是单例 (一个实例) 对象,,其它对象通过依赖注入(DI)访问。如你计划多次使用一个对象,那就得像SessionService在整个应用不同的组件中使用那样,你就得请求一个服务的同一个实例并重用服务。 这得通过DI的辅助下创建同一个对象的引用来实现。

第 11 段(可获 1.91 积分)

在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中,依赖注入是通过组件类的构造函数中来完成的。组件依赖的任何 services 或 components都是通过它们的构造方法来注入的。 依赖注入使得代码测试更容易,易于测试的代码也有更高的可重用性,反之亦然。

constructor(private customerService: CustomerService, private sessionService: SessionService ) { }
第 12 段(可获 2.1 积分)

请牢记, Angular 2是一个树形的结构。在应用的其它组件中总是包含一个根组件。 子组件中也可以包含下级子组件中注入的组件。子组件的下级组件可以访问作为它的父级单例组件中注入的组件。 

等待UI

其中一个挑战是:完全重写的Angular 2的预发布版本中不包含已经使用到的上一个版本中可用的功能,而在示例代码中,我又想使用 Angular 1中的UI blocking 功能。

第 13 段(可获 1.49 积分)

在 Angular 1开发的应用中  UI Blocking的功能就是在访问HTTP RESTful服务时在整个页面覆盖一个显示带有"请稍等"消息的遮罩。不幸的是:在我写本文中代码时,Angular 2 中还没有类似Angular 1中的Block UI 。

经过稍微研究后,我发现了一种创建自定义block UI的方法。 首先,在默认的首页中添加了一小段HTML片段和CSS,使得在整个页面上显示出一个带有圆转动进度条和“请稍等”消息的遮罩。当在主页的组件中设置blackUI属性为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>&nbsp;&nbsp;please wait...</span>
    </div>
</div>
</div>

接下来,需要创建一个单例的BlockUI service,以使得在应用的任何地方开关 Block UI 功能。 

第 14 段(可获 2.05 积分)

Block UI service 使用了Angular 2的事件分发器(Event Emitter)。 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 service中,新建一个事件分发器,在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
    });

}
第 15 段(可获 1.16 积分)

在上面的主页组件中,  Angular 2会在组件启动时执行 OnInit 方法。当主页组件启动后它就订阅了一个  blockUI 事件,你订阅一个事件后,当该事件被触发后就会执行你指定的方法。 在上面的示例中,当block UI事件被触发、blockUI属性被设置true或false值后,blockUnblockUI 方法就会执行,进而使得blocking UI遮罩和“请稍等”消息显示或隐藏。

注:在上面的代码中,有如下一段代码
applicationRef.zone.run(() => applicationRef.tick());  这是为在IE浏览器中正常使用后退按钮而打的补丁,不这样的话点击后退按钮将不能返回到上一界面。真希望这个补丁能一劳永逸。

第 16 段(可获 1.9 积分)

HTTP 服务

本文示例应用另一个有用的单例服务是 HttpService 组件,它被用于调用HTTP RESTful 服务。在应用中不直接使用 Angular 2 Http 组件,而是在本服务中封装,使得Http调用更容易自定义和重用。

Angular2的 http.post 需要一个url 和body, 前两个参数都是字符串,还可有一个可选参数对象。 在自定义的HttpService 中,headers 属性被修改了,将请求修改为json请求。

第 17 段(可获 1.1 积分)

另外,示例应用程序中使用了JSON web 令牌来存储认证信息且改认证信息也被添加到了header属性中。 JSON web 令牌是在服务端生成的而存储在浏览器的本地存储中的,因此后续的请求都可以重新保存和访问的。

http.post 返回的是Observable.  Angular 2 的Observable 提供了一种处理异步请求的新模式,Angular 2 Http 组件默认返回的是Observable 而不是Promise。 相比Promise,Observables 提供了更多的功能也更灵活。利用 Observables提高了我们代码的可读性和可维护性,因为Observables 能更加从容的应对复杂场景,如:多值传递、取消observable 请求。

在下面的代码片段中,获得响应后,使用map函数从响应的JSON对象中抽取数据。每调用一次自定义的服务中的HttpPost 方法,blockUI service也都会被调用而触发blockUI 显示。  

当响应返回时,数据解析后,再次调用blockUI service来隐藏blockUI。

// 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;

    }

}

 

第 18 段(可获 2.5 积分)

另外,在上面的代码片段中有这样一行代码放在了http请求的header中 'Accept', 'q=0.8;application/json;q=0.9' 。这是为了解决在Firefox浏览器中解析json响应。如果请求中没有添加该header,那么在Firefox浏览器中 JSON 解析会失败。

使用HTTP Service

我想把所有的http调用和相关的web api 地址放在一个独立的服务中。在下面的例子中,我创建了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);

    }

}

 

第 19 段(可获 2.14 积分)

自定义数据表格

几乎所有web应用都得需要数据表格。本文的写作的时候,还没有很好的针对Angular 2的数据表格。 基于更深入学习 Angular 2的目的,我决定创建一个数据表格 - 带有分页、排序、过滤和选择行功能。

在Angular 1下,大多数表格都需要你创建一个包含列信息的集合 ,如列名、列头、宽度等,还需要在controller中格式化并显示数据。 我也准备这样实现示例程序中需要的数据表格组件。

第 20 段(可获 1.45 积分)

在下面数据表格组件的HTML模板中,包含了3部分。第一部分,通过列信息集合循环来显示表头且有支持排序的上下箭头。排序功能是可选的,你需要在列中指定是否开启。

第二部分,对需要显示在表格中的数据集合进行循环。使用表格中提供的格式化方式进行数据格式化。 *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}}:&nbsp;</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}}:&nbsp;</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}}:&nbsp;</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}}:&nbsp;</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}}&nbsp;</b>
                </button>&nbsp;
            </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>
        &nbsp;{{currentPageNumber}}&nbsp;/&nbsp;{{totalPages}}&nbsp;
        <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>
        &nbsp;items per page
</div>

<!--<br class="grid-pager-break" style="clear:both;" />-->

<div class="grid-pager-responsive-custom">
        &nbsp;{{itemNumberBegin}}&nbsp;-&nbsp;{{itemNumberEnd}}&nbsp;of&nbsp;{{totalRows}}&nbsp;
        items&nbsp;
</div>
第 21 段(可获 1.94 积分)

Data Grid 组件

DataGrid 组件有两个参数,一个是需要显示的列集合另一个是数据集合。DataGrid 组件也使用了事件分发器来触发翻页和排序。另外还支持选择行事件和点击单元格中的配置的按钮事件。

为了使用数据表格,在使用它的HTML模板中得有一个选择器。数据表格有它自己的CSS样式文件。 在Angular 2中允许在使用它的组件中单独修改CSS样式。

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

    }

}

 

第 22 段(可获 1.43 积分)

使用Data Grid

Customer Inquiry 页面将会使用到自定义的数据表格组件。在客户查询页面的模板中仅需要添加  <datagrid> 标签并附上对应的参数。

// 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">&nbsp;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 事件函数中,需要显示的列集合被创建好了。这个列集合是由包含了指定显示格式的DataGridColumn 对象组成。

CustomerInquiryComponents通过数据表格组件的输出参数绑定了DataGrid 事件分发器。  由于它被绑定了数据表格组件上,因此在CustomerInquiry 组件中的datagridEvent(event) 就会被执行。这个代码也展示了在使用的组件中不订阅事件而绑定事件响应函数的方法。 

第 23 段(可获 2.15 积分)

当事件被触发进入到CustomerInquiry 中时,组件会执行对应翻页、排序、过滤方法进而会调用服务端重新获取数据显示到表格中。 

当服务端返回数据后,这些数据会传入到表格中同时分页信息也会更新。在 customer-inquiry.component.ts  代码中,通过@ViewChild 注解实现了调用表格组件中的databind 方法来修改分页信息。

由于Angular 2的所有组件都是类,所以你可以在其中访问它父组件中的方法和属性。如果要访问它的子组件中的方法就得使用 @ViewChild 注解。

客户查组件中也支持预取功能,使用用户输入的客户名进行过滤 。这个功能使用了 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)

    }

}
第 24 段(可获 2.34 积分)

在整个示例应用程序中,我在templateUrl 的url后加添加了一个时间戳。这样就能在开发过程中防止浏览器使用缓存。我还使用了浏览器同步功能:当我的应用的TypeScript和HTML文件被修改后,它会自动刷新浏览器。后面构建生产环境时,会通过Gulp的task来去掉时间戳。

export var debugVersion = "?version=" + Date.now();

@Component({
    templateUrl: 'application/customer/customer-inquiry.component.html' + debugVersion,
    directives: [DataGrid, AlertBoxComponent],
    providers: [AlertService]
})
第 25 段(可获 0.93 积分)

实体

最后,我需要创建一些类来表示从服务端返回的数据。每一个实体都是一个单独的类。在 Angular 2中,你可以考虑把这些实体作为view model 实体,因为它不必匹配服务端的数据格式。 View model 类与服务端的相比,属性通常都是或多或少。 

很棒的是:在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;

}

 

第 26 段(可获 1.65 积分)

结论

在用 TypeScript 开发完一个 Angular 2 应用后,我必须说我已经爱上 Angular 2 和 TypeScript 了。看起来这是客户端开发的完美乌托邦。Angular 2 是一个完全重新开发的版本,感觉它看起来比 Angular 1 要简洁和简单得多。而使用 TypeScript 将会受益于其强大的开发体验以及更好的代码阅读体验。如果你用过 Angular 1 的话,你会发现 Angular 2 的学习曲线很短。当发布 Angular 2 应用时你还会注意到新版本比老版本的页面加载更快,数据绑定效率更高,同时也更加精简和高效。我非常期待 Angular 2 正式版的发布。

第 27 段(可获 1.71 积分)

文章评论