angular4学习笔记
文章目录
HTTP
可观察对象 (Observable)
Http
服务中的每个方法都返回一个 HTTP Response
对象的Observable
实例
一个可观察对象是一个事件流,我们可以用数组型操作符来处理它。
模板
TODO: ngModel
在标识符前加上#
就能声明一个模板引用变量。
模板引用变量通常用来引用模板中的某个DOM元素,它还可以引用Angular组件或者Web Component
获取input输入
1 | ({ |
获取Form的引用
1 | <form #demoForm="ngForm" (ngSubmit)="onSubmit(demoForm)"> |
从模板视图中获取元素
ViewChild是属性装饰器,用来从模板视图中获取匹配的元素。视图查询在 ngAfterViewInit 钩子函数调用前完成,因此在 ngAfterViewInit 钩子函数中,才能正确获取查询的元素。
@ViewChild 使用模板变量名
1 | import { Component, ElementRef, ViewChild, AfterViewInit } from '@angular/core'; |
@ViewChild 使用模板变量名及设置查询条件
1 | import { Component, TemplateRef, ViewChild, ViewContainerRef, AfterViewInit } from '@angular/core'; |
@ViewChild 使用类型查询
child.component.ts
1 | import { Component, OnInit } from '@angular/core'; |
app.component.ts
1 | import { Component, ViewChild, AfterViewInit } from '@angular/core'; |
结构性指令
<ng-template>
是一个 Angular 元素,用来渲染HTML。 它永远不会直接显示出来。 事实上,在渲染视图之前,Angular 会把<ng-template>
及其内容替换为一个注释。
等同于js中的模板文件:1
2
3
4
5<script id="myTemplate" type="text/ng-template">
<div>
My awesome template!
</div>
</script>
或者:1
2
3
4
5<template id="myTemplate">
<div>
My awesome template!
</div>
</template>
要获取上面的模板并实例化它,我们需要使用简单的JavaScript:1
2
3
4
5
6
7<div id="host"></div>
<script>
let template = document.querySelector('#myTemplate');
let clone = document.importNode(template.content, true);
let host = document.querySelector('#host');
host.appendChild(clone);
</script>
使用trackBy,作为items的key
TODO: ngFor index count odd…
Angular把对象或keys与特定的DOM节点相互联系起来。如此当对象改变或UI需要重新渲染时,框架能更有效的处理。Angular的ngFor
默认使用对象的引用(?)做检查。但我们可以自定义跟踪方案:
1 | <ul> |
1 | trackById(index, contact) { |
trackBy
通知angular使用每个contact的id来建立联系。
Notes: 如果使用数组的index来作为id,那重新排序之后,就会导致DOM对象的销毁和重建而不是复用
依赖注入
使用依赖注入就不需要在class内部去新建实例对象,这样可以更好的支持功能扩展,例如构造方法的改变(参数的增加)
属性装饰器
宿主元素(Host Element)
为了将Angular组件渲染成DOM树,需要将Angular组件与一个DOM元素相关联,这样的DOM元素称之为:宿主元素。
宿主元素的概念适用于指令(应用指令的元素)和组件(组件本身)。
组件可以与宿主元素进行如下方式的交互:
- 监听宿主元素事件 —>
@HostListener
- 更改宿主元素属性 —> 指令如
ngClass
,[attr.style]以及@HostBinding
- 调用宿主元素方法
@HostListener
HostListener是属性装饰器,用来为宿主元素添加事件监听
HostListener 装饰器定义
1 | /** |
HostListener 装饰器应用
demo.directive.ts1
2
3
4
5
6
7
8
9
10
11import { Directive, HostListener } from '@angular/core';
'button[counting]'}) ({selector:
class CountClicks {
numberOfClicks = 0;
'click', ['$event.target']) (
onClick(btn) {
console.log('button', btn, 'number of clicks:', this.numberOfClicks++);
}
}
demo.component.ts1
2
3
4
5
6
7import { Component} from '@angular/core';
({
selector: 'app',
template: '<button counting>Increment</button>',
})
class App {}
此外,我们也可以监听宿主元素外,其它对象产生的事件,如window
或document
对象。例如:1
2
3
4
5
6
7
8
9
10...
'document:click', ['$event']) (
onClick(btn: Event) {
if (this.el.nativeElement.contains(event.target)) {
this.highlight('yellow');
} else {
this.highlight(null);
}
}
...
Host Event Listener
也可以在指令的 metadata 信息中,设定宿主元素的事件监听信息,具体示例如下:
demo.diretive.ts1
2
3
4
5
6
7
8
9
10
11
12
13
14
15import { Directive, HostListener } from '@angular/core';
({
selector: 'button[counting]',
host: {
'(click)': 'onClick($event.target)'
}
})
class CountClicks {
numberOfClicks = 0;
onClick(btn) {
console.log('button', btn, 'number of clicks:', this.numberOfClicks++);
}
}
@HostBinding
HostBinding 是属性装饰器,用来动态设置宿主元素的属性值。
HostBinding 装饰器定义
1 | /** |
HostBinding 装饰器应用
demo.directive.ts1
2
3
4
5
6
7'[ngModel]'}) ({selector:
class NgModelStatus {
constructor(public control:NgModel) {}
'class.valid') get valid() { return this.control.valid; } (
'class.invalid') get invalid() { return this.control.invalid; } (
}
demo.component.ts1
2
3
4
5
6
7 ({
selector: 'app',
template: `<input [(ngModel)]="prop">`,
})
class App {
prop;
}
Host Property Bindings
也可以在指令的 metadata 信息中,设定宿主元素的属性绑定信息,具体示例如下:
demo.directive.ts1
2
3
4
5
6
7
8
9
10
11
12
13 ({
selector: '[ngModel]',
host: {
'[class.valid]': 'valid',
'[class.invalid]': 'invalid'
}
})
class NgModelStatus {
constructor(public control:NgModel) {}
get valid() { return this.control.valid; }
get invalid() { return this.control.invalid; }
}
测试
如果是测试一个只有内联模板和内联样式的控件,就不需要async
函数。angular4官网的测试demo如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39// banner-inline.component.ts
import { Component } from '@angular/core';
({
selector: 'app-banner',
template: '<h1>{{title}}</h1>'
})
export class BannerComponent {
title = 'Test Tour of Heroes';
}
// banner-inline.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { BannerComponent } from './banner-inline.component';
describe('BannerComponent (inline template)', () => {
let comp: BannerComponent;
let fixture: ComponentFixture<BannerComponent>;
let de: DebugElement;
let el: HTMLElement;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ BannerComponent ], // declare the test component
});
fixture = TestBed.createComponent(BannerComponent);
comp = fixture.componentInstance; // BannerComponent test instance
// query for the title <h1> by CSS element selector
de = fixture.debugElement.query(By.css('h1'));
el = de.nativeElement;
});
});
TestBed测试台
TestBed
(测试台)是Angular测试工具集中的首要概念。
它创建Angular测试模块(一个@NgModule
类),我们可以通过调用它的configureTestingModule
方法来为要测试的类生成模块环境。
其效果是,你可以把被测试的组件从原有的应用模块中剥离出来,把它附加到一个动态生成的Angular测试模块上,而该测试模块可以为这些测试进行特殊裁剪。
这里的元数据对象只是声明了要测试的组件BannerComponent
。
这个元数据中没有imports
属性,这是因为:
(a) 默认的测试模块配置中已经有了BannerComponent
所需的一切,
(b) BannerComponent
不需要与任何其它组件交互。
在beforeEach
中调用configureTestingModule
,以便TestBed
可以在运行每个测试之前都把自己重置回它的基础状态。
基础状态中包含一个默认的测试模块配置,它包含每个测试都需要的那些声明(组件、指令和管道)以及服务提供商(有些是Mock版)。
createComponent 方法
在配置好TestBed
之后,我们可以告诉它创建一个待测组件的实例。
在这个例子中,TestBed.createComponent
创建了一个BannerComponent
的实例,并返回一个组件测试夹具。
createComponent
方法封闭了当前的TestBed
实例,以免将来再配置它。
我们不能再调用任何TestBed
的方法修改配置:不能调用configureTestingModule
或任何override
…方法。
如果这么做,TestBed
就会抛出错误。
ComponentFixture、DebugElement 和 query(By.css)
createComponent
方法返回ComponentFixture,用来控制和访问已创建的组件所在的测试环境。
这个fixture提供了对组件实例自身的访问,同时还提供了用来访问组件的DOM元素的DebugElement对象。
title
属性被插值到DOM的<h1>
标签中。 用CSS选择器从fixture的DebugElement
中query`
`元素。
query方法接受predicate函数,并搜索fixture的整个DOM树,试图寻找第一个满足predicate函数的元素。
By类是Angular测试工具之一,它生成有用的predicate。 它的By.css
静态方法产生标准CSS选择器 predicate,与JQuery选择器相同的方式过滤。
测试程序
再每个测试程序之前,Jasmin都一次运行beforeEach
函数:1
2
3
4
5
6
7
8
9
10it('should display original title', () => {
fixture.detectChanges();
expect(el.textContent).toContain(comp.title);
});
it('should display a different test title', () => {
comp.title = 'Test Title';
fixture.detectChanges();
expect(el.textContent).toContain('Test Title');
});
测试带有外部模板的组件
在实际应用中,BannerComponent
的行为和刚才的版本相同,但是实现方式不同。
它有一个外部模板和CSS文件,通过templateUrl
和styleUrls
属性来指定。
1 | import { Component } from '@angular/core'; |
这些测试有一个问题。 TestBed.createComponent
方法是同步的。
但是Angular模板编译器必须在创建组件实例之前先从文件系统中读取这些值,而这是异步的。
以前测试内联模板时使用的设置方式不适用于外部模板。
BannerComponent
测试的设置方式必须给Angular模板编译器一些时间来读取文件。
以前放在beforeEach
中的逻辑被拆分成了两个beforeEach
调用。 第一个beforeEach
处理异步编译工作。
1 | import { async } from '@angular/core/testing'; |
TestBed.compileComponents
方法会异步编译这个测试模块中配置的所有组件。
在这个例子中,BannerComponent
是唯一要编译的组件。
当compileComponents
完成时,外部组件和css文件会被“内联”,而TestBed.createComponent
会用同步的方式创建一个BannerComponent
的新实例。
NOTE: WebPack用户不用调用compileComponents,因为它会在构建过程中自动内联模板和css,然后执行测试
测试有依赖的组件
组件经常依赖其他服务。
WelcomeComponent
为登陆的用户显示一条欢迎信息。它从注入的UserService
的属性得知用户的身份:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27// welcome.component.ts
import { Component, OnInit } from '@angular/core';
import { UserService } from './model/user.service';
({
selector: 'app-welcome',
template: '<h3 class="welcome" ><i>{{welcome}}</i></h3>'
})
export class WelcomeComponent implements OnInit {
welcome = '-- not initialized yet --';
constructor(private userService: UserService) { }
ngOnInit(): void {
this.welcome = this.userService.isLoggedIn ?
'Welcome, ' + this.userService.user.name :
'Please log in.';
}
}
// welcome.component.sepc.ts
TestBed.configureTestingModule({
declarations: [ WelcomeComponent ],
// providers: [ UserService ] // NO! Don't provide the real service!
// Provide a test-double instead
providers: [ {provide: UserService, useValue: userServiceStub } ]
});
提供服务替身
注入真实的service组件有可能很麻烦,而且不能确保能获取到相同的测试对象。
实际上,服务的替身(stubs、fakes、spies或者mocks)通常会更加合适。
所以在真实的UserService
的位置创建和注册UserService
替身,会让测试更加容易和安全。
1 | userServiceStub = { |
获取注入的服务
最安全并总是有效的获取注入服务的方法,是从被测试的组件的注入器获取。 组件注入器是fixture的DebugElement
的属性。
1 | // UserService actually injected into the component |
TestBed.get 方法
你可以通过TestBed.get
方法来从根注入器中获取服务。 它更容易被记住,也更加简介。
但是只有在Angular使用测试的根注入器中的那个服务实例来注入到组件时,它才有效。
幸运的是,在这个测试套件中,唯一的UserService
提供商就是根测试模块,所以像下面这样调用TestBed.get
很安全:
1 | // UserService from the root injector |
测试程序
1 | beforeEach(() => { |
生命周期
ExpressionChangedAfterItHasBeenCheckedError
error
constructor -> OnChanges -> onInit -> doCheck -> afterContentInit -> afterContentChecked -> afterViewInit -> afterViewChecked -> onDestroy
angular从app-root开始做变化检查(change detection),检查所有的数据绑定,然后开始检查子组件,
并且其只做一次检查(而不是循环检查直到稳定),父组件的数据绑定将不会再次检查,
所以此时应用处在一个不稳定状态(inconsistent state。
在开发模式中,angular进行两个变化检查,第二次将会确认数据绑定是否发生改变,如果有就抛出这个
错误。
所以,如果在afterViewInit中修改了数据绑定就会出现这个错误。
解决方案:
- setTimeout / Promise.resolve().then
- ChangeDetectorRef#detectChanges() 强制检查
- 如果是对与
*ngIf
的问题,可用[hidden]
替代