从今天开始学习Angular
在这篇文章中,将解释用于Web应用开发的JavaScript框架Angular。
Angular是什么
Angular(角)是由Google开发的面向客户端的JavaScript框架。它是一个全栈框架,几乎支持了构建Web应用所需的所有功能。你可以使用Angular CLI(命令行界面)来执行项目的生成、构建和部署。
目前(2019年7月),最新版本是v8.0.0(主要版本)。Google宣布每半年升级一次版本,预计2019年10月会有v9.x版本升级,并且2020年5月会有v10.x版本的更新。
Angular的开发初期(v1.x)是以“AngularJS”为产品名进行开发的,但在升级到v2.x时,进行了根本性的架构重新审视,并将名字改为“Angular”。
AngularJS是基于JavaScript的框架,而Angular改为基于TypeScript,并且对语法进行了更新,因此AngularJS和Angular之间不兼容。
※网上有很多关于AngularJS时期的信息,请不要被误导。。。
Angular的开发是在Node.js环境下进行的。这是因为Angular的开发支持功能本身是用TypeScript(编译成JavaScript)编写的。
此外,Angular本身可以使用npm进行安装。
Node.js需要v8.x以上,npm需要v5.x以上。注意,如果版本过旧会出错。
安装
安装Angular就等于安装Angular CLI。
通过使用Angular CLI,可以生成Angular项目并享受该框架的各种好处。
npm install @angular/cli -save
项目的创建
要生成Angular项目,请使用Angular CLI执行以下命令。
执行命令后,将在当前目录下创建一个新的子目录,并在其中存储构建应用程序所需的源代码。
同时,Angular所依赖的外部库也会自动安装。
ng new <プロジェクト名>
※「ng」是调用Angular CLI命令的命令,但在许多地方也经常用来表示Angular。
在生成的项目文件夹中,包含了表示为Angular项目的 angular.json,以及表示为TypeScript项目的 tsconfig.json。
Angular 自身的模块和外部库等都存储在 node_modules 文件夹中。
实际上,作为应用程序源代码的是 src 文件夹。
/
├ node_modules/
└ src/
├ app/
│ ├ app.component.html
│ ├ app.component.css
│ ├ app.component.ts
│ ├ app.component.spec.ts
│ └ app.module.ts
├ assets/
├ environments/
├ dist/
├ index.html
├ main.ts
├ styles.css
└ etc...
执行应用程序
可以通过Angular CLI从命令行方便地进行开发中的操作确认。需要在Angular项目的根文件夹中运行。
ng serve --open
使用open选项,构建完成后会自动打开浏览器。
在ng serve内部,使用了webpack的开发虚拟服务器功能(webpack-dev-server),支持热部署(自动检测源代码更改并自动重新加载),因此源代码的修改结果会直接反映到浏览器中。
应用程序的构建
当你要将Angular源码实际运作为应用程序时,需要进行编译。
执行以下命令将会在内部运行webpack的处理过程,生成编译和打包的源码。
必须在Angular项目的根文件夹中执行。
ng build --configuration=production
–configuration选项可以指定构建环境设置。通常,我们会定义各个环境设置,如production、staging、development等。只有在production构建中,可以使用–prod的简写来进行指定。关于构建环境和源代码的输出设置等配置,可以在angular.json文件中定义。
組件
Angular 是一个基于组件的框架。
组件可以是特定的界面,也可以是共用的部件,或者是某个具有特定功能的一块“整体”。
通过组合不同的组件来构建界面是 Angular 的 Web 应用程序的特点。
(例如,可以通过组合头部组件、导航组件和主内容组件来构建界面等)
要创建组件,请在 Angular CLI 中运行以下命令。
生成的组件源代码将在以同名的文件夹中整理起来。
ng generate component hoge
执行该命令会生成源代码。
CREATE src/app/hoge/hoge.component.html (19 bytes)
CREATE src/app/hoge/hoge.component.spec.ts (614 bytes)
CREATE src/app/hoge/hoge.component.ts (262 bytes)
CREATE src/app/hoge/hoge.component.css (0 bytes)
UPDATE src/app/app.module.ts (467 bytes)
「ng generate」不仅可以创建源文件,还可以将新创建的组件注册到模块中。
为了使Angular能够使用组件,必须在某个模块中声明这些组件。
关于Angular中的模块将在后面的章节中进行解释。
└ app/
└ hoge/
└ hoge.component.html
└ hoge.component.css
└ hoge.component.ts
└ hoge.component.spec.ts
Angular是一个遵循Model-View-ViewModel(模型-视图-视图模型)的MVVM模式的框架。
-
- Model アプリケーションのコアとなるドメインロジック(ユーザからは見えない)
-
- View ブラウザ上でユーザと対話するロジック(ユーザから見える)
- ViewModel Model と View の橋渡しを行うロジック
每个组件单元负责 View-ViewModel 层。
生成的文件中,.html 文件和 .css 文件是 View,.ts 文件是 ViewModel。
组件的使用
将生成的组件集成到应用程序中有几种方法可以使用。
基本的方法有以下三种。
-
- HTML(ビュー)の中でコンポーネントセレクタを記述する
-
- ルーティングで遷移する
- アプリケーションのスクリプトで動的にコンポーネントを生成する
当描述组件选择器时
在Angular组件的TypeScript类文件(.ts文件)中,需要使用@Component装饰器来记录组件的元信息(组件本身的信息)。
@Component({
selector: 'app-hoge',
templateUrl: './hoge.component.html',
styleUrls: ['./hoge.component.css']
})
templateUrl、styleUrls可以直接在.ts文件中以“template”和“style”的形式编写HTML和CSS。
但是,从责任分离的角度来看,最好尽量避免这样做。
例中的组件可以在Angular的View中使用app-hoge标签进行描述和使用。
显然,app-hoge标签不是HTML标准标签,它并不是浏览器可以解释的标签。
它仅仅是Angular独自扩展HTML的标签,并且在实际应用中会由Angular转换为可以被浏览器解释的形式。
<app-hoge></app-hoge>
如果通过路由进行转换的话
通常情况下,当我们在网页上点击超链接时,可以跳转到另一个网页。
同时,浏览器会记录页面的访问历史,我们可以通过点击“返回”和“前进”按钮来进行导航。
在 SPA 中(不仅限于 Angular),几乎不会存在”转到另一个网页”的概念。
通过将与请求的 URL 相对应的组件与当前显示的组件交换,模拟了转到另一个网页的效果。
同时,通过 JavaScript 在浏览器的历史记录中进行操作,就可以像常规页面导航那样使用”后退”和”前进”按钮。
这个过程被称为路由,并且为执行路由操作的模块被称为路由器。
由于Angular Router是框架中的重要功能,所以我们将在后面的章节中进行解释。
如果需要动态生成组件
通过编写脚本,您可以动态生成组件。
(例如,可以考虑在按钮按下时打开对话框等用例。)
生成组件需要使用以下三个类。
ComponentFactoryResolver コンポーネントクラスから ComponentFactory を特定するクラス
ComponentFactory コンポーネントを生成するクラス
ViewContainerRef View への参照を提供するクラス
ComponentFacroty 的任务是将组件类转换为浏览器可见的形式。
然而,Angular 应用程序中包含了无数个组件,相应的 ComponentFactory 实例也有无数个。
ComponentFactoryResolver 的任务是从这许多 ComponentFactory 实例中找到与所需组件对应的工厂。
通过 ComponentFactory 生成的组件将通过设置到 View 中而出现在浏览器上。
提供访问 View 的方法的是 ViewContainerRef。
生成实际组件的流程如下。
-
- 通过将组件类作为参数传递给ComponentFactoryResolver的resolveComponentFactory方法,获取相应的ComponentFactory。
- 通过将ComponentFactory作为参数传递给ViewContainerRef的createComponent方法,在View中生成组件。
我们不会直接操作 Factory。
ViewContainerRef自身有一个用于生成组件的方法,如果直接将 Factory 传给它,ViewContainerRef会为我们处理一切。
但是,动态组件的生成往往会使脚本逻辑变得有些复杂。
如果没有特别的限制,最好使用Angular Material的MatDialogModule。
服务
Angular 可以将特定的逻辑作为服务与组件分离。
将功能提取为服务,并在多个组件中进行调用和共享。
这样可以将组件的职责最小化,提高可维护性。
同时,可以避免业务逻辑与特定组件耦合,提高重用性。
Angular 并且支持依赖注入(DI:Dependency Injection)。这个框架具有一个叫做注入器的机制,根据组件类或服务类的需求,可以从外部注入(注入)服务。服务的使用者只需在构造函数中声明他们想要使用的服务,就可以在类中使用服务的功能,而不需要手动实例化服务。
要创建服务,请在Angular CLI中运行以下命令。
ng generate service hoge
CREATE src/app/hoge.service.spec.ts (323 bytes)
CREATE src/app/hoge.service.ts (133 bytes)
└ app/
└ hoge.service.ts
使用服务
为了使用服务,首先需要向注入器(Injector)提供服务的创建方式。
将服务的创建方式作为providers元信息来告诉注入器。对于每个服务,都需要定义它的providers元信息。
在下面的例子中,通过在@Component装饰器的providers中指定导入的HogeService,注入器就可以实例化并注入HogeService。
src/app/hoge/hoge.component.ts的原始位置。
import { Component } from '@angular/core';
import { HogeService } from '../hoge.service';
@Component({
selector: 'app-hoge',
templateUrl: './hoge.component.html',
styleUrls: ['./hoge.component.css'],
providers: [HogeService]
})
export class HogeComponent {
constructor(private hoge: HogeService) {
this.hoge.out();
}
}
/src/app/hoge.service.ts的中文原生释义为:
import { Injectable } from '@angular/core';
@Injectable()
export class HogeService {
public out(): void {
console.log('this is HogeService.');
}
}
注射器在每个应用程序的层次结构中存在。
换句话说,根据在哪里定义providers元数据,可以改变可使用服务的范围。
在上述示例中,由于在组件级别上定义,因此只有知道如何创建HogeService的注射器才能访问该组件的注射器,其他组件无法使用HogeService。
如果服务注入器在使用服务的级别无法注入服务,则会回溯到祖先级别的注入器来寻找可注入的服务。
如果在应用程序的根级别定义提供程序,则该应用程序的所有组件都可以使用该服务。
除非有特殊原因,否则最好在根级别进行定义。
要将提供程序设置为根级别,可以使用以下任一方法。
-
- root モジュール(app.module.ts)の providers にサービスを記述する
- サービスの@Injectable デコレータの providedIn メタ情報に「root」と記述する
在提供者中,有几种可以指定的服务创建方法。
useClass 指定したクラスをインスタンス化(new)して注入する
useValue 指定したインスタンスやオブジェクトを注入する
useFactory 指定した関数の戻り値を注入する
上面示例中的providers: [HogeService]是以下语法的糖衣语法。
providers: [{ provide: HogeService, useClass: HogeService }];
provide属性是用于标识依赖注入令牌(名称)的服务实例。
通过使用该属性,只需指定HogeService,就可以确定要注入的HogeService的实例。
如下所示,provider 的本质是一个对象,它同时持有名称和服务创建方法的集合。
注入器依靠这些 provider 对象来创建服务,创建出的服务(实例或对象)在注入器内共享。
綁定
在Angular中,View(.html文件)和ViewModel(.ts文件)相互配合来构建组件。
View和ViewModel之间通过数据绑定来交换信息。
数据绑定有以下三种类型。
イベントバインディング View から ViewModel へ情報を渡す
プロパティバインディング ViewModel から View へ情報を渡す
双方向バインディング View と ViewModel で同期する
事件绑定
事件绑定可以将在视图中发生的某些事件传递给视图模型。
可以将其理解为类似于HTML中的onClick、onLoad等事件。
要将它绑定到HTML按钮的点击事件上,可以这样写。
<button (click)="hogeFnc()">Click Me!</button>
通过事件绑定,可以从网页操作中执行组件的函数。
在传递参数给函数时,可以使用模板引用变量。
通过给视图元素附加 # 前缀以及任意的名称,可以访问元素的值。
<button (click)="hogeFnc(hogeForm.value)">Click Me!</button>
<input type="text" #hogeForm />
public hogeFnc(txt: string): void {
console.log(`hogeForm has value : ${txt}`);
}
属性绑定
在属性绑定中,可以将ViewModel中的信息嵌入到View中。
要绑定到HTML文本表单的value属性,可以这样写。
<input type="text" [value]="hoge" />
在上面的示例中,ViewModel 将 hoge 变量设置为文本表单的值。
请注意事件绑定和括号的形式不同。
-
- イベントバインディング :View から ViewModel へ ⇒ ()丸括弧
プロパティバインディング :ViewModel から View へ ⇒ []角括弧
在上述示例中,即使更改文本表单的值,ViewModel中的”hoge”变量的值也不会改变。
文本表单的值仅仅是”设置hoge变量的值”,并且不会将文本表单的更改反馈给hoge变量。
要将更改反馈到hoge变量中,可以使用双向绑定。(如下所述)
如果您想要简单地输出变量的内容,而不将其绑定到任何HTML元素上,可以按照以下方式进行描述。
{{ hoge }}
在中文中,我们称这种用大括号来写的语法为插值(Interpolation)。
花括号内不仅可以输出变量,还可以写表达式和函数。
此外,您还可以通过使用属性绑定来更改视图的样式。
这包括将样式绑定到style属性和将类绑定到class属性。
在样式绑定中,您需要指定 [style.CSS属性名] 并设置相应的值。
在类绑定中,您需要指定 [class.CSS类名] 并使用布尔值来控制类的启用或禁用。
<p [style.color]="hogeStyle">hoge works!</p>
<p [class.hogeblue]="hogeClass">hoge works!</p>
public hogeStyle: string = 'red';
public hogeClass: boolean = true;
.hogeblue {
color: blue;
}
在样式绑定的情况下,ViewModel 需要持有样式信息。这并不是理想的状态,从责任分离的角度来看(实际上,样式应该集中在 CSS 中)。所以,除非有特殊的理由,最好使用类绑定。
双向绑定
通过将事件绑定和属性绑定结合起来,可以实现 View 和 ViewModel 的同步。
以下是最简单的双向绑定示例。
<input type="text" #hogeForm [value]="hoge" (input)="hoge = hogeForm.value" />
在文本框中设置hoge变量的值后,如果文本框的值发生变化,那么hoge变量的值也会相应改变。虽然这种方法可以实现双向绑定,但过于冗长。因此,通常情况下会使用NgModel指令。Angular提供了FormsModule模块来方便处理表单操作,而NgModel指令是FormsModule模块的一个功能。通过编写NgModel指令,可以将表单元素作为FormControl实例处理。
使用NgModel指令需要导入FormsModule模块,然后在视图中编写NgModel指令。
import { FormsModule } from '@angular/forms';
@NgModule({
declarations: [
~ 省略 ~
],
imports: [
FormsModule,
~ 省略 ~
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
<input type="text" [(ngModel)]="hoge" />
通过将HTML描述设置为[(ngModel)]和hoge变量,该表单的FormControl实例将与hoge变量关联,并立即同步更改。
事件绑定的语法是带有()括号的,属性绑定的语法是带有[]括号的,而这个语法是结合了它们的形式[()](被称为双向绑定语法)。
指令
通过在HTML中编写Angular可以解释的特定关键字,可以改变视图的输出结果。这个机制被称为指令。
指令分为两种大类。
構造ディレクティブ View の構造(レイアウト)そのものを変化させる
属性ディレクティブ View の属性を操作して見た目や振る舞いを変化させる
构造指令
常常使用的包括根据条件添加/删除DOM元素的NgIf和通过循环生成DOM元素的NgFor。
<div *ngIf="hoge === fuga">あいうえお</div>
<div *ngFor="let item of ['AAAA','BBBB','CCCC']">{{ item }}</div>
如果条件表达式(或布尔变量)为真,则NgIf会生成DOM元素;如果条件表达式为假,则NgIf不会生成DOM元素。
通常情况下,可以使用CSS的display: none;等方式来消除特定元素,但这样CSS会在生成DOM元素后再隐藏它。
由于NgIf不会生成DOM元素,因此在性能方面具有优势。
在NgFor的情况下,会根据指定的可迭代值循环生成DOM元素。
基本上是用for-of语法来编写的,将循环中提取的值存入临时变量中并使用。
由于NgFor无法执行固定次数循环(类似于”let i=0; i < 10; i++”),因此必须传递可迭代的值。(可将其类比为ForEach)
属性指令
经常使用的是NgClass,它用于一次性应用CSS类;还有NgStyle,它用于一次性应用CSS样式。
<div [ngClass]="hogeClass">あいうえお</div>
.classA {
color: red;
}
.classB {
font-weight: bold;
}
.classC {
background-color: yellow;
}
import { Component } from '@angular/core';
@Component({
selector: 'app-hoge',
templateUrl: './hoge.component.html',
styleUrls: ['./hoge.component.css']
})
export class HogeComponent {
public hogeClass = {
classA: true,
classB: true,
classC: true
};
}
在NgClass中,我们描述一个对象,其中包含与CSS类名匹配的键值对。如果键值对的Value值为true,则应用相应的类;如果Value为false,则不应用该类。
<div [ngStyle]="hogeStyle">あいうえお</div>
import { Component } from '@angular/core';
@Component({
selector: 'app-hoge',
templateUrl: './hoge.component.html',
styleUrls: ['./hoge.component.css']
})
export class HogeComponent {
public hogeStyle = {
color: 'blue',
'text-decoration': 'underline',
'font-size': '20pt'
};
}
在NgStyle中,需要编写包含与CSS属性名相匹配的键值对的对象。
样式将根据所指定的值应用。
在使用NgStyle时,ViewModel需要包含样式信息。
这不是一种理想的状态,从责任分离的角度来看(样式应该集中在CSS中),因此除非有特殊原因,否则最好使用NgClass。
自定义指令
除了 Angular 本身已经实现的内置指令外,你也可以自定义指令。
就像创建组件和服务一样,你可以使用 Angular CLI 生成指令。
ng generate directive directive/mydirective
CREATE src/app/directive/mydirective.directive.spec.ts (244 bytes)
CREATE src/app/directive/mydirective.directive.ts (151 bytes)
UPDATE src/app/app.module.ts (689 bytes)
<div appMydirective>あいうえお</div>
import { Directive, ElementRef, OnInit } from '@angular/core';
@Directive({
selector: [appMydirective]
})
export class MydirectiveDirective implements OnInit {
constructor(private elRef: ElementRef) {}
ngOnInit() {
const el = this.elRef.nativeElement;
el.style.backgroundColor = 'green';
el.style.color = 'yellow';
}
}
在上述的例子中,我们将指令选择器命名为appMydirective。
通过在元素中写入appMydirective的方式,可以发挥指令的作用,从而改变元素的样式。
为了引用已应用指令的元素,我们使用了ElementRef类。
管子
在Angular中,我们使用简单的函数(输入→格式化→输出)的机制来在视图中对显示内容进行格式化,这个机制被称为管道。
类似于常见的命令行界面中的管道操作符,我们可以使用“|”来连接函数并调用它,以对数据进行处理。
以下是一些常用的内置管道示例。
<p>{{ 'This is Pipe' | uppercase }}</p>
<p>{{ 'This is Pipe' | lowercase }}</p>
<!-- 0文字目から4文字抽出 -->
<p>{{ 'This is Pipe' | slice:0:4 }}</p>
<p>{{ 10000 | currency }}</p>
<p>{{ 10000 | currency:'JPY' }}</p>
<p>{{ 10000 | number }}</p>
<!-- 整数桁10桁以上、小数点以下2桁 -->
<p>{{ 10000 | number:'10.2' }}</p>
<p>{{ 0.5 | percent }}</p>
<p>{{ dateObj | date }}</p>
<p>{{ dateObj | date:'yyyy年 MM月 dd日' }}</p>
※dateObj:日期对象
定制管道
与指令一样,我们也可以自己创建管道。
就像创建组件和服务一样,我们可以通过Angular CLI进行生成。
ng generate pipe pipe/mypipe
CREATE src/app/pipe/mypipe.pipe.spec.ts (187 bytes)
CREATE src/app/pipe/mypipe.pipe.ts (205 bytes)
UPDATE src/app/app.module.ts (828 bytes)
{{ '吾輩は猫である。名前はまだ無い。' | mypipe }}
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'mypipe'
})
export class MypipePipe implements PipeTransform {
transform(value: string): string {
if (typeof value === 'string') {
return value.replace('猫', '犬');
} else {
return value;
}
}
}
在上面的例子中,将管道选择器命名为mypipe。
通过将数据导入到mypipe中,可以替换传递的字符串中的关键字。
实际上,管道的转换操作是通过transform方法来完成的。
Angular通过调用实现了PipeTransform接口的transform方法来进行数据转换。
需要注意的是,由于管道处理会在视图中频繁执行,因此应尽量避免执行重量级的操作。
此外,在管道的处理过程中,最好始终检查传递的参数类型是否符合预期。
路由器
在Angular中,我们通过组合组件来构建页面,但通过替换显示组件来表示页面跳转的动作(例如用户点击按钮、运行函数等),可以实现页面的转换。
这个功能被称为路由,并由RouterModule模块提供。
要进行路由,需要进行以下准备工作。
-
- 创建一个AppRoutingModule模块来描述基本要素。
-
- 设置根路由。
-
- 描述路由出口。
- 描述进行过渡的触发器。
描述基础要素
首先,在index.html文件的head标签中使用标签来指示应用程序的基准页面的位置。
通过将href属性设置为“/”,将此页面指示为路由器中应用程序的根。
需要注意的是,如果使用Angular CLI生成应用程序的模板(ng new),则会自动写入index.html文件中。
<base href="/" />
创建 AppRoutingModule 模块。
為了使用路由功能,需要導入Angular的RouterModule。
雖然可以直接在根模塊中導入,但通常會將路由專用的模塊準備好並導入根模塊。
由於需要進行初始設定工作,將模塊分離可以避免根模塊的代碼變得複雜。
模塊名可以任意命名,但通常推薦使用「AppRoutingModule」作為慣例。
ng generate module app-routing --flat --module=app
–flat 固有フォルダを作らず、src/app 直下に作成する
–module 指定したモジュールのインポートに自動的に追加する
CREATE src/app/app-routing.module.ts (194 bytes)
UPDATE src/app/app.module.ts (1119 bytes)
由于路由模块是在整个应用程序中使用的模块,所以通常会将其放在AppModule(根模块)的相同文件夹中。
另外,在上述示例中,我们直接将生成的模块导入到AppModule中。
如果不使用module选项,则需要手动导入。
设定路径
Angular内置的RouterModule模块不能直接使用。
需要教给它作为进行路由的地图的路由(Route)。
刚刚生成的AppRoutingModule模块是一个空的状态,什么也没有写。
我们将导入RouterModule模块,并进行设置,使其成为一个路由模块。
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router'; // RouterModuleとRoutes型をインポート
import { HogeComponent } from './hoge/hoge.component'; // サンプル用コンポーネント #1
import { FugaComponent } from './fuga/fuga.component'; // サンプル用コンポーネント #2
const routes: Routes = [
// https://example.jp/hoge にアクセスされたらHogeComponentを表示する
{ path: 'hoge', component: HogeComponent },
// https://example.jp/fuga にアクセスされたらFugaComponentを表示する
{ path: 'fuga', component: FugaComponent },
// 上記以外のURLにアクセスされたら https://example.jp/ に遷移する
{ path: '**', redirectTo: '/' }
];
@NgModule({
imports: [RouterModule.forRoot(routes)], // ルートを読み込む
exports: [RouterModule]
})
export class AppRoutingModule {}
首先,我们导入RouterModule模块和Routes类型。
Routes类型是Route类型(一个路由的1个条目)的数组。
Route类型可以指定以下属性之一,例如:
**
を指定するとワイルドカードになる。pathMatchURL の比較ルール。・prefix(前方一致):デフォルト
・full(完全一致)componentpath が一致した場合に表示するコンポーネントredirectTopath が一致した場合にリダイレクトする URLcanActivateアクティブ化可能か判定するガード(後述)の配列loadChildren遅延ロードを行うためのコールバック
路由的优先级按照指定的顺序来确定。
当使用通配符时,如果顺序不正确,所有访问都会被通配符捕获,所以务必在最后进行设置。
通过在 RouterModule 模块的 forRoot 方法中加载创建的路由(routes 常量),可以将其作为路由器运行。
描述路由器插座
在路由器中被替代的组件占位符是”router-outlet”指令。通过在视图的任意位置进行编写,可以将与URL相对应的组件与路由器进行绑定。
<router-outlet></router-outlet>
描述触发迁移的行为
通常情况下,我们通过在Web页面上点击链接或按钮来进行页面跳转。为了定义这些跳转触发器,有以下几种方法可供选择。
routerLinkディレクティブを記述する
Routerのnavigateメソッドを実行する
如果使用routerLink指令,需要将该指令添加到视图的a标签或button标签中。
<div>
<a routerLink="hoge">Go To Hoge</a>
<button routerLink="hoge">Go To Hoge</button>
</div>
<div>
<a routerLink="fuga">Go To Fuga</a>
<button routerLink="fuga">Go To Fuga</button>
</div>
<router-outlet></router-outlet>
在上述的例子中,无论是点击链接还是按钮,都会分别转到”/hoge”和”/fuga”。(”/hoge”和”/fuga”的内容会显示在下方的router-outlet中。)
如果在脚本中完成某些处理后希望进行页面跳转,可以使用以下方式调用 navigate 方法。
<div>
<input type="text" #password />
<button (click)="goNext(password.value)">Go</button>
</div>
<router-outlet></router-outlet>
import { Component } from '@angular/core';
import { Router } from '@angular/router'; // 遷移を行うためのRouterをインポート
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'ngtest';
constructor(private router: Router) {}
goNext(pass: string): void {
if (pass === 'piyo') {
this.router.navigate(['hoge']); // 入力されたパスワードが「piyo」なら「/hoge」に遷移
} else {
console.log('NG! パスワードが誤っています');
}
}
}
保安员
通过路由器,用户可以自由切换和引用应用程序内的组件。然而,在一般的应用程序中,有一些功能仅允许特定用户进行引用(例如登录功能就是最典型的例子)。为了控制针对这种特殊功能的路由方式,Angular 拥有一种名为“守卫”的功能。守卫可以被视为用于判断目标转换是否可用的函数。如果守卫返回 NG,则路由器会取消转换。
ng generate guard guard/auth
import { Injectable } from '@angular/core';
import {
CanActivate,
ActivatedRouteSnapshot,
RouterStateSnapshot
} from '@angular/router';
@Injectable({
providedIn: 'root'
})
export class AuthGuard implements CanActivate {
canActivate(
next: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): boolean {
// ここに何か処理を書く
return true;
}
}
在上述例子中,路由器通过调用CanActivate接口的canActivate方法来进行判断。
创建的保卫者会在路由对象中指定。
这样,在尝试转到相应路径时,将执行保卫者检查并判断是否可以进行转换。
const routes: Routes = [
{ path: "hoge", component: HogeComponent, canActivate: [AuthGuard] }, // AuthGuardのチェックがOKなら参照可能
{ path: "fuga", component: FugaComponent, canActivate: [AuthGuard, UserGuard, DateGuard, ....]}, // すべてのガードのチェックがOKなら参照可能
{ path: "piyo", component: PiyoComponent }, // ガードのチェックなしで参照可能
{ path: "**", redirectTo: "/" }
];
进行检查的守卫是通过数组来指定的。
如果有多个守卫连续排列,只有当所有守卫的判断都为OK时,才能被访问。
检查的顺序将按照指定的数组顺序进行,即使在中间的守卫出现NG的情况,所有守卫的检查仍将被执行。
HTTP 客户端
在Angular中,提供了用于与服务器进行异步通信的HTTP客户端。要使用HTTP客户端,需要导入HttpClientModule。通常情况下,由于HTTP客户端是应用程序全局所需的功能,因此应在根模块中进行导入。
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http';
@NgModule({
imports: [
BrowserModule,
// BrowserModule のあとに HttpClientModule をインポート
HttpClientModule
],
declarations: [AppComponent],
bootstrap: [AppComponent]
})
export class AppModule {}
通过导入`HttpClientModule`,可以使用`HttpClient`类。
实际的HTTP访问是通过`HttpClient`类的方法来实现的。
在组件中使用HttpClient是不推荐的。如果在组件中使用HttpClient,组件的职责会变得多样化并且难以管理。一般来说,应该准备一个独立的(封装的)服务类来使用HttpClient,从而与组件分离。
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
const URL: string = 'https://hogehoge.jp/api/backend-fuga';
@Injectable()
export class ServerAccessService {
constructor(private http: HttpClient) {}
getRequest(): Observable<string> {
return this.http.get(URL);
}
postRequest(param: string): Observable<string> {
const httpOptions = {
'Content-Type': 'text/plain; charset=UTF-8'
};
return this.http.post(URL, param, httpOptions);
}
}
为了捕捉异步通信的响应,HttpClient 方法的返回值是 Observable 类型的值。
通过订阅(subscribe)此值,应用程序可以接收到服务器的响应。
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
private data: string;
constructor(private accessService: ServerAccessService) {}
getData() {
this.accessService.get().subscribe(
(data: string) => (this.data = data), // 通信成功時
error => console.log('error!!') //通信失敗時
);
}
}
除了订阅(subscribe)外,你也可以使用async管道(View)。
在这种情况下,当接收到服务器的响应时,Angular会自动读取数据并将其绑定到视图上。
与订阅不同,使用async管道的情况下,Angular会在组件被销毁时自动取消订阅。
然而,async管道不适用于处理具有复杂数据结构的响应。
此外,在接收到响应之前,会将其视为null,并且需要注意在视图上不会显示任何内容。
@Component({
selector: "app-root",
templateUrl: "./app.component.html",
styleUrls: ["./app.component.css"]
})
export class AppComponent {
private data$: Observable<string>; // Observable変数であることを示すために、慣例的に「$」を末尾に付与します
constructor(private accessService: ServerAccessService) {}
getData() {
this data$ = this.accessService.get();
}
}
<div>
<h1>サーバから受信した値</h1>
{{ data$ | async }}
</div>
RxJS
RxJS 是一個可以簡潔地描述處理隨時間變化的(不知道何時發生的)數據,例如異步處理和事件處理的程式庫。
Angular 在其核心功能的依賴庫中使用 RxJS,因此在 Angular 應用程序中也無法避免使用 RxJS 的思維方式。
在RxJS中,以下三个重要概念被引入。
Observable
(観測可能なもの)遅延プッシュされる複数の値のコレクション。Observer
(観測者)プッシュされた値の処理方法を定義したオブジェクト。以下の 3 つの関数を持つ。
・next(成功時の処理)
・error(エラー時の処理)
・complete(完了時の処理)
Subscribe
(購読)Observable と Observer を紐づけ、Observable の観測を開始すること。
根据生产者断续提供的数据作出反应,并进行某种处理的设计思想称为响应式编程。正如RxJS所指的“JavaScript的响应式扩展库”,它符合这种响应式编程的理念。在上述表中,Observable的角色相当于生产者,Observer的角色相当于消费者。
对于 Observable 类型的数据,通过 subscribe 方法提供 Observer,并按照 Observer 所具有的处理方法来处理推送的值。
在下面的示例中,我们为依次推送并完成的包含值 “1”、”2″、”3″ 的 Observable 分别观察并执行不同的处理操作。
import { Observable } from 'rxjs';
const observable = new Observable(observer => {
observer.next(1);
observer.next(2);
observer.next(3);
observer.complete();
});
const observer1 = {
next(data) {
console.log('subscription1 がデータを受信しました: ' + data);
},
error(err) {
console.error('subscription1 でエラーが発生しました: ' + err);
},
complete() {
console.log('subscription1 の購読が完了しました。');
}
};
const observer2 = {
next(data) {
console.log('subscription2 got new value: ' + data);
},
error(err) {
console.error('subscription2 got new error: ' + err);
},
complete() {
console.log('subscription2 finished.');
}
};
const subscription1 = observable.subscribe(observer1);
const subscription2 = observable.subscribe(observer2);
subscription1 がデータを受信しました: 1main.js:591:25
subscription1 がデータを受信しました: 2main.js:591:25
subscription1 がデータを受信しました: 3main.js:591:25
subscription1 の購読が完了しました。main.js:597:25
subscription2 got new value: 1 main.js:602:25
subscription2 got new value: 2 main.js:602:25
subscription2 got new value: 3 main.js:602:25
subscription2 finished. main.js:608:25
可以直接在subscribe方法中编写Observer对象。除了将Observer对象作为参数外,还可以通过传递1至3个函数来实现subscribe。在这种情况下,这些函数将依次被视为next、error和complete。另外,除了next方法外,其他方法都是可选的,可以不指定而仍然可以工作。
const subscription1 = observable.subscribe({
next(data) {
console.log('subscription1 がデータを受信しました: ' + data);
},
error(err) {
console.error('subscription1 でエラーが発生しました: ' + err);
},
complete() {
console.log('subscription1 の購読が完了しました。');
}
});
const subscription2 = observable.subscribe(
data => console.log('subscription2 がデータを受信しました: ' + data),
err => console.log('subscription2 でエラーが発生しました: ' + err),
() => console.log('subscription2 の購読が完了しました。')
);
const subscription3 = observable.subscribe(data =>
console.log('subscription3 がデータを受信しました: ' + data)
);
subscription1 がデータを受信しました: 1main.js:591:25
subscription1 がデータを受信しました: 2main.js:591:25
subscription1 がデータを受信しました: 3main.js:591:25
subscription1 の購読が完了しました。main.js:597:25
subscription2 がデータを受信しました: 1main.js:600:68
subscription2 がデータを受信しました: 2main.js:600:68
subscription2 がデータを受信しました: 3main.js:600:68
subscription2 の購読が完了しました。main.js:600:182
subscription3 がデータを受信しました: 1main.js:601:68
subscription3 がデータを受信しました: 2main.js:601:68
subscription3 がデータを受信しました: 3main.js:601:68
当调用complete方法或Error方法时,观测将结束,但也有Observable在不调用complete方法的情况下持续提供数据进行观测。
在这种情况下,数据的使用者需要在任意时间结束观测。
要结束观测,需要执行Subscription对象的unsubscribe方法。
Subscription是表示观测行为本身的对象(可以将其类比为订阅杂志的契约)。
当通过subscribe方法开始观测时,将返回其作为返回值。
// 観測の開始
const subscription = observable.subscribe({
next(data) {
// 何かの処理
},
error(err) {
// 何かの処理
},
complete() {
// 何かの処理
}
});
// 観測の終了
subscription.unsubscribe();
作为 RxJS 的特点之一,它可以像处理数组一样处理间断地提供的数据。
例如,如果有一个 Observable 按顺序提供 “1”,”2″,”3″ ,我们可以像操作数组 [1, 2, 3] 一样对其进行操作。
这种数据操作功能称为操作符。
在下面的示例中,我们从 Observable 提供的数据中提取出奇数,并将其乘以100。
import { Observable, pipe } from 'rxjs';
import { map, filter } from 'rxjs/operators';
const observable = new Observable<number>(observer => {
observer.next(1);
observer.next(2);
observer.next(3);
observer.complete();
});
const sub = observable
.pipe(
filter(data => data % 2 !== 0), // 奇数のみ取り出す
map(data => data * 100) // 100倍する
)
.subscribe(data => console.log(data));
100 main.js:591:232
300 main.js:591:232
在上述的例子中,filter和map是运算符。
要连续使用多个运算符,可以使用pipe方法。
Angular材料
近年,Google提倡的设计方法是材质设计。这是一种通过给由”纸”和”墨水”组成的元件添加反射光和阴影效果,以更自然地表现”物体存在”的设计方法。
根据 Material Design 的原则,Angular Material 是一个 UI 组件(按钮、表单等)库。这个库是由 Angular 开发团队创建的,属于官方的 Angular 家族。因此,它能够提供与 Angular 框架高度兼容、不影响在 View(.html 文件)中的处理,并且具有良好的互操作性的 UI 组件。
Angular Material 以 npm 包的形式发布于外部。可以通过 npm install 进行安装,也可以通过 ng add 命令进行安装。由于 ng add 可以轻松完成以下相关的初始设置,因此官方推荐使用 ng add。
-
- 依存ライブラリ(Component Dev Kit、Angular Animations)のインストール
-
- カラーテーマの設定
-
- HammerJS(ジェスチャー認識ライブラリ)のインストール
-
- BrowserAnimationsModule のインポート
-
- index.html への Roboto フォントの追加
-
- index.html へのマテリアルデザインアイコンフォントの追加
-
- root CSS の調整
-
- body の margin を 0 に
-
- html と body の height を 100%に
- デフォルトフォントの設定
ng add @angular/material
在Angular Material中,我们称适用于其的配色方案为主题。虽然你可以自己创建主题,但Angular Material提供了以下4种预设主题作为内置主题。
-
- deeppurple-amber.css
-
- indigo-pink.css
-
- pink-bluegrey.css
- purple-green.css
可以通过在根CSS中引入或将其添加到Angular.json的构建设置(全局样式)中来应用到应用程序中,这是您想要使用的主题(如果您是通过ng add安装的话,请使用后一种方法)。
"build": {
~ 省略 ~
"styles": [
"./node_modules/@angular/material/prebuilt-themes/deeppurple-amber.css",
"src/styles.css"
],
~ 省略 ~
}
Angular Material的UI组件(如按钮和复选框等)都是模块化的,因此在应用程序中使用它们需要首先导入相应的模块。
如果在根模块中进行导入,那么它们就可以在整个应用程序中使用,所以除非有特殊的理由,最好在根模块中进行导入。
import { MatButtonModule, MatCheckboxModule } from '@angular/material';
@NgModule({
~ 省略 ~
imports: [
MatButtonModule,
MatCheckboxModule
],
~ 省略 ~
})
通过将导入的 UI 组件写入 View 中,您可以在屏幕上放置 Angular Material 的 UI 组件。
以下是一些常用组件的示例。
当然,您也可以对这些组件使用指令或进行绑定。
<div>
<button mat-raised-button>Button</button>
</div>
<div>
<mat-form-field>
<input matInput type="text" />
</mat-form-field>
</div>
<div>
<mat-checkbox>Checkbox label</mat-checkbox>
</div>
<div>
<mat-radio-group>
<mat-radio-button>Radio A</mat-radio-button>
<mat-radio-button>Radio B</mat-radio-button>
<mat-radio-button>Radio C</mat-radio-button>
</mat-radio-group>
</div>
<div>
<mat-icon>home</mat-icon>
</div>
如果使用侧滑切换和滑块等支持手势操作的UI组件,需要导入HammerJS库。
如果使用ng add安装了Angular Material,则会一并确认是否安装HammerJS。
如果在ng add时拒绝了HammerJS的安装,或者通过npm install安装了Angular Material等其他方式,可以通过另外从npm安装来使用HammerJS。
npm install --save hammerjs
当安装完成后,需要在应用程序的入口点(main.ts)进行导入。
这样,您就可以使用具有手势操作的 UI 组件了。
import 'hammerjs';
考试
Angular 支持以下自动测试。
Jasmineテストフレームワーク + Karmaテストランナーによる自動ユニットテスト
Protractorテストフレームワークによる自動 e2e(End-to-End)テスト
单元测试
单元测试在Angular中由Jasmine和Karma执行。
解释和执行测试代码是由Jasmine的功能完成的。
Karma利用Jasmine的功能在浏览器中进行测试,并生成报告。
此外,Karma会在执行过程中检测到源代码的更改并重新执行测试。
您可以通过Angular CLI命令来自动执行单元测试。
执行以下命令即可开始测试,并执行在项目中定义的.spec.ts文件中的测试。
ng test
$ ng test
Browserslist: caniuse-lite is outdated. Please run next command `npm update`
30% building 15/15 modules 0 active22 11 2019 15:36:30.973:WARN [karma]: No captured browser, open http://localhost:9876/
22 11 2019 15:36:31.026:INFO [karma-server]: Karma v4.1.0 server started at http://0.0.0.0:9876/
22 11 2019 15:36:31.027:INFO [launcher]: Launching browsers Chrome with concurrency unlimited
30% building 17/17 modules 0 active22 11 2019 15:36:31.089:INFO [launcher]: Starting browser Chrome
22 11 2019 15:36:35.035:WARN [karma]: No captured browser, open http://localhost:9876/
22 11 2019 15:36:35.132:INFO [Chrome 78.0.3904 (Windows 10.0.0)]: Connected on socket WxxEvvEsAy5bM4a2AAAA with id 10021485
Chrome 78.0.3904 (Windows 10.0.0): Executed 12 of 12 SUCCESS (2.265 secs / 2.225 secs)
TOTAL: 12 SUCCESS
TOTAL: 12 SUCCESS
TOTAL: 12 SUCCESS
=============================== Coverage summary ===============================
Statements : 100% ( 41/41 )
Branches : 100% ( 2/2 )
Functions: 100% ( 17/17 )
Lines: 100% ( 32/32 )
================================================================================
要输出测试代码覆盖率,请在 ng test 命令中添加 –code-coverage 选项,或者在 Angular.json 文件中进行以下设置。
"test": {
~ 省略 ~
"codeCoverage": true
~ 省略 ~
}
当输出代码覆盖率时,将会在项目中创建一个名为 coverage 的文件夹,并生成一个以 HTML 形式的报告。
同时,还会像上述例子一样在控制台显示覆盖率摘要。
测试代码(.spec.ts)的结构如下所示。
-
- import
-
- describe
-
- beforeEach
-
- it
-
- expect
- afterEach
在import中,我们会导入所需的模块和要测试的类。
describe用于表示一个完整的测试场景。我们也可以在describe中嵌套另一个describe。
beforeEach、it、afterEach可以在describe中任意多次地进行记录。
表示实际测试案例的是 it。
在每个 it 前后,我们会记录初始化和结束的处理,这些处理会被 beforeEach 和 afterEach 执行。
在 it 中,我们必须始终写下预期结果验证处理 expect。
可以任意写下多个 expect,但如果其中一个失败,那么该 it 就会变成 NG。
import { TestBed, async } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { AppComponent } from './app.component';
describe('C-1: ルートコンポーネントのテスト', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [RouterTestingModule],
declarations: [AppComponent]
}).compileComponents();
}));
it('C-1-1: コンポーネントが生成できること', () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app).toBeTruthy();
});
it(`C-1-2: タイトルが「test-sample」であること`, () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app.title).toEqual('test-sample');
});
it('C-1-3: H1見出しのテキストが「Welcome to test-sample!」であること', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.debugElement.nativeElement;
expect(compiled.querySelector('h1').textContent).toContain(
'Welcome to test-sample!'
);
});
});
expect 后面跟着 toBeTruthy、toEqual、toContain 等函数被称为Matcher。
通过将 expect 的参数(被验证的对象)和 Matcher 的参数(比较的对象)进行对比,如果满足条件,expect 就会成功。
在Angular的单元测试中,我们可以使用名为TestBed的测试虚拟NgModule。TestBed可以像普通的NgModule一样设置declarations,imports,providers等,并且如果需要使用模拟服务或模拟组件,可以将它们加载到TestBed中。
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { TweetListComponent } from './tweet-list.component';
import { Component, Input } from '@angular/core';
import { By } from '@angular/platform-browser';
@Component({
selector: 'app-tweet',
template: '<div>Stub Component</div>'
})
class StubTweetComponent {
@Input() tweet: any;
}
describe('C-3: ツイートリストコンポーネントのテスト', () => {
let component: TweetListComponent;
let fixture: ComponentFixture<TweetListComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [TweetListComponent, StubTweetComponent]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(TweetListComponent);
component = fixture.componentInstance;
const stubValue = [
{
user: 'test 1',
content: `xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx`,
retweet: 12345,
like: 67890
}
];
component.tweetList = stubValue;
fixture.detectChanges();
});
it('C-3-1: ツイートリストがすべて表示されていること', () => {
const element: HTMLElement = fixture.debugElement.nativeElement;
const tweetComponents = element.querySelectorAll('app-tweet');
expect(tweetComponents.length).toBe(3);
});
it('C-3-2: リツイートイベントに応じてRT数が増加すること', () => {
let beforeCount: number;
const spy = spyOn(component, 'retweet').and.callThrough();
component.tweetList.forEach(tweet => {
beforeCount = tweet.retweet;
fixture.debugElement
.query(By.css('.tweet'))
.triggerEventHandler('retweet', tweet);
expect(spy).toHaveBeenCalled();
expect(tweet.retweet).toBe(beforeCount + 1);
});
});
});
Jasmine提供了一个功能,可以检测特定的对象方法或函数的执行,并将其替换为任意的操作。
在上述例子(测试用例C-3-2)中,我们对TweetListComponent实例的retweet方法进行了拦截,并使用callThrough方法(调用原始方法)。
通过在expect中检查spy对象的toHaveBeenCalled方法,我们可以检查该方法是否被调用。
上述例子只是通过spy转发,但是通过让spy返回预先确定的返回值,可以实现模拟和存根。
端到端测试
在浏览器上进行的测试应用程序的构建测试被称为e2e(End-to-End)测试。
在单元测试中,我们将准备模拟和存根,并进行仅关注测试对象行为的测试,而e2e测试旨在检查整体的运行情况,包括服务器端。(相当于综合测试阶段)
在e2e测试中,我们使用Protractor框架,而Protractor在内部使用Jasmine的测试功能。
因此,测试代码的语法等可以像单元测试一样编写。
您可以使用Angular CLI的命令来执行自动的端到端测试。
执行以下命令时,会运行app.e2e-spec.ts文件(文件名可更改)中定义的测试代码,并生成测试报告。
ng e2e
$ ng e2e
[15:09:32] I/config_source - curl -oC:\Users\user\Desktop\DEV\test-sample\node_modules\protractor\node_modules\webdriver-manager\selenium\chrome-response.xml https://chromedriver.storage.googleapis.com/
[15:09:33] I/update - chromedriver: file exists C:\Users\user\Desktop\DEV\test-sample\node_modules\protractor\node_modules\webdriver-manager\selenium\chromedriver_78.0.3904.105.zip
[15:09:33] I/update - chromedriver: unzipping chromedriver_78.0.3904.105.zip
[15:09:34] I/update - chromedriver: chromedriver_78.0.3904.105.exe up to date
Browserslist: caniuse-lite is outdated. Please run next command `npm update`
Date: 2019-11-22T06:09:51.188Z
Hash: 13114b74aad04ac5cd11
Time: 8615ms
chunk {main} main.js, main.js.map (main) 28 kB [initial] [rendered]
chunk {polyfills} polyfills.js, polyfills.js.map (polyfills) 248 kB [initial] [rendered]
chunk {runtime} runtime.js, runtime.js.map (runtime) 6.08 kB [entry] [rendered]
chunk {styles} styles.js, styles.js.map (styles) 16.7 kB [initial] [rendered]
chunk {vendor} vendor.js, vendor.js.map (vendor) 3.94 MB [initial] [rendered]
** Angular Live Development Server is listening on localhost:4200, open your browser on http://localhost:4200/ **
i 「wdm」: Compiled successfully.
[15:09:53] I/launcher - Running 1 instances of WebDriver
[15:09:53] I/direct - Using ChromeDriver directly...
DevTools listening on ws://127.0.0.1:55696/devtools/browser/05988235-51e7-497a-920a-346cd5940efc
Jasmine started
workspace-project App
√ should display welcome message
Executed 1 of 1 spec SUCCESS in 1 sec.
[15:10:00] I/launcher - 0 instance(s) of WebDriver still running
[15:10:00] I/launcher - chrome #01 passed