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';
({selector: 'button[counting]'})
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({selector: '[ngModel]'})
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]替代