Angular 专家级编程(一)
原文:
zh.annas-archive.org/md5/EE5928A26B54D366BD1C7A331E3448D9译者:飞龙
前言
学习如何使用 Angular 框架为任何部署目标(移动、桌面或原生应用)构建出色的应用程序。
本书涵盖了编写现代、直观和响应式应用程序所需的一切。
本书涵盖了概念和基础知识,以及详细的代码片段和示例,这将帮助您通过学习 Angular 框架来快速启动并开启新的想法。
本书的章节涵盖了任何开发人员轻松掌握 Angular 编程技能的主题。与此同时,经验丰富的开发人员将学会掌握从现有的 AngularJS 框架迁移的技能,并学习本书中涵盖的高级技术和最佳实践。
除了出色的功能,任何应用程序都严重依赖于设计方面。本书将向您介绍并帮助您通过 Material Design 和 Bootstrap CSS 来提高您的设计技能。
学习如何编写和创建可重用、可测试和可维护的服务、表单、管道、异步编程、动画、路由等等。
我们说过可测试吗?当然。本书向您介绍了 Jasmine 和 Protractor 框架。在学习这些框架的同时,我们将学会使用 Jasmine 编写单元测试脚本,以及使用 Protractor 框架编写端到端测试脚本。
学习和掌握 Angular 技能的旅程将是有趣的、发人深省的,最重要的是简单的。逐步指南帮助用户在其应用程序和项目中实现这些概念。
本书内容包括
第一章,“Angular 的架构概述和构建简单应用”,解释了 Angular 的架构、TypeScript 的基础知识,以及如何创建一个简单的 Angular 应用。
第二章,“将 AngularJS 应用迁移到 Angular 应用”,展示了如何将 AngularJS 应用迁移到 Angular 4,并讨论了迁移应用的最佳实践。
第三章,“使用 Angular CLI 生成最佳实践的 Angular 应用”,展示了如何使用 Angular 命令行界面为 Angular 应用生成样板代码。
第四章,“使用组件”,讨论了组件的生命周期。我们将学习如何实现多个和容器组件,以及不同组件之间的交互。
第五章,“实现 Angular 路由和导航”,展示了如何为我们的 Angular 应用程序创建路由策略和路由。我们将学习路由的构建模块,创建路由、子路由,并使用路由守卫来保护路由。状态是路由中的一个重要方面。我们将实现状态以创建安全的、多状态的应用程序路由。
第六章,“创建指令和实现变更检测”,解释了指令,Angular 提供的不同类型的指令,以及如何创建自定义用户定义的指令。我们将深入学习 Angular 如何处理变更检测,以及如何在我们的应用程序中利用变更检测。
第七章,“使用 Observables 进行异步编程”,展示了如何利用 Angular 的 Observable 和 Promises 来实现异步编程。此外,我们还将学习如何构建一个基本但可扩展的异步 JSON API,用于查询漫威电影宇宙。
第八章,“模板和数据绑定语法”,讨论了用于编写表达式、运算符、属性和将事件附加到元素的模板语法。数据绑定是允许数据从数据源到视图目标以及反之的关键功能之一。此外,我们还将学习不同的数据绑定方式,并创建许多示例。
第九章,“Angular 中的高级表单”,解释了如何使用和掌握响应式表单。我们通过强调 HTML 模型与 NgModels 之间的关系来解决响应式表单的响应式部分,以便在给定表单上的每次更改都传播到模型。
第十章,“Angular 中的 Material Design”,讨论了 Material Design,这是关于设计的新热潮。在本章中,我们将学习如何将 Material Design 与 Angular 集成。此外,我们还将学习如何使用诸如网格和按钮之类的有用组件。
第十一章《实现 Angular 管道》解释了在视图中转换数据是我们在应用程序中必须做的最常见的仪式之一。我们将学习如何使用各种内置管道来转换值,并创建我们自己的管道。此外,我们还将学习如何传递参数并根据需要自定义管道。
第十二章《实现 Angular 服务》讨论了服务和工厂,创建 Angular 服务,使用服务从组件中访问数据以及创建异步服务。
第十三章《应用依赖注入》解释了如何创建可用作各种组件之间共享资源的可注入对象、服务和提供者类。此外,我们还将学习如何使用 Inject、Provider、useClass和useValue动态创建对象并及时使用。
第十四章《处理 Angular 动画》展示了动画对于设计和构建具有平滑过渡和效果的美观用户体验至关重要。我们将学习并实施使用动画、过渡、状态和关键帧的示例。
第十五章《将 Bootstrap 集成到 Angular 应用程序中》讨论了 Bootstrap,这可能是目前最流行的前端框架,在本章中,我们将了解拥有 Angular x Bootstrap 应用程序意味着什么。
第十六章《使用 Jasmine 和 Protractor 框架测试 Angular 应用程序》教授了软件开发过程中可能最重要的方面——使用 Jasmine 和 Protractor 框架测试 Angular 应用程序。我们将首先概述每个框架,然后转向 Angular 提供的测试工具。我们还将创建用于测试 Angular 组件和服务的示例测试脚本。
第十七章《Angular 中的设计模式》讨论了 TypeScript,这是一种面向对象的编程语言,我们可以利用数十年关于面向对象架构的知识。在本章中,我们将探索一些最有用的面向对象设计模式,并学习如何在 Angular 中应用它们。
你需要为这本书做好准备
您将需要以下软件清单:
-
NodeJS 6.10 或更高版本
-
NPM 3.10 或更高版本
-
良好的编辑器,如 Visual Studio Code 或 Sublime Text
-
浏览器,如 Chrome 或 Firefox 或 Edge
-
互联网连接以下载和安装节点包
这本书是为谁准备的
这本书是为具有一定 Angular 先前经验的 JavaScript 开发人员准备的。我们假设您对 HTML、CSS 和 JavaScript 有一定的了解。
约定
在本书中,您将找到一些文本样式,用于区分不同类型的信息。以下是一些示例以及它们的含义解释。文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄显示如下:“然后,我们进入advanced-forms文件夹并删除不在chap7/angular-promise子目录中的所有内容。”
代码块设置如下:
@Component({ selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
任何命令行输入或输出都按照以下方式编写:
npm install -g typescript
tsc mytypescriptcodefile.ts
警告或重要提示显示在这样的框中。提示和技巧显示如下。
第一章:Angular 的架构概述和构建简单应用
无论您是新手还是对 AngularJS 或 Angular 都不熟悉。如果您想快速开发具有丰富 UI 和 Angular 组件、模板和服务功能的优秀 Web 应用程序,您需要掌握 Angular,这本书就是为您准备的。
Angular 是一个 JavaScript 框架,使开发人员能够构建 Web 和移动应用程序。使用 Angular 构建的应用程序可以针对任何设备,如手机、平板电脑和台式电脑。Angular 不是 AngularJS 的增量版本。它完全重写了改进的依赖注入、动态加载和更简单的路由,并建议开发人员使用 TypeScript 并利用面向对象编程、静态类型、泛型和 lambda。
在本章中,我们将涵盖以下主题:
-
Angular 架构
-
TypeScript 的基础知识
-
构建一个简单的应用程序
Angular 架构
在讨论架构之前,让我们看看 Angular 的新功能。Angular 的主要重点是移动设备,因为重要的是要考虑应用程序在手机上的性能和加载时间。许多模块已经从 Angular 核心中解耦,只留下了绝对核心的模块;从 Angular 核心中移除不需要的模块可以提高性能。
Angular 的目标是 ES6,并利用 TypeScript 作为开发脚本语言,可以在编译时对类型进行检查,而不是在运行时。TypeScript 在实例化类时提供了关于类的额外信息,通过为类注释元数据。您也可以使用 ES5 和 Dart 作为开发语言。有一个改进的依赖注入版本,支持子注入器和实例范围。路由器被完全重写,引入了组件路由器。Angular 支持组件指令、装饰器指令和模板指令。$scope 已经完全从 Angular 中移除。
Angular 的架构包括模块、组件、模板、元数据、指令和服务。
NgModules
Angular 框架有各种库,这些库被分组为模块,以构建应用程序。Angular 应用程序具有模块化的特性,并通过组装各种模块来构建。模块可能包含组件、服务、函数和/或值。一些模块可能包含其他模块的集合,被称为库模块。
Angular 包,如core,common,http和router,它们以@angular为前缀,包含许多模块。我们从这些库模块中导入我们的应用程序需要的内容,如下所示:
import {Http, Response} from @angular/http';
在这里,我们从库模块@angular/http中导入Http和Response。@angular/http指的是 Angular 包中的一个文件夹。可以通过引用模块的文件名将任何定义为导出的模块导入到另一个模块中。
注意:这个导入语句是在 ES2015 中引入的,用于导入从其他模块或脚本导出的对象或函数
但是,我们也可以像我们引用@angular/http一样引用文件夹。这可以通过在文件夹中添加一个index.ts文件并添加代码来从文件夹中导出模块来实现。这是 Angular 风格指南建议的最佳实践,称为桶技术:
export * from './http';
这是在@angular/http中找到的index.ts中的导出语句。该语句意味着它导出 HTTP 中的所有模块,并且它们可以在我们的应用程序中根据需要导入。
当我们编写一个 Angular 应用程序时,我们首先定义一个AppComponent(不一定要使用相同的名称)并导出它。
组件
组件是一个具有属性和方法的类,用于在视图中使用。这些暴露给视图的属性和方法使视图能够与组件交互。我们在组件类中编写支持视图的逻辑:
例如,接下来是一个组件类 book,它具有properties标题和作者以及一个getPubName方法,该方法返回书的名称:
export class BookComponent {
title: string;
author: string;
constructor() {
this.title = 'Learning Angular for .Net Developers';
this.author = 'Rajesh Gunasundaram';
}
getPubName() : string {
return 'Packt Publishing';
}
}
注意:在本书的所有示例中,我们将使用 TypeScript。
组件的生命周期由 Angular 根据用户与应用程序的交互来管理。我们还可以添加一个根据组件状态变化触发的event方法。这些event方法称为生命周期钩子,是可选的。
我们将在第五章中详细了解组件,“实现 Angular 路由和导航”。
模板
模板可以被视为根据应用程序的 UI/UX 需求可视化的组件的表示。一个组件将有一个与之关联的模板。模板负责根据用户事件显示和更新数据:
这是一个简单的模板,用于显示书籍的标题和作者:
<h1>Book Details</h1>
<p>Title of the Book: {{title}}</p>
<p>Author Name : {{author}}</p>
在这里,用花括号括起来的标题和作者值将由相关组件实例提供。
我们将在第八章中详细讨论模板及其语法,模板和数据绑定语法。
元数据
通过使用@Component对类进行注释并传递必要的元数据,如selector、template或templateUrl,可以将类转换为组件。只有在向类附加元数据后,Angular 才会将其视为组件:
让我们重新访问一下我们之前定义的BookComponent类。除非我们对其进行注释,否则 Angular 不会将此类视为组件。TypeScript 利用 ES7 功能,提供了一种用元数据装饰类的方法,如下所示:
@Component({
selector: 'book-detail',
templateUrl: 'app/book.component.html'
})
export class BookComponent { ... }
在这里,我们用@Component装饰了BookComponent类,并附加了选择器和templateUrl的元数据。这意味着,无论在视图中的哪里,Angular 都会看到特殊的<book-detail/>标签,并创建一个BookComponent实例,并呈现分配给templateUrl的视图,即book.component.html。
TypeScript 提供的装饰器是一个函数,它接受配置参数,这些参数由 Angular 用于创建组件实例并呈现相关视图。配置参数还可能包含有关指令和提供者的信息,在创建组件时,Angular 将使其可用。
数据绑定
数据绑定是开发人员在编写代码时的核心责任之一,用于将数据绑定到用户界面,并根据用户与用户界面的交互更新变化的数据。Angular 减轻了编写大量代码来处理数据绑定的负担:
Angular 通过与模板和组件协调来处理数据绑定。模板向 Angular 提供了如何以及绑定什么的指令。在 Angular 中有两种类型的绑定:全局单向数据绑定和双向数据绑定。单向数据绑定处理从组件到 DOM 或从 DOM 到组件的数据绑定。双向数据绑定处理通信的双方,即组件到 DOM 和 DOM 到组件。
<div>Title: {{book.title}}<br/>
Enter Author Name: <input [(ngModel)]="book.author">
</div>
在这里,book.title用双大括号包裹,处理单向数据绑定。如果组件实例中有书名的值,它将显示在视图中。book.author赋给输入元素的ngModel属性,处理双向数据绑定。如果组件实例中的作者属性有值,它将被赋给输入元素,如果用户在输入控件中更改了值,更新后的值将在组件实例中可用。
我们将在第八章中详细学习数据绑定,模板和数据绑定语法。
指令
指令是用于渲染模板的指令或指导方针。一个带有@Directive装饰的类附加了元数据,被称为指令。Angular 支持三种类型的指令,即组件指令、结构指令和属性指令:
组件是带有模板的指令的一种形式,它被装饰为@Component:实际上它是一个带有模板特性的扩展@Directive:
<book-detail></book-detail>
结构指令通过添加、删除和替换 DOM 元素来操作 DOM 元素并改变它们的结构。以下代码片段使用了两个结构指令:
<ul>
<li *ngFor="let book of books">
{{book.title}}
</li>
</ul>
在这里,div元素有一个*ngFor指令,它遍历 books 集合对象并替换每本书的标题。
属性指令有助于更新元素的行为或外观。让我们使用属性指令来设置段落的字体大小。以下代码片段显示了一个使用属性指令实现的 HTML 语句:
<p [myFontsize]>Fontsize is sixteen</p>
我们需要实现一个带有@Directive注解的类,以及指令的选择器。这个类应该包含指令行为的指令:
import { Directive, ElementRef, Input } from '@angular/core';
@Directive({ selector: '[myFontsize]' })
export class FontsizeDirective {
constructor(el: ElementRef) {
el.nativeElement.style.fontSize = 16;
}
}
在这里,Angular 将查找带有[myFontsize]指令的元素,并将字体大小设置为16。
需要将myFontSize指令传递给@NgModule的 declarations 元数据,如下所示:
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { FontsizeDirective } from './fontsize.directive';
@NgModule({
imports: [ BrowserModule ],
declarations: [
AppComponent,
FontsizeDirective
],
bootstrap: [ AppComponent ]
})
export class AppModule { }
我们将在第六章中详细讨论指令,创建指令和实现变更检测。
服务
服务是用户定义的用于解决问题的类。Angular 建议只在组件中包含特定于模板的代码。组件的责任是丰富 Angular 应用程序中的 UI/UX,并将业务逻辑委托给服务。组件是服务的消费者:
应用程序特定或业务逻辑,如持久化应用程序数据、记录错误和文件存储,应该委托给服务,组件应该消费相应的服务来处理适当的业务或应用程序特定逻辑:
例如,我们可以有一个名为BookService的服务,用于插入新书籍,编辑或删除现有书籍,并获取所有可用书籍的列表。
我们将在第十一章中更多地了解服务,实现 Angular 管道。
依赖注入
当创建类的实例时,为其正常运行提供所需的依赖项称为依赖注入。Angular 提供了依赖注入的现代和改进版本:
在 Angular 中,注入器维护容器来保存依赖项的实例,并在需要时提供它们。如果依赖项的实例在容器中不可用,则注入器将创建依赖项的实例并提供它:
如前所述,组件具有与模板相关的逻辑,并且大多数情况下消费服务以执行业务逻辑。因此,组件依赖于服务。当我们为组件编写代码时,我们创建一个带有服务作为参数的构造函数。这意味着创建组件的实例取决于构造函数中的服务参数。Angular 要求注入器在组件的构造函数参数中提供服务的实例。如果可用,注入器将提供所请求服务的实例;否则,它将创建一个新的实例并提供它:
export class BookComponent {
constructor(private service: BookService) { }
}
在此代码片段中,: 符号来自 TypeScript,并不是 Angular 语法糖。private 关键字也来自 TypeScript,并且可以自动将传递的构造函数分配给类实例。类型信息用于推断要注入的类型。BookComponent 依赖于 BookService 并在构造函数中注入。因此,当创建 BookComponent 的实例时,Angular 也会确保 BookService 的实例对于 BookComponent 实例来说是可用的。
注射器知道要从提供程序创建的依赖项,并在引导应用程序或装饰组件时配置所需的依赖项类型,如下所示:
@NgModule({
imports: [BrowserModule],
declarations: [AppComponent,],
providers: [BookService],
bootstrap: [ AppComponent ]
})
export class AppModule { }
前面的代码片段将 BookService 添加为引导函数的提供程序。注射器将创建 BookService 的实例,并在整个应用程序中保持其可用性,以便在请求时注入:
@Component({
providers: [BookService]
})
export class BookComponent { ... }
前面的代码片段将 BookService 添加为组件的元数据提供程序。当遇到创建 BookComponent 实例的请求时,注射器将创建 BookService 的实例。
我们将在第十二章中详细讨论依赖注入和分层依赖注入,实现 Angular 服务。
TypeScript 的基础知识
TypeScript 是 JavaScript 的超集,是由 Microsoft 开发的开源语言。用 TypeScript 编写的代码将被编译为 JavaScript,并在运行 Node.js 的任何浏览器或服务器上执行。TypeScript 实际上是 JavaScript 的一种类型。TypeScript 有助于提高您在 JavaScript 中编写的代码的质量。如果我们使用外部库,我们需要使用导入库的类型定义文件。类型定义文件提供 JavaScript 工具支持,并通过推断代码结构来启用编译时检查、代码重构和变量重命名支持。TypeScript 正在不断发展,并不断添加与 ES2016 规范和以后对齐的其他功能。
市场上有各种编辑器可以编写 TypeScript 代码,并使用 TypeScript 编译器进行编译。这些编辑器负责将您的 TypeScript 编译为 JavaScript。这里显示了一些流行的编辑器:
-
Visual Studio
-
Visual Studio Code
-
Sublime text
-
Atom
-
Eclipse
-
Emacs
-
WebStorm
-
Vim
您还可以通过在 Node.js 命令行工具中执行以下命令来将 TypeScript 作为Node.js包下载到全局:
npm install -g typescript
要将 TypeScript 代码转译为 JavaScript,您可以在命令行工具中执行以下命令:
tsc mytypescriptcodefile.ts
在这里,tsc是 TypeScript 编译器,它将 TypeScript 文件转换为 JavaScript 文件。mytypescriptfile是您的 TypeScript 代码文件的名称,.ts是 TypeScript 文件的扩展名。执行tsc命令时,它会生成一个与.ts源文件同名的.js文件。
在本章中,我们将使用 Visual Studio Code 编辑器进行示例代码演示。让我们看看 TypeScript 的基本特性,并举例说明。
基本类型
让我们探索 TypeScript 中一些基本类型以及如何使用它们。基本类型包括原始类型,如数字、字符串、布尔和数组。JavaScript 只在运行时验证类型,但 TypeScript 在编译时验证变量类型,并大大减少了运行时类型转换问题的可能性。
数字类型
数字类型表示浮点值。它可以保存十进制、二进制、十六进制和八进制文字等值:
let decimal: number = 6;
let hex: number = 0xf00d;
let binary: number = 0b1010;
let octal: number = 0o744;
布尔类型
布尔类型是一个非常简单的类型,可以保存两个值中的任一个,true 或 false。这种布尔类型用于在变量中维护状态:
let isSaved: Boolean;
isSaved = true;
在这里,布尔类型的isSaved变量被赋值为 true。
字符串
字符串数据类型可以保存一系列字符。声明和初始化字符串变量非常简单,如下所示:
var authorName: string = "Rajesh Gunasundaram";
在这里,我们声明了一个名为authorName的变量,类型为字符串,并赋值为Rajesh Gunasundaram。TypeScript 支持用双引号(")或单引号(')括起字符串值。
数组
数组数据类型旨在保存特定类型的值的集合。在 TypeScript 中,我们可以以以下两种方式定义数组:
var even:number[] = [2, 4, 6, 8, 10];
此语句使用方括号([])在数据类型数字后声明了一个数字类型的数组变量,并将其赋值为从 2 到 10 的一系列偶数。定义数组的第二种方式如下:
var even:Array<number> = [2, 4, 6, 8, 10];
此语句使用了通用数组类型,它使用 Array 关键字后跟尖括号(<>)来包裹数字数据类型。
枚举
枚举数据类型将具有一组命名的值。我们使用枚举器为标识某些值的常量提供用户友好的名称:
enum Day {Mon, Tue, Wed, Thu, Fri, Sat, Sun};
var firstDay: Day = Day.Mon;
这里,我们有Day枚举变量,它保存了代表每周每天的一系列值。第二个语句展示了如何访问特定的枚举值,并将其赋值给另一个变量。
任意
任意数据类型是一个动态数据类型,可以容纳任何值。如果将字符串变量赋给整数变量,TypeScript 会抛出编译时错误。如果不确定一个变量将要容纳什么值,并且希望在赋值时退出编译器对类型的检查,可以使用任意数据类型:
var mixedList:any[] = [1, "I am string", false];
mixedList [2] = "no you are not";
这里,我们使用了任意类型的数组,以便它可以容纳任何类型,比如数字、字符串和布尔值。
Void
Void 实际上什么都不是。它可以用作函数的返回类型,声明这个函数不会返回任何值:
function alertMessage(): void {
alert("This function does not return any value");
}
类
类是一个可扩展的模板,用于创建具有成员变量以保存对象状态和成员函数以处理对象行为的对象。
JavaScript 只支持基于函数和基于原型的继承来构建可重用的组件。ECMAScript 6 提供了使用类的语法糖来支持面向对象编程。然而,并非所有浏览器都理解 ES6,我们需要转译器,比如 TypeScript,将代码编译成 JavaScript 并针对 ES5,这与所有浏览器和平台兼容:
class Customer {
name: string;
constructor(name: string) {
this.name = name;
}
logCustomer() {
console.log('customer name is ' + this.name;
}
}
var customer = new Customer("Rajesh Gunasundaram");
这个Customer类有三个成员:一个 name 属性,一个构造函数和一个logCustomer方法。在 customer 类外部的最后一个语句使用new关键字创建了一个 customer 类的实例。
接口
接口是定义类行为的抽象类型。接口是抽象实现的契约。接口为可以在客户端之间交换的对象提供了类型定义。这使得客户端只能交换符合接口类型定义的对象。否则,我们会得到一个编译时错误。
在 TypeScript 中,接口定义了代码内部和项目外部对象的契约。让我们看一个使用 TypeScript 的例子:
function addCustomer(customerObj: {name: string}) {
console.log(customerObj.name);
}
var customer = {id: 101, name: "Rajesh Gunasundaram"};
addCustomer(customer);
类型检查器验证了addCustomer方法调用并检查了它的参数。addCustomer期望一个具有字符串类型的 name 属性的对象。但调用addCustomer的客户端传递了一个具有两个参数id和name的对象。
然而,编译器不会检查id属性,因为它不在addCustomer方法的参数类型中。对于编译器来说,只要求的属性存在即可。
让我们重写应用interface作为参数类型的方法如下:
interface Customer {
name: string;
}
function addCustomer(customerObj: Customer) {
console.log(customerObj.name);
}
var customer = {id: 101, name: "Rajesh Gunasundaram"};
addCustomer(customer);
在这里,我们用Customer接口声明了name参数,并修改了addCustomer签名以接受Customer接口类型的参数。其余语句与前面的代码片段相同。编译器只检查对象的形状,因为 TypeScript 实现了结构类型系统。它不会检查我们传递的对象是否实现了Customer接口。它只查找参数中string类型的name属性,然后允许它存在。
使用接口的可选属性
在某些情况下,我们可能只想为最小的参数传递值。在这种情况下,我们可以将接口中的属性定义为可选属性,如下所示:
interface Customer {
id: number;
name: string;
bonus?: number;
}
function addCustomer(customer: Customer) {
if (customer.bonus) {
console.log(customer.bonus);
}
}
addCustomer({id: 101, name: "Rajesh Gunasundaram"});
在这里,通过在name属性末尾添加问号(?),将bonus属性定义为可选属性。
函数类型接口
我们刚刚看到如何在接口中定义属性。类似地,我们也可以在接口中定义函数类型。我们可以通过给出函数的签名和返回类型来在接口中定义函数类型。请注意,在下面的代码片段中,我们没有添加函数名:
interface AddCustomerFunc {
(firstName: string, lastName: string): string;
}
现在,我们有了AddCustomerFunc。让我们定义一个名为AddCustomerFunc的接口变量,并将一个具有相同签名的函数分配给它,如下所示:
var addCustomer: AddCustomerFunc;
addCustomer = function(firstName: string, lastName: string) {
console.log('Full Name: ' + firstName + ' ' + lastName);
return firstName + ' ' + lastName;
}
函数签名中的参数名称可以变化,但数据类型不能变化。例如,我们可以修改字符串类型的fn和ln函数参数如下:
addCustomer = function(fn: string, ln: string) {
console.log('Full Name: ' + fn + ' ' + ln);
}
因此,如果我们在这里改变参数的数据类型或函数的返回类型,编译器将抛出关于参数不匹配或返回类型与AddCustomerFunc接口不匹配的错误。
数组类型接口
我们还可以为数组类型定义一个接口。我们可以指定索引数组的数据类型和数组项的数据类型如下:
interface CutomerNameArray {
[index: number]: string;
}
var customerNameList: CutomerNameArray;
customerNameList = ["Rajesh", "Gunasundaram"];
TypeScript 支持两种索引类型:数字和字符串。这种数组类型接口还规定了数组的返回类型应与声明相匹配。
类类型接口
类类型接口定义了类的契约。实现接口的类应该满足接口的要求:
interface CustomerInterface {
id: number;
firstName: string;
lastName: string;
addCustomer(firstName: string, lastName: string);
getCustomer(id: number): Customer;
}
class Customer implements CustomerInterface {
id: number;
firstName: string;
lastName: string;
constructor() { }
addCustomer(firstName: string, lastName: string) {
// code to add customer
}
getCustomer(id: number): Customer {
return this;
}
}
类类型接口只处理类的公共成员。因此,不可能向接口添加私有成员。
扩展接口
接口可以被扩展。扩展接口使其共享另一个接口的属性,如下所示:
interface Manager {
hasPower: boolean;
}
interface Employee extends Manager {
name: string;
}
var employee = <Employee>{};
employee.name = "Rajesh Gunasundaram";
employee.hasPower = true;
在这里,Employee接口扩展了Manager接口,并与Employee接口共享其hasPower。
混合类型接口
混合类型接口用于当我们希望将对象既用作函数又用作对象时。如果实现了混合类型接口,我们可以像调用函数一样调用对象,或者我们可以将其用作对象并访问其属性。这种类型的接口使您能够将接口用作对象和函数,如下所示:
interface Customer {
(name: string);
name: string;
deleteCustomer(id: number): void;
}
var c: Customer;
c('Rajesh Gunasundaram');
c.name = 'Rajesh Gunasundaram';
c.deleteCustomer(101);
继承
继承是从另一个类或对象继承行为的概念。它有助于实现代码的重用性,并建立类或对象之间的关系层次结构。此外,继承帮助您转换类似的类。
JavaScript 以 ES5 为目标,不支持类,因此无法实现类继承。但是,我们可以实现原型继承而不是类继承。让我们通过示例来探索 ES5 中的继承。
首先,创建一个名为Animal的函数如下:
var Animal = function() {
this.sleep = function() {
console.log('sleeping');
}
this.eat = function() {
console.log('eating');
}
}
在这里,我们创建了一个名为Animal的函数,其中包含两个方法:sleep和eat。现在,让我们使用原型扩展这个Animal函数,如下所示:
Animal.prototype.bark = function() {
console.log('barking');
}
现在,我们可以创建一个Animal实例,并调用扩展函数bark,如下所示:
var a = new Animal();
a.bark();
我们可以使用Object.Create方法克隆父级的原型并创建一个子对象。然后,我们可以通过添加方法来扩展子对象。让我们创建一个名为Dog的对象,并从Animal继承它:
var Dog = function() {
this.bark = new function() {
console.log('barking');
}
}
现在,让我们克隆Animal的原型,并继承Dog函数中的所有行为。然后,我们可以使用Dog实例调用Animal方法,如下所示:
Dog.prototype = Object.create(animal.prototype);
var d = new Dog();
d.sleep();
d.eat();
TypeScript 中的继承
我们刚刚看到了如何使用原型在 JavaScript 中实现继承。现在,我们将看到如何在 TypeScript 中实现继承,这基本上是 ES6 继承。
在 TypeScript 中,类似于扩展接口,我们也可以通过继承另一个类来扩展类,如下所示:
class SimpleCalculator {
z: number;
constructor() { }
addition(x: number, y: number) {
this.z = this.x + this.y;
}
subtraction(x: number, y: number) {
this.z = this.x - this.y;
}
}
class ComplexCalculator extends SimpleCalculator {
constructor() { super(); }
multiplication(x: number, y: number) {
this.z = x * y;
}
division(x: number, y: number) {
this.z = x / y;
}
}
var calculator = new ComplexCalculator();
calculator.addition(10, 20);
calculator.Substraction(20, 10);
calculator.multiplication(10, 20);
calculator.division(20, 10);
在这里,我们可以使用ComplexCalculator的实例来访问SimpleCalculator的方法,因为它扩展了SimpleCalculator。
私有和公共修饰符
在 TypeScript 中,类中的所有成员默认都是public的。我们必须显式添加private关键字来控制成员的可见性,而这个有用的特性在 JavaScript 中是不可用的。
class SimpleCalculator {
private x: number;
private y: number;
z: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
addition() {
this.z = this.x + this.y;
}
subtraction() {
this.z = this.x - this.y;
}
}
class ComplexCalculator {
z: number;
constructor(private x: number, private y: number) { }
multiplication() {
this.z = this.x * this.y;
}
division() {
this.z = this.x / this.y;
}
}
请注意,在SimpleCalculator类中,我们将x和y定义为私有属性,这些属性在类外部不可见。在ComplexCalculator中,我们使用参数属性定义了x和y。这些参数属性将使我们能够在一个语句中创建和初始化成员。在这里,x和y在构造函数中创建和初始化,而不需要在其中编写任何进一步的语句。
访问器
我们还可以实现对属性的 getter 和 setter,以控制从客户端访问它们。我们可以在设置属性变量的值之前或获取属性变量的值之前拦截一个过程:
var updateCustomerNameAllowed = true;
class Customer {
private _name: string;
get name: string {
return this._name;
}
set name(newName: string) {
if (updateCustomerNameAllowed == true) {
this._name = newName;
}
else {
alert("Error: Updating Customer name not allowed!");
}
}
}
在这里,name属性的 setter 确保客户名称可以更新。否则,它会显示一个警报消息,说明这是不可能的。
静态属性
这些属性不是特定于实例的,并且通过类名而不是使用this关键字来访问:
class Customer {
static bonusPercentage = 20;
constructor(public salary: number) { }
calculateBonus() {
return this.salary * Customer.bonusPercentage/100;
}
}
var customer = new Customer(10000);
var bonus = customer.calculateBonus();
在这里,我们声明了一个名为bonusPercentage的静态变量,它在calculateBonus方法中使用Customer类名进行访问。这个bonusPercentage属性不是特定于实例的。
模块
JavaScript 是一种强大而动态的语言。使用 JavaScript 进行动态编程时,我们需要结构化和组织代码,以使其易于维护,并且还能够轻松地找到特定功能的代码。我们可以通过应用模块化模式来组织代码。代码可以分成各种模块,并且相关的代码可以放在每个模块中。
TypeScript 通过使用模块关键字更容易实现模块化编程。模块使您能够控制变量的范围、代码的可重用性和封装性。TypeScript 支持两种类型的模块:内部模块和外部模块。
命名空间
我们可以使用 namespace 关键字在 TypeScript 中创建命名空间。在命名空间下定义的所有类都将在此命名空间下进行作用域限定,并且不会附加到全局范围:
namespace Inventory {
class Product {
constructor (public name: string, public quantity:
number) { }
}
// product is accessible
var p = new Product('mobile', 101);
}
// Product class is not accessible outside namespace
var p = new Inventory.Product('mobile', 101);
为了使Product类在namespace之外可用,我们需要在定义Product类时添加export关键字,如下所示:
module Inventory {
export class Product {
constructor (public name: string, public quantity: number) { }
}
}
// Product class is now accessible outside namespace
var p = new Inventory.Product('mobile', 101);
我们还可以通过在引用文件的开头添加引用语句来跨文件共享命名空间,如下所示:
/// <reference path="Inventory.ts" />
模块
TypeScript 还支持模块,因为我们处理大量外部 JavaScript 库,这种模块化将帮助我们组织我们的代码。使用 import 语句,我们可以导入模块,如下所示:
Import { inv } from "./Inventory";
var p = new inv.Product('mobile', 101);
在这里,我们刚刚导入了先前创建的模块 Inventory,创建了Product的一个实例并将其分配给变量p。
函数
遵循 ES5 规范的 JavaScript 不支持类和模块。但是,我们尝试使用 JavaScript 中的函数式编程来限定变量和模块化。函数是 JavaScript 应用程序的构建块。
尽管 TypeScript 支持类和模块,但函数在定义特定逻辑方面起着关键作用。我们可以在 JavaScript 中定义命名函数和匿名函数,如下所示:
//Named function
function multiply(a, b) {
return a * b;
}
//Anonymous function
var result = function(a, b) { return a * b; };
在 TypeScript 中,我们使用函数箭头表示法定义参数的类型和返回类型,这也是 ES6 中支持的,如下所示:
var multiply:(a: number, b: number) => number =
function(a: number, b: number): number { return a * b; };
可选和默认参数
例如,我们有一个带有三个参数的函数,有时我们可能只在函数中传递前两个参数的值。在 TypeScript 中,我们可以使用可选参数来处理这种情况。我们可以将前两个参数定义为正常参数,将第三个参数定义为可选参数,如下面的代码片段所示:
function CustomerName(firstName: string, lastName: string, middleName?: string) {
if (middleName)
return firstName + " " + middleName + " " + lastName;
else
return firstName + " " + lastName;
}
//ignored optional parameter middleName
var customer1 = customerName("Rajesh", "Gunasundaram");
//error, supplied too many parameters
var customer2 = customerName("Scott", "Tiger", "Lion", "King");
//supplied values for all
var customer3 = customerName("Scott", "Tiger", "Lion");
在这里,middleName是可选参数,当调用function时可以忽略它。
现在,让我们看看如何在函数中设置默认参数。如果在函数中没有提供参数的值,我们可以定义它以采用配置的默认值:
function CustomerName(firstName: string, lastName: string, middleName:
string = 'No Middle Name') {
if (middleName)
return firstName + " " + middleName + " " + lastName;
else
return firstName + " " + lastName;
}
在这里,middleName是默认参数,如果调用者没有提供值,它将默认为No Middle Name。
剩余参数
使用剩余参数,您可以将值数组传递给函数。这可以用于您不确定将向函数提供多少值的情况:
function clientName(firstClient: string, ...restOfClient: string[]) {
console.log(firstClient + " " + restOfClient.join(" "));
}
clientName ("Scott", "Steve", "Bill", "Sergey", "Larry");
在这里,请注意restOfClient剩余参数前面带有省略号(...),它可以保存一个字符串数组。在函数的调用者中,只有提供的第一个参数的值将被赋给firstClient参数,其余的值将被赋给restOfClient作为数组值。
泛型
泛型对于开发可重用的组件非常有用,可以针对任何数据类型进行操作。因此,消费该组件的客户端将决定它应该对哪种类型的数据进行操作。让我们创建一个简单的函数,返回传递给它的任何数据:
function returnNumberReceived(arg: number): number {
return arg;
}
unction returnStringReceived(arg: string): string {
return arg;
}
正如你所看到的,我们需要单独的方法来处理每种数据类型。我们可以使用任意数据类型在一个函数中实现它们,如下所示:
function returnAnythingReceived (arg: any): any {
return arg;
}
这与泛型类似。但是,我们无法控制返回类型。如果我们传递一个数字,我们无法预测函数是否会返回该数字,返回类型可以是任何类型。
泛型提供了一个特殊的T类型变量。将这种类型应用于函数,使客户端能够传递他们希望这个函数处理的数据类型:
function returnWhatReceived<T>(arg: T): T {
return arg;
}
因此,客户端可以按照以下方式调用这个函数来处理各种数据类型:
var stringOutput = returnWhatReceived<string>("return this");
// type of output will be 'string'
var numberOutput = returnWhatReceived<number>(101);
// type of output will be number
请注意,在函数调用中,要处理的数据类型是通过尖括号(<>)包裹传递的。
泛型接口
我们还可以使用T类型变量定义泛型接口,如下所示:
interface GenericFunc<T> {
(arg: T): T;
}
function func<T>(arg: T): T {
return arg;
}
var myFunc: GenericFunc<number> = func;
在这里,我们定义了一个泛型接口和GenericFunc类型的myFunc变量,将数字数据类型传递给T类型变量。然后,将这个变量赋值给一个名为func的函数。
泛型类
与泛型接口类似,我们也可以定义泛型类。我们使用尖括号(<>)定义带有泛型类型的类,如下所示:
class GenericClass<T> {
add: (a: T, b: T) => T;
}
var myGenericClass = new GenericClass<number>();
myGenericClass.add = function(a, b) { return a + b; };
在这里,通过传递数字作为泛型数据类型来实例化泛型类。因此,add 函数将处理并添加作为参数传递的两个数字类型的变量。
装饰器
装饰器使我们能够通过添加行为来扩展类或对象,而无需修改代码。装饰器为类添加额外功能。装饰器可以附加到类、属性、方法、参数和访问器上。在 ECMAScript 2016 中,装饰器被提议用于修改类的行为。装饰器以@符号和在运行时调用的函数解析为装饰器名称。
以下代码片段显示了授权函数,并且它可以作为@authorize装饰器应用于任何其他类:
function authorize(target) {
// check the authorization of the use to access the "target"
}
类装饰器
类装饰器在类声明之前声明。类装饰器可以观察、修改和替换被其应用于的类的定义,通过应用于该类的构造函数。TypeScript 中ClassDecorator的签名如下:
declare type ClassDecorator = <TFunction extends Function>(target:
TFunction) => TFunction | void;
考虑一个Customer类;我们希望该类被冻结。其现有属性不应被移除,也不应添加新属性。
我们可以创建一个单独的类,可以接受任何对象并将其冻结。然后我们可以用@freezed装饰客户类,以防止向类添加新属性或删除现有属性:
@freezed
class Customer {
public firstName: string;
public lastName: string;
constructor(firstName : string, lastName : string) {
this.firstName = firstName;
this.lastName = lastName;
}
}
前面的类在firstname和lastname构造函数中接受四个参数。以下是为@freezed装饰器编写的函数的代码片段:
function freezed(target: any) {
Object.freeze(target);
}
在这里,freezed 装饰器接受target,即被装饰的Customer类,并在执行时将其冻结。
方法装饰器
方法装饰器在方法声明之前声明。此装饰器用于修改、观察或替换方法定义,并应用于方法的属性描述符。以下代码片段显示了一个简单的类,其中应用了方法装饰器:
class Hello {
@logging
increment(n: number) {
return n++;
}
}
Hello类具有increment方法,该方法递增其参数提供的数字。请注意,increment方法使用@logging装饰器进行装饰,以记录递增方法的输入和输出。以下是logging函数的代码片段:
function logging(target: Object, key: string, value: any) {
value.value = function (...args: any[]) {
var result = value.apply(this, args);
console.log(JSON.stringify(args));
return result;
}
};
}
方法装饰器函数接受三个参数:target,key和value。target保存被装饰的方法;key保存被装饰方法的名称;value是对象上存在的指定属性的属性描述符。
当调用递增方法时,logging 方法被调用,并将值记录到控制台。
访问器装饰器
访问器装饰器在访问器声明之前加上前缀。这些装饰器用于观察、修改或替换访问器定义,并应用于属性描述符。以下代码片段显示了一个简单的类,其中应用了访问器装饰器:
class Customer {
private _firstname: string;
private _lastname: string;
constructor(firstname: string, lastname: string) {
this._firstname = firstname;
this._lastname = lastname;
}
@logging(false)
get firstname() { return this._firstname; }
@logging(false)
get lastname() { return this._lastname; }
}
在这个类中,我们使用@logging装饰器修饰了firstname和lastname的获取器,并传递了boolean来启用或禁用日志记录。以下代码片段显示了@logging装饰器的函数:
function logging(value: boolean) {
return function (target: any, propertyKey: string, descriptor:
PropertyDescriptor) {
descriptor.logging = value;
};
}
logging函数将布尔值设置为日志属性描述符。
属性装饰器
属性装饰器是前缀到属性声明的。它们实际上通过添加额外的行为来重新定义被装饰的属性。在 TypeScript 源代码中,PropertyDecorator的签名如下:
declare type PropertyDecorator = (target: Object, propertyKey: string |
symbol) => void;
以下是一个类的代码片段,其中应用了属性装饰器:
class Customer {
@hashify
public firstname: string;
public lastname: string;
constructor(firstname : string, lastname : string) {
this.firstname = firstname;
this.lastname = lastname;
}
}
在这段代码中,firstname属性被@hashify属性装饰器修饰。现在,我们将看到@hashify属性装饰器函数的代码片段:
function hashify(target: any, key: string) {
var _value = this[key];
var getter = function () {
return '#' + _value;
};
var setter = function (newValue) {
_value = newValue;
};
if (delete this[key]) {
Object.defineProperty(target, key, {
get: getter,
set: setter,
enumerable: true,
configurable: true
});
}
}
_value保存了被装饰属性的值。获取器和设置器函数都可以访问变量_value,在这里我们可以通过添加额外的行为来操纵_value。我已经在获取器中连接了#来返回带有哈希标记的firstname。然后我们使用delete运算符从类原型中删除原始属性。将创建一个带有原始属性名称和额外行为的新属性。
参数装饰器
参数装饰器是前缀到参数声明的,并且它们应用于类构造函数或方法声明的函数。ParameterDecorator的签名如下:
declare type ParameterDecorator = (target: Object, propertyKey:
string | symbol, parameterIndex: number) => void;
现在,让我们定义Customer类,并使用参数装饰器来修饰参数,以使其成为必需参数,并验证值是否已被提供:
class Customer {
constructor() { }
getName(@logging name: string) {
return name;
}
}
在这里,名称参数已被@logging修饰。参数装饰器隐式接受三个输入,即具有此装饰器的类的prototype,具有此装饰器的方法的name,以及被装饰的参数的index。参数装饰器的logging函数实现如下:
function logging(target: any, key : string, index : number) {
console.log(target);
console.log(key);
console.log(index);
}
在这里,target是具有装饰器的类,key是函数名称,index包含参数索引。这段代码只是将target、key和index记录到控制台。
构建一个简单的应用程序
我假设您已经安装了 Node.js、npm 和 Visual Studio Code,并准备好用它们进行开发。现在让我们通过克隆 Git 存储库并执行以下步骤来创建一个 Angular 应用程序:
- 打开
Node.Js命令提示符并执行以下命令:
**git clone https://github.com/angular/quickstart my-angular**
使用 Visual Studio Code 打开克隆的my-angular应用程序。此命令将克隆 Angular 快速启动存储库,并为您创建一个名为 my-angular 的 Angular 应用程序,其中包含所需的所有样板代码。
my-angular 应用程序的文件夹结构。
文件夹结构和样板代码按照官方样式指南angular.io/docs/ts/latest/guide/style-guide.html进行组织。src文件夹包含与应用程序逻辑相关的代码文件,e2e文件夹包含与端到端测试相关的文件。现在不要担心应用程序中的其他文件。让我们现在专注于package.json。
- 点击
package.json文件,它将包含有关元数据和项目依赖项配置的信息。以下是package.json文件的内容:
{
"name":"angular-quickstart",
"version":"1.0.0",
"description":"QuickStart package.json from the documentation,
supplemented with testing support",
"scripts":{
"build":"tsc -p src/",
"build:watch":"tsc -p src/ -w",
"build:e2e":"tsc -p e2e/",
"serve":"lite-server -c=bs-config.json",
"serve:e2e":"lite-server -c=bs-config.e2e.json",
"prestart":"npm run build",
"start":"concurrently \"npm run build:watch\" \"npm run
serve\"",
"pree2e":"npm run build:e2e",
"e2e":"concurrently \"npm run serve:e2e\" \"npm run
protractor\" --kill-others --success first",
"preprotractor":"webdriver-manager update",
"protractor":"protractor protractor.config.js",
"pretest":"npm run build",
"test":"concurrently \"npm run build:watch\" \"karma start
karma.conf.js\"",
"pretest:once":"npm run build",
"test:once":"karma start karma.conf.js --single-run",
"lint":"tslint ./src/**/*.ts -t verbose"
},
"keywords":[
],
"author":"",
"license":"MIT",
"dependencies":{
"@angular/common":"~4.0.0",
"@angular/compiler":"~4.0.0",
"@angular/core":"~4.0.0",
"@angular/forms":"~4.0.0",
"@angular/http":"~4.0.0",
"@angular/platform-browser":"~4.0.0",
"@angular/platform-browser-dynamic":"~4.0.0",
"@angular/router":"~4.0.0",
"angular-in-memory-web-api":"~0.3.0",
"systemjs":"0.19.40",
"core-js":"².4.1",
"rxjs":"5.0.1",
"zone.js":"⁰.8.4"
},
"devDependencies":{
"concurrently":"³.2.0",
"lite-server":"².2.2",
"typescript":"~2.1.0",
"canonical-path":"0.0.2",
"tslint":"³.15.1",
"lodash":"⁴.16.4",
"jasmine-core":"~2.4.1",
"karma":"¹.3.0",
"karma-chrome-launcher":"².0.0",
"karma-cli":"¹.0.1",
"karma-jasmine":"¹.0.2",
"karma-jasmine-html-reporter":"⁰.2.2",
"protractor":"~4.0.14",
"rimraf":"².5.4",
"@types/node":"⁶.0.46",
"@types/jasmine":"2.5.36"
},
"repository":{
}
}
- 现在,我们需要在命令窗口中运行
npm install命令,导航到application文件夹中,以安装package.json中指定的所需依赖项:
执行 npm 命令以安装
package.json中指定的依赖项。
现在,您将在node_modules文件夹下添加所有依赖项,如此屏幕截图所示:
node_modules文件夹下的依赖项。
- 现在,让我们运行这个应用程序。要运行它,在命令窗口中执行以下命令:
npm start
运行此命令将构建应用程序,启动 lite 服务器,并将应用程序托管到其中。
打开任何浏览器,导航到http://localhost:3000/;您将看到以下页面显示,这是通过我们的 Angular 应用程序呈现的:
在 Visual Studio Code 中激活调试窗口。
现在让我们浏览index.html的内容。以下是index.html的内容:
<!DOCTYPE html>
<html>
<head>
<title>Hello Angular 4</title>
<base href="/">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="styles.css">
<!-- Polyfill(s) for older browsers -->
<script src="node_modules/core-
js/client/shim.min.js">
</script>
<script
src="node_modules/zone.js/dist/zone.js">
</script>
<script
src="node_modules/systemjs/dist/system.src.js">
</script>
<script src="systemjs.config.js"></script>
<script>
System.import('main.js').catch(function(err){
console.error(err); });
</script>
</head>
<body>
<my-app>My first Angular 4 app for Packt
Publishing...</my-app>
</body>
</html>
请注意,脚本是使用System.js加载的。System.js是在运行时加载模块的模块加载器。
哇!终于,我们的第一个 Angular 应用程序已经启动运行。到目前为止,我们已经看到了如何通过从 GitHub 克隆官方快速启动存储库来创建 Angular 应用程序。我们运行了应用程序,并成功在浏览器中看到了它。
总结
哇!这是一个很棒的介绍,不是吗?我们从学习 Angular 的架构开始。我们讨论了 Angular 架构的各种构件。然后我们深入了解了 TypeScript 的基础知识。我们已经看到了一些基本类型和示例。我们还学习了如何编写类,使用接口,并在类中实现它们。我们还学习了继承。
我们已经学习了通过使用模块和命名空间来构建我们的代码。我们还涵盖了一些 TypeScript 的高级主题,如修饰符、访问器、静态属性、泛型和装饰器。
最后,我们使用 Angular 和 TypeScript 创建了一个简单的应用程序。本章为您提供了使用 TypeScript 开发 Angular 应用程序所需的知识,使用了它提供的语法糖。
在下一章中,我们将讨论将 AngularJS 应用迁移到 Angular。
第二章:将 AngularJS 应用程序迁移到 Angular 应用程序
我们都知道 Angular 有很多改进,并且是从头开始设计的。因此,Angular 开发人员中最令人困扰的问题之一是如何将现有的 AngularJS 应用程序迁移到 Angular。在本章中,我们将讨论成功迁移现有 AngularJS 应用程序所推荐的最佳实践、方法和工具。
在本章中,我们将涵盖以下主题:
-
迁移过程
-
语法差异
-
升级到 Angular 的好处
-
升级到 Angular 的规则
-
使用 UpgradeAdapter 进行增量升级
-
组件迁移
-
从 AngularJS 到 Angular 的路线图
迁移过程
AngularJS 和 Angular 在语法和概念上有所不同。因此,迁移过程不仅涉及在语法层面上的代码更改,还涉及实现层面的更改。Angular 团队通过在 Angular 中提供内置工具,使开发人员更容易将 AngularJS 应用程序迁移到 Angular。在开始迁移过程之前,我们的现有 AngularJS 应用程序中有一些初步过程要做。
初步过程涉及解耦现有代码并使现有代码可维护。这个初步过程不仅为升级代码做好准备,还将改善现有的 AngularJS 应用程序的性能。
我们可以通过在同一个应用程序中同时运行 AngularJS 和 Angular,并逐个启动迁移过程,从组件开始逐步迁移。这种方法有助于迁移大型应用程序,将业务与任何影响隔离开,并在一段时间内完成升级。这种方法可以使用 Angular 升级模块实现。
Angular 和 AngularJS 之间的语法差异
Angular 在许多方面与 AngularJS 的语法不同。让我们在这里看一些。
模板中的本地变量和绑定
模板是处理应用程序的 UI 部分的视图,使用 HTML 编写。首先,我们将看到单向数据绑定的语法差异。
AngularJS:
<h1>Book Details:</h1>
<p>{{vm.bookName}}</p>
<p>{{vm.authorName}}</p>
Angular:
<h1>Book Details:</h1>
<p>{{bookName}}</p>
<p>{{authorName}}</p>
这两个代码片段都显示了单向数据绑定,将书籍和作者名称绑定到 UI,使用双大括号。然而,AngularJS 在引用控制器的属性以绑定到模板时会加上控制器的别名前缀,而 Angular 不会使用别名前缀,因为视图或模板默认与组件关联。
模板中的过滤器和管道
AngularJS 过滤器现在在 Angular 中被称为管道。在 AngularJS 中,过滤器在管道字符(|)之后使用,在 Angular 中没有语法上的变化。然而,Angular 将过滤器称为管道。
AngularJS:
<h1>Book Details:</h1>
<p>{{vm.bookName}}</p>
<p>{{vm.releaseDate | date }}</p>
Angular:
<h1>Book Details:</h1>
<p>{{bookName}}</p>
<p>{{releaseDate | date }}</p>
请注意,我们已经将日期管道或过滤器应用于releaseDate,在 AngularJS 和 Angular 之间没有语法上的变化。
模板中的本地变量
让我们看看在 AngularJS 和 Angular 中分别使用本地变量在ng-repeat和ngFor中的示例。
AngularJS:
<tr ng-repeat="book in vm.books">
<td>{{book.name}}</td>
</tr>
Angular:
<tr *ngFor="let book of books">
<td>{{book.name}}</td>
</tr>
请注意,在 AngularJS 中,本地变量 book 是隐式声明的,在 Angular 中,使用 let 关键字来定义本地变量 book。
Angular 应用程序指令
AngularJS 允许使用ng-app指令声明性地引导应用程序。但是,Angular 不支持声明性引导。它只支持通过调用引导函数并传递应用程序的根组件来显式引导应用程序。
AngularJS:
<body ng-app="packtPub">
Angular:
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
platformBrowserDynamic().bootstrapModule(AppModule);
请注意,在 AngularJS 中,Angular 模块名称packtPub已分配给ng-app指令。然而,在 Angular 中,根据执行环境,我们将AppModule传递给引导模块。请注意,AppModule是NgModule类,它是我们刚刚根据执行环境引导的应用程序的根模块。
处理 CSS 类
AngularJS 提供了ng-class指令来包含或排除 CSS 类。同样,Angular 有ngClass指令根据表达式应用或移除 CSS 类。类绑定是 Angular 提供的另一个更好的选项,用于添加或移除 CSS 类。
AngularJS:
<div ng-class="{active: isActive}">
Angular:
<div [ngClass]="{active: isActive}">
<div [class.active]="isActive">
请注意,在 Angular 中将类绑定应用于第二个div。
绑定点击事件
AngularJS 提供了基于事件的指令ng-click,可以将click事件绑定到关联控制器中的方法。Angular 通过使用可以使用( )语法来定位本机 DOM 元素来实现相同的功能,并通过将单向数据绑定与event绑定相结合来实现这一点。
AngularJS:
<button ng-click="vm.showBook()">
<button ng-click="vm.showBook($event)">
Angular:
<button (click)="showBook()">
<button (click)="showBook($event)">
请注意,在 Angular 中,目标事件 click 是在括号内定义的,并且组件中的方法在引号中指定。
模板中的控制器和组件
AngularJS 提供ng-controller指令来将控制器附加到视图,并将视图与该视图相关的控制器绑定。Angular 不支持控制器和ng-controller指令将控制器与视图关联。组件同意其关联的视图或模板,而不是反过来。
AngularJS:
<div ng-controller="PacktBooksCtrl as vm">
Angular:
@Component({
selector: 'packt-books',
templateUrl:'app/packtbooks.component.html'
})
在 AngularJS 中,我们使用立即调用的函数表达式(IIFE)来定义控制器。在 Angular 中,我们使用装饰有@Component的 TypeScript 类来定义组件,提供元数据,如selector、templateUrl等。
AngularJS:
(function () {
...
}());
Angular:
@Component({
selector: 'packt-books',
templateUrl:'app/packtbooks.component.html'
})
export class PacktBooks {
}
升级到 Angular 的好处
让我们看看升级到 Angular 的一些好处:
-
更好的性能:Angular 支持更快的变更检测、更快的引导时间、视图缓存、模板预编译等。
-
服务器端渲染:Angular 已经分成了一个应用层和一个渲染层。这使我们能够在 Web 工作者或者除了浏览器之外的服务器上运行 Angular。
-
更强大的模板:Angular 引入了新的模板语法,去除了许多指令,并与 Web 组件和其他元素更好地集成。
-
更好的生态系统:Angular 生态系统将来会变得更好,更有趣。
升级到 Angular 的策略
有不同的升级策略可用于迁移到 Angular。它们如下:
-
一次性:替换整个 AngularJS 应用程序,从一个点开始重写代码为 Angular。
-
增量:逐个服务或组件升级现有应用程序,同时运行 AngularJS 和 Angular。
如果 AngularJS 应用程序很小,那么一次性重写可能是升级的最简单和最快的方式。如果 AngularJS 应用程序较大,无法一次性重写整个代码,我们需要逐步重写,逐个组件,逐个服务。这被称为增量升级。然而,同时运行ng1和ng2会对性能产生影响。
增量升级到 Angular 的规则
如果我们遵循以下一套规则,逐步升级将会更容易:
-
每个文件实现一个组件;这有助于隔离组件并逐个迁移它们。
-
应用模块化编程并按功能排列文件夹;这将使开发人员能够集中精力逐步迁移一个功能。
-
使用模块加载器;遵循前面的规则,您将在项目中得到大量的文件。这会带来组织文件和在 HTML 页面中正确引用它们的麻烦。当您使用诸如
SystemJS、Webpack或Browserify之类的模块加载器时,它使我们能够使用 TypeScript 内置的模块系统。这使开发人员能够明确地导入或导出功能,并在应用程序的各个部分之间共享它们的代码中使用。 -
首先安装 TypeScript;在开始实际升级过程之前,最好先引入 TypeScript 编译器。这可以通过简单的安装 TypeScript 编译器来实现。
-
使用组件指令;最好使用组件指令而不是在 AngularJS 应用程序中使用
ng-controller和ng-include,这样在 Angular 中迁移组件指令将比迁移控制器更容易。
使用 UpgradeAdapter 进行增量升级
可以使用UpgradeAdapter无缝进行增量升级。UpgradeAdapter是一个可以引导和管理同时支持 Angular 和 AngularJS 代码的混合应用程序的服务。UpgradeAdapter使您能够同时运行 AngularJS 和 Angular 代码。UpgradeAdapter促进了从一个框架到另一个框架的组件和服务之间的互操作性。UpgradeAdapter将负责依赖注入、DOM 和变更检测的互操作性。
将 AngularJS 依赖注入到 Angular 中
我们可能会遇到这样的情况,即将 AngularJS 服务上的业务逻辑或任何内置服务(如$location或$timeout)注入到 Angular 代码中。这可以通过将 AngularJS 提供者升级到 Angular 并在需要的地方将其注入到 Angular 代码中来处理。
将 Angular 依赖注入到 AngularJS 中
有时可能需要将 Angular 依赖项降级,以便在 AngularJS 代码中使用它们。当我们需要将现有服务迁移到 Angular 或在 Angular 中创建新服务时,这是必要的,因为这些服务在 AngularJS 中编写的组件依赖于它们。
组件迁移
将 AngularJS 应用程序设计为以组件为中心的做法比以控制器为中心的设计更好。如果您按照这种做法开发了应用程序,那么迁移将会更容易。AngularJS 中的组件指令将具有与 Angular 组件类似的模板、控制器和绑定。但请确保您的 AngularJS 应用程序组件指令没有使用 compile、replace、priority 和 terminal 等属性。如果您的应用程序实现了具有这些属性的组件指令,那么它就不符合 Angular 架构。如果您的 AngularJS 应用程序是使用 AngularJS 1.5 开发的,并且组件是使用组件 API 实现的,那么您可能已经注意到了与 Angular 组件的相似之处。
从 AngularJS 到 Angular 的路线图
在将 AngularJS 迁移到 Angular 的过程中,遵循这个路线图是很好的:
-
JavaScript 转换为 TypeScript
-
安装 Angular 包
-
创建 AppModule
-
引导您的应用程序
-
升级您的应用程序服务
-
升级您的应用程序组件
-
添加 Angular 路由器
让我们在以下部分详细讨论它们。
JavaScript 转换为 TypeScript
通过引入 TypeScript 开始迁移过程,因为您将在 Angular 中使用 TypeScript 编写代码。将 TypeScript 安装到您的 Angular 应用程序中非常容易。运行以下命令,从npm安装 TypeScript 到您的应用程序,并将包信息保存到package.json中:
npm i typescript --save-dev
注意:由于 Angular 包仅在 npm 上可用,我们将从 npm 安装任何新包,并逐渐淘汰 Bower 包管理器
我们还需要配置 TypeScript,指示它将 TypeScript 代码转译为tsconfig.json文件中的 ES5 代码。
最后,我们需要在package.json的 scripts 部分下添加以下命令,以在后台以监视模式运行 TypeScript 编译器,这样当您进行更改时,代码将被重新编译:
"script": {
"tsc": "tsc",
"tsc:w": "tsc -w",
}
安装 Angular 包
我们需要安装 Angular 以及SystemJS模块加载器。最快的方法是从 GitHub 克隆quickstart应用程序到您的开发系统。然后将与 Angular 相关的依赖项从package.json复制到您的应用程序package.json中,并将SystemJS配置文件systemjs.config.js复制到您的应用程序根目录。完成所有这些后,然后运行以下命令来安装我们刚刚在package.json中添加的软件包:
npm install
将以下语句添加到index.html文件中。这将帮助相对 URL 从app文件夹中提供服务。这很重要,因为我们需要将index.html文件从app文件夹移动到应用程序的root文件夹中:
<base href="/app/">
现在,让我们添加 JavaScript 文件引用并通过SystemJS加载 Angular。最后,使用System.import语句加载实际应用程序:
<script src="/node_modules/core-js/client/shim.min.js"></script>
<script src="/node_modules/zone.js/dist/zone.js"></script>
<script src="/node_modules/systemjs/dist/system.src.js"></script>
<script src="/systemjs.config.js"></script>
<script>
System.import('/app');
</script>
创建 AppModule
我们需要为您的应用程序创建一个AppModule。以下AppModule类定义了最小的NgModule:
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
@NgModule({
imports: [
BrowserModule,
],
})
export class AppModule {
}
在这里,我们只是从@angular/core导入了一个NgModule和从@angular/platform-browser导入了BrowserModule。任何简单的基于浏览器的 Angular 应用程序都会有这样一个简单的AppModule。
引导您的应用程序
通过将ng-app指令附加到<html>元素来引导 AngularJS 应用程序。这在 Angular 中将不再起作用,因为引导 Angular 应用程序是不同的。
通过运行以下命令安装 Angular 升级包,并将映射添加到system.config.js:
npm install @angular/upgrade --save
该语句还会更新package.json,引用了@angular/upgrade。更新后的systemjs.config.js如下所示:
System.config({
paths: {
'npm:': '/node_modules/'
},
map: {
'ng-loader': '../src/systemjs-angular-loader.js',
app: '/app',
'@angular/upgrade/static': 'npm:@angular/upgrade/bundles/upgrade-static.umd.js'
}
})
现在从index.html文件中的<html>元素中删除ng-app属性。然后我们需要将UpgradeModule导入到AppModule中。为了以 Angular 方式引导我们的 AngularJS 应用程序,我们需要在AppModule中重写ngDoBootstrap函数如下:
import { UpgradeModule } from '@angular/upgrade/static';
@NgModule({
imports: [
BrowserModule,
UpgradeModule,
],
})
export class AppModule {
constructor(private upgrade: UpgradeModule) { }
ngDoBootstrap() {
this.upgrade.bootstrap(document.documentElement, [yourApp']);
}
}
最后,我们需要在main.ts中引导AppModule,该文件在system.config.js中配置为应用程序的入口点。main.ts的代码片段如下所示:
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app.module';
platformBrowserDynamic().bootstrapModule(AppModule);
升级应用程序服务
在 Angular 应用程序中,服务主要用于在整个应用程序中提供数据,并且这些数据将从任何服务中获取。在 AngularJS 中,我们一直在使用ngResource和%http来与服务通信和处理数据。
作为迁移的一部分,我们需要在我们使用ngResource和$http的地方使用 Angular HTTP 模块。要使用 Angular HTTP 模块,我们首先需要导入HttpModule并将其添加到AppModule的NgModule指令的导入数组中,如下所示:
import { HttpModule } from '@angular/http';
@NgModule({
imports: [
BrowserModule,
UpgradeModule,
HttpModule,
],
})
export class AppModule {
constructor(private upgrade: UpgradeModule) { }
ngDoBootstrap() {
this.upgrade.bootstrap(document.documentElement, ['yourApp']);
}
}
接下来,用装饰有@Injectable指令的新 TypeScript 类替换应用程序中基于ngResource或$http的服务的代码片段,如下所示:
@Injectable()
export class BookService {
/* . . . */
}
装饰器@Injectable将向BookService类添加特定于依赖注入的元数据,以便 Angular 知道哪些类已准备好进行依赖注入。我们需要将 HTTP 服务注入到BookService的构造函数中,并且注入的 HTTP 服务将用于访问books.json中的数据以获取书籍列表,如下所示:
@Injectable()
export class BookService {
constructor(private http: Http) { }
books(): Observable<Book[]> {
return this.http.get(`data/books.json`)
.map((res: Response) => res.json());
}
}
以下是可以作为书籍模型类型的Book接口:
export interface PhoneData {
title: string;
author: string;
publication: string;
}
这个 Angular 服务与 AngularJS 不兼容,不能直接注入。因此,我们需要将injectable方法降级以将我们的BookService插入到 AngularJS 代码中。为此,我们需要在@angular/upgrade/static中使用一个名为downgradeInjectable的方法:
declare var angular: angular.IAngularStatic;
import { downgradeInjectable } from '@angular/upgrade/static';
@Injectable()
export class BookService {
}
angular.module('core.lib')
.factory('core.lib', downgradeInjectable(BookService));
BookService的完整代码片段如下所示:
import { Injectable } from '@angular/core';
import { Http, Response } from '@angular/http';
import { Observable } from 'rxjs/Rx';
declare var angular: angular.IAngularStatic;
import { downgradeInjectable } from '@angular/upgrade/static';
import 'rxjs/add/operator/map';
export interface Book {
title: string;
author: string;
publication: string;
}
@Injectable()
export class BookService {
constructor(private http: Http) { }
books(): Observable<Book[]> {
return this.http.get(`data/books.json`)
.map((res: Response) => res.json());
}
}
angular.module('core.lib')
.factory('phone', downgradeInjectable(BookService));
最后,我们需要在NgModule下注册BookService作为提供者,以便 Angular 将BookService的实例保持准备好在整个应用程序中注入。app.module.ts的更新代码片段如下所示:
import { BookService } from './book.service';
@NgModule({
imports: [
BrowserModule,
UpgradeModule,
HttpModule,
],
providers: [
BookService,
]
})
export class AppModule {
constructor(private upgrade: UpgradeModule) { }
ngDoBootstrap() {
this.upgrade.bootstrap(document.documentElement, [yourApp']);
}
}
升级你的应用程序组件
作为升级组件的一部分,我们需要创建一个降级的 Angular 组件,以便它可以被 AngularJS 代码消耗。以下是降级的 Angular 组件的代码片段:
declare var angular: angular.IAngularStatic;
import { downgradeComponent } from '@angular/upgrade/static';
@Component({
selector: 'book-list',
templateUrl: './book-list.template.html'
})
export class BookListComponent {
}
angular.module('bookList')
.directive(
'bookList',
downgradeComponent({component: BookListComponent}) as
angular.IDirectiveFactory
);
在这里,我们向 TypeScript 编译器指示directive工厂是从downgradeComponent返回的。现在我们需要通过将其添加到AppModule的entryComponents来注册downgradeComponent,如下所示:
import { BookListComponent } from './components/book-list.component';
@NgModule({
imports: [
BrowserModule,
UpgradeModule,
HttpModule
],
declarations: [
BookListComponent,
],
entryComponents: [
BookListComponent,
})
export class AppModule {
constructor(private upgrade: UpgradeModule) { }
ngDoBootstrap() {
this.upgrade.bootstrap(document.documentElement, ['yourApp']);
}
}
phone-list.template.html的更新模板如下所示:
<ul>
<li *ngFor="let book of books">
{{book.title}}
</li>
</ul>
这里ng-repeats已被替换为*ngFor。
添加 Angular 路由器
Angular 已经完全重新定义了路由器。逐模块升级路由器模块是一个好的做法。Angular 有一个特殊的标签<router-outlet>,用于显示或加载路由视图。这应该在根组件的模板中。所以对于你的应用程序,我们需要创建一个名为AppComponent的根组件:
import { Component } from '@angular/core';
@Component({
selector: 'your-app',
template: '<router-outlet></router-outlet>'
})
export class AppComponent { }
这是一个指令,如果在网页中找到<your-app>,就将根组件加载到其中。因此,让我们用应用程序元素<your-app>替换index.html中的ng-view指令:
<body>
<your-app></your-app>
</body>
我们需要为路由创建另一个NgModule,代码片段如下所示:
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { HashLocationStrategy, LocationStrategy } from '@angular/common';
import { BookListComponent } from './components/book-list.component';
const routes: Routes = [
{ path: '', redirectTo: 'books', pathMatch: 'full' },
{ path: 'books', component: BookListComponent }
];
@NgModule({
imports: [ RouterModule.forRoot(routes) ],
exports: [ RouterModule ],
providers: [
{ provide: LocationStrategy, useClass: HashLocationStrategy },
]
})
export class AppRoutingModule { }
在路由对象中定义了单个路由,还为应用程序的空路径或根路径设置了默认路由。然后,我们将路由对象传递给RouterModule.forRoot,以便RouterModule来处理它。我们使用HashLocationStrategy来指示RouterModule在 URL 的片段中使用一个哈希(#)。
最后,让我们更新AppModule来导入AppRoutingModule,并且我们已经到了一个阶段,可以移除ngDoBootstrap,因为现在一切都是 Angular。以下是AppModule的更新代码片段:
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HttpModule } from '@angular/http';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { BookService } from './services/book.service';
import { BookListComponent } from './components/book-list.component';
@NgModule({
imports: [
BrowserModule,
HttpModule,
AppRoutingModule
],
declarations: [
AppComponent,
BookListComponent
],
providers: [
BookService
],
bootstrap: [ AppComponent ]
})
export class AppModule {}
请注意,我们将AppRoutingModule添加到NgModule属性的导入集合中,以便应用程序路由将在AppModule中注册。
总结
干得好!好多东西,不是吗?!我们开始学习在 Angular 中进行迁移。
然后,我们看到了将 AngularJS 迁移到 Angular 应用程序的各种方法和最佳实践。
接下来,我们讨论了使用升级适配器进行增量升级。
最后,我们详细了解了从 AngularJS 迁移到 Angular 的路线图。
在下一章中,我们将讨论 Angular CLI,这是 Angular 的命令行界面。