![c06645fb0364b73da821a9aae675d819.png](https://i-blog.csdnimg.cn/blog_migrate/ef0d658f249d3393e00f72bfc8bceb29.jpeg)
前言
今天是二月份的最后一天,是该好好总结下。2月12号正式开始工作,第一周有些不在状态,所以18号-28号真正投入工作和学习,最近工作节奏不是特别快(两周的polish week 修复bug),但是完成任务还是需要花一些时间的,所以2月份的学习任务时间上还是有一些紧张,仍然很庆幸做了两件事情,第一:对http://angular.cn中一些不太理解的特性重新阅读,包括NgModule、Angular DI、Angular Form、Angular Lifecycle,第二:读《ng-book.2.Angular.4》 ,重点阅读了Write Your First Angular Web App、TypeScript、Forms in Angular 、Dependency Injection、Advanced Component等章节,书中对Angular的讲解比较基础,所讲内容85%基本在我平常的开发中都有用到,所以即使书是英文版的也没关系,另外我看的书讲解时基于Angular4,这本书是电子书并且会根据Angular版本的更新而更新,基于Angular7的版本已经更新(电子书地址)并且需要收费,所以想先把这本免费的看完再去读最新版本,顺便学习下英文。
问题
个人读文档或者技术书的时候,有些东西用语言表达出来的时候很难理解,没有示例或者示例不够清楚,经常需要反复的去理解,所以我觉得总结一下还是很有必要的。
想法
第一次读ng-book.2.Angular.4,很多单词、短语需要查,发现好多通用的程序术语,下周找两天整理下,做个记录增加印象,也能供他人参考。
--整理地址:https://zhuanlan.zhihu.com/p/58414025
笔记
Angular DI
![04d3379e8ae57ead0028f71f2ac1d84f.png](https://i-blog.csdnimg.cn/blog_migrate/b009b57d3baeb2cb9235e1b5e308e573.jpeg)
图片来自《ng-book2.angular.4》
配置提供商
根据方式不同获取的注入实例也是不同,根据业务确定配置方式 多级提供商(配置Providers) NgModule中配置提供商 获取的实例其实也是单例模式(惰性加载的模块情况会有不同,这个另外讨论) Component配置提供商 每次注入都是新的实例
全局提供商
@Injectable({
providedIn: 'root'
})
单例模式
使用Injectable方式或者在AppModule中配置提供商
配置Providers的三种方式
使用Class
最常用的方式,代码简写如下:
providers: [ UserService ]
正常配置:
providers: [
{ provide: UserService, useClass: UserService }
]
使用Value值
providers: [
{ provide: 'API_URL', useValue: 'http://my.api.com/v1' }
]
//使用
import { Inject } from '@angular/core';
export class AnalyticsDemoComponent {
constructor(@Inject('API_URL') apiUrl: string) {
//do something
}
使用工厂函数
有些特殊情况,需要根据传入的参数去实例化实例
providers: [
{ provide: AnalyticsService, useFactory: () => ... }
]
//例如
useFactory() {
const loggingImplementation: AnalyticsImplementation = {
recordEvent: (metric: Metric):void => {
console.log('The metric is:', metric);
}
};
// create our new `AnalyticsService` with the implementation
return new AnalyticsService(loggingImplementation); }
}
一个问题
Angular的依赖注入,暂时只支持构造器注入(为什么这样做,具体原因不清楚),这个在使用的时候确实不太方面,比如我有基类:Store 有如下构造器
![40aa17ebf3cc4c0df10ae812c79b3a15.png](https://i-blog.csdnimg.cn/blog_migrate/726e817a61129c410d5577298e7b445a.jpeg)
这个时候我的子类继承Store后,如果子类同样需要注入一些Http或者其他自定义服务就有一个问题,这就要求我的子类同样需要注入这个RootContainer然后调用super,代码如下:
![633b0fabc32073aa567fbb0e7bc66923.png](https://i-blog.csdnimg.cn/blog_migrate/aaae617153c941eb0d35516fd9afeeb7.jpeg)
一个不太完美的解决方案是使用Injector服务,在基类中注入Injector,如下:
![4d5f67b0967cadd601ab2e5217f0b32d.png](https://i-blog.csdnimg.cn/blog_migrate/50999686edc6d586b2535af386cd4278.jpeg)
子类继承时代码(子类在注册基类的注入对象时,必须加@Inject(Injector),郁闷!!!):
![ce189abb8dbaa4b8aa703c3fbb772acb.png](https://i-blog.csdnimg.cn/blog_migrate/992db0ddb681d102e922fe7f86902d71.jpeg)
其它想法:
1.使用ReflectiveInjector对象,自己配置需要的Providers,生成一个全局的injector,直接在基类Store中使用这个injector,这种方式基本脱离了Angular的依赖范围,示例如下:
![0ce4431e88c4fb06716c8f6043d7f28f.png](https://i-blog.csdnimg.cn/blog_migrate/78ee4357d445f4d682875a6f5a84ec7b.jpeg)
2.Angular应该提供的有其他解决放啊,还不太了解,后面会阅读下Angular的相关资料和代码查找解决方案。
Angular Lifecycle ( 生命周期钩子 )
声明周期列表
- OnInit
- OnDestroy
- DoCheck
- OnChanges
- AfterContentInit
- AfterContentChecked
- AfterViewInit
- AfterViewChecked
ngOnInit、ngOnDestroy
最常用的做初始化逻辑/注销全局资源的钩子
OnChanges
当组件的一个或者多个属性发生变化时调用的钩子,ngOnChanges接收一个SimpleChange对象,SimpleChange对象是一个Map对象,key是值发生变化的属性名称,value形如:
{ currentValue: '', previousValue: '' }
一个效果:
![de0765c66d7515427ab324d8a3328748.gif](https://i-blog.csdnimg.cn/blog_migrate/48d01a1189c9003e0f772ed746cd41c1.gif)
总结:ngOnChanges只对输入的值变化调用这个钩子(图中的power),对引用类型实例中的某一个字段值得变化不调用这个钩子,因为Angular认为引用类型的指针地址没有发生变化。
DoCheck
可以弥补OnChanges的不足,用来检测那些被Angular忽略的更改,主要这个钩子会被频繁调用(每次变更检测周期之后,发生了变化的每个地方都会调用它,所以这里面的逻辑要尽量简洁,避免造成性能问题),这块内容http://angular.cn中讲的非常模糊,下面的内容主要出自ng-book.2.angular4 后面如果发现有版本不支持的地方我会再更新. 为了检测数据的变更Angular提供了differs,differs用来确定给定的属性是否发生了变化,存在内置的两个differs:KeyValueDiffers (检测属性是否增加、移除、属性值发生变化)和IterableDiffers(检测列表是否增加或者移除对象)
KeyValueDiffers
使用代码
//构造函数获取differs实例
constructor(differs: KeyValueDiffers) {
this.differ = differs.find([]).create(null);
}
//使用语法创建differ实例
ngDoCheck(): void {
const changes = this.differ.diff(this.comment);
if (changes) {
changes.forEachAddedItem(r => this.logChange('added', r));
changes.forEachRemovedItem(r => this.logChange('removed', r));
changes.forEachChangedItem(r => this.logChange('changed', r));
}
}
// logChange
logChange(action, r) {
if (action === 'changed') {
console.log(r.key, action, 'from', r.previousValue, 'to', r.currentValue);
}
if (action === 'added') {
console.log(action, r.key, 'with', r.currentValue);
}
if (action === 'removed') {
console.log(action, r.key, '(was ' + r.previousValue + ')');
}
}
通过上面代码,可以检测值发生变化并打印到控制台
IterableDiffers
类似于KeyValueDiffers
constructor(differs: IterableDiffers) {
this.differ = differs.find([]).create(null);
this.comments = [];
this.authors = ['Elliot', 'Helen', 'Jenny', 'Joe', 'Justen', 'Matt']; this.texts = ["Ours is a life of constant reruns. We're always circling back to where we'd we started, then starting all over again. Even if we don't run extra laps that day, we surely will come back for more of the same another day soon.",'Really cool!','Thanks!'];
this.addComment();
}
ngDoCheck(): void {
const changes = this.differ.diff(this.comments);
if (changes) {
changes.forEachAddedItem(r => console.log('Added', r.item));
changes.forEachRemovedItem(r => console.log('Removed', r.item));
}
}
ngAfterContentInit 、 ngAfterContentChecked
外部模板在组件中渲染相关的钩子,针对的是ContentChildren和ContentChild,ngAfterContentInit在ngOnInit之后content内容初始化完成后调用并且只调用一次,ngAfterContentChecked类似,它被调用在组件Check完成之后,一般顺序是DoCheck、ngAfterContentChecked、ngAfterViewChecked.
ngAfterViewInit 、 ngAfterViewChecked
在content之后执行,view完全被初始化完成后调用,针对的是ViewChild、ViewChildren
,ngAfterViewChecked 在 ngAfterContentChecked之后调用
组件高级用法
Styling
组件样式主要提供了三种模式(只需要了解,看到这种样式理解是怎么回事就行):
Emulated
组件的默认选项,无论使用styleUrls引入外部样式文件,还是使用styles直接写样式,都会被组件的上下文封装,写的样式只对当前组件有效,对组件外的元素无效,封装成如下结果:
![0485da4d62d7d32fb178c375644bff44.png](https://i-blog.csdnimg.cn/blog_migrate/06bfef02e482d095f4e0db55011a72e9.jpeg)
Native
Shadow DOM 封装,如下图所示,实现和默认类似的效果,起到隔离作用,不会对组件外的样式产生影响
![ee878164da58c2a7c6d5b31483c6ad62.png](https://i-blog.csdnimg.cn/blog_migrate/c7e1b34338b192e4889a2661f608183d.jpeg)
None
不作处理,直接应用样式,会与组件外的样式叠加
Host Elements、 ElementRef 等对象使用
组件或者指令开发中,如果想操作Host Elements(组件最外层或者指令的宿主元素),Angular提供了很方便的方式 简单的样式控制可以通过如下代码来做
@HostBinding('class.thy-cascader-menu-item') item = true;
//item 可以来自输入变量
@HostBinding('class.thy-cascader-menu-item-active')
@Input()
active = false;
//可以通过get访问器做一些简单的逻辑,感觉这块尽量不要太过复杂
@HostBinding('class.thy-cascader-menu-item-expand')
get expand() {
return this.option && !this.option.isLeaf;
}
同样是修改样式,还可以通过另外一种方式实现,这种适合稍微复杂一些的逻辑控制
constructor(
private elementRef: ElementRef,//获取当前组件的元素实例
private updateHostClassService: UpdateHostClassService
) {
updateHostClassService.initializeElement(elementRef.nativeElement);
}
上面代码主要是注入了一个UpdateHostClassService服务,这个服务的提供商是在当前组件的装饰器中注入,应为组件需要传入当前组件的DOM,代码中通过elementRef.elementRef获取。 UpdateHostClassService这个服务,这个服务是我们公司内部封装的,记得Angular CDK也有类似功能的服务,主要是实现根据传入的DOM动态更新样式类,实现如下:
import { Injectable, Renderer2 } from '@angular/core';
import { Dictionary } from '../typings';
@Injectable()
export class UpdateHostClassService {
private _classNames: string[] = [];
private _hostElement: HTMLElement;
constructor(private renderer: Renderer2) {
}
initializeElement(nativeElement: HTMLElement) {
this._hostElement = nativeElement;
}
updateClass(classNames: string[]) {
if (this._classNames) {
this._classNames.forEach((className) => {
if (classNames.indexOf(className) < 0) {
this.removeClass(className);
}
});
}
const newClasses: string[] = [];
classNames.forEach((className) => {
if (className) {
newClasses.push(className);
if (this._classNames.indexOf(className) < 0) {
this.addClass(className);
}
}
});
this._classNames = newClasses;
}
updateClassByMap(classMap: Dictionary<boolean>) {
const newClasses = [];
for (const key in classMap) {
if (classMap.hasOwnProperty(key) && classMap[key]) {
newClasses.push(key);
}
}
this.updateClass(newClasses);
}
addClass(className: string) {
this.renderer.addClass(this._hostElement, className);
}
removeClass(className: string) {
this.renderer.removeClass(this._hostElement, className);
}
}
绑定事件可以通过如下代码:
@HostListener('click') displayMessage(): void {
alert(this.message);
}
exportAs
exportAs作为Directive的一个属性,可以用来导出一个名称,在模板中用于关联变量连接到指令,使用形如 #var="exportName"的语法,exportAs使用如下
@Directive({
selector: '[popup]',
exportAs: 'popup',
})
export class PopupDirective {
@Input() message: String;
constructor(_elementRef: ElementRef) {
console.log(_elementRef);
}
@HostListener('click') displayMessage(): void { alert(this.message);
} }
//模板中使用
template: `
<div class="ui message" popup #popup1="popup"
message="Clicked the message">
<div class="header">
Learning Directives
</div>
<p>
This should use our Popup diretive
</p>
</div>
<i class="alarm icon" popup #popup2="popup"
message="Clicked the alarm icon">
</i>`
ViewChild、ContentChild、ContentChildren、 ViewChildren
主要用于取当前组件视图元素或组件外部传递过来的投影元素,View代表去当前视图中组件或者元素、Content组件外部投射到当前组件的元素。 模板step.component.html:
<ng-template><ng-content></ng-content></ng-template>
则可以ts文件中取ng-template元素实例
@ViewChild(TemplateRef) content: TemplateRef<any>; //ng-template标签对应的类型为TemplateRef,此种写法适用于当前模板只有一个ng-template的情况
代码取外部传入的模板,模板组件的类型为ThyStepComponent
@ContentChildren(ThyStepComponent) steps: QueryList<ThyStepComponent>;
模板定义如下:
<thy-stepper>
<thy-step label="第一步">
<div class="demo-stepper-body">
<button thyButton="primary" thyStepperNext>下一步</button>
</div>
</thy-step>
<thy-step label="第二步">
<div class="demo-stepper-body">
<button thyButton="primary" thyStepperNext>下一步</button>
<a thyButton="link-secondary" thyStepperPrevious>上一步</a>
</div>
</thy-step>
<thy-step label="第三步">
<div class="demo-stepper-body">
<a thyButton="link-secondary" thyStepperPrevious>上一步</a>
</div>
</thy-step>
</thy-stepper>
thy-step标签对应的组件类型就是ThyStepComponent
高级模板
实现自己的ngBookIf、ngBookFor 模板语法糖,正常我们使用的代码号的结构指令其实都是语法糖,如ngFor:
<app-comment *ngFor="let comment of comments"
[comment]="comment"
(onRemove)="removeComment($event)">
</app-comment>
它会被转化为:
<app-comment template="ngFor let comment of comments; #i=index"
[comment]="comment"
(onRemove)="removeComment($event)">
</app-comment>
然后转化为:
<template ngFor [ngForOf]="comments" let-comment="$implicit" let-index="i">
<app-comment
[comment]="comment"
(onRemove)="removeComment($event)">
</app-comment>
</template>
ngBookIf
自定义结构指令,实现ngIf功能
@Directive({
selector: '[ngBookIf]'
})
ngBookIf作为选择器,和使用ngIf的结果一样,当使用ngBookIf='condition',代码将被转化为:
<template ngBookIf [ngBookIf]="condition">
ViewContainerRef 使用 - 创建一个新的View使用嵌入的模板 - 清除view container中的内容 构造器注入ViewContainer
constructor(private viewContainer: ViewContainerRef,
private template: TemplateRef<any>) {}
使用setter访问器接受condition输入
@Input() set ngBookIf(condition) {
if (condition) {
this.viewContainer.createEmbeddedView(this.template);
} else {
this.viewContainer.clear();
}
}
使用ngBookIf
<button class="ui primary button" (click)="toggle()"> Toggle
</button>
<div *ngBookIf="display"> The message is displayed
</div>
变化检测、Zone、OnPush
这块http://angular.cn和《ng-book.2》中讲的都比较少,但是有提到所以我在列大纲的时候协商了,回头查详细资料在做补充。
结束语
虽然都是一些简单的用法,但是只有做个Angular开发才能看的懂,因为好多点都么有前后铺垫,这其实是自己认为一些重要知识点的摘抄和概要讲解,希望不要给看官造成困扰。针对这些比较重要的点,如DI、变化检测、Form、NgZone,我后续希望可以整理出涉及基础实现、设计思路方面的文章,共勉!