架构
架构概览
模块简介
组件简介
服务与DI简介
单例模式
在 Angular 中有三种方式来生成单例服务:
- 声明该服务应该在应用的根上提供,即放在 AppModule 中注入。
- 把该服务包含在 AppModule 或某个只会被 AppModule 导入的模块中。
- 在 @Injectable 装饰器的元数据中指明
provideIn: root
。
后续步骤
组件与模板
显示数据
模板语法
- 插值表达式 ``
- 模板表达式
[property]='expression'
:表达式上下文(本组件),表达式指南(执行迅速、没有副作用、简单、幂等) - 模板语句
(event)='statement'
:语句上下文(本组件),语句指南(简单,通常是函数调用或属性赋值) - 绑定语法:
- 数据源 -> 视图:
, [target]='expression', bind-target='expression'
- 视图 -> 数据源:
(target)='statement', on-target='statement'
- 数据源 <-> 视图:
[(target)]='expression', bindon-target='expression'
- 数据源 -> 视图:
- HTML attribute vs DOM property: HTML attribute init DOM property; HTML attribute can’t change, but DOM property can
- 数据绑定:
- 单向输入:不能通过属性绑定来调用方法(有副作用),因为可能会导致UI依赖的一些View Model改变,从而无法形成单向数据流
- 一次性字符串初始化:
routerLink='/posts'
vs[routerLink]='"/posts"'
- attribute 绑定:
[attr.colspan]
- CSS 样式绑定:
[style.background]
- CSS 类绑定:
[class.myclass]
[ngClass]='{className1: true, className2: false}'
[ngStyle]='{color: red, background: #FFF}'
(click)='onSave($event)'
[(ngModel)]='item'
- 模板引用变量:
#var
或ref-var
,一般指向引用的 DOM 元素,也可以通过赋值引用 Angular 组件或指令,比如<form #myForm='ngForm'></form>
- 模板输入变量:
<ng-template let-item='obj'></ng-template>
- 输入和输出属性:
@Input(), @Output(), @Input('alias')
- 安全导航操作符:
obj?.prop
- 非空断言操作符:
obj!.prop
, 非空断言操作符不会防止出现 null 或 undefined。它只是告诉 TypeScript 的类型检查器对特定的属性表达式,不做 “严格空值检测”。不使用这个操作符时类型检查器可能会报错。 - 类型转换函数:
$any(obj).prop
生命周期钩子
- class 内的默认赋值语句先执行
- constructor:在所有生命周期钩子前执行
- 通常在构造函数中进行简单的赋值,不从远程请求数据
- OnChanges:每次(重新)设置输入属性后
- OnInit:第一次设置组件/指令的输入属性后调用,在第一次 onChanges 之后,只调用一次
- 放置复杂初始化逻辑的好地方
- 组件获取远程数据的好地方
- DoChecked:每个变化检测周期中调用
- AfterContentInit:内容投影进组件之后调用,只调用一次
- AfterContentChecked:每当被投影组件变化检测之后调用
- 无需担心单向数据流,为什么?
- AfterViewInit:初始化完组件视图及其子视图后调用
- 这里要注意必须是
单向数据流
,在要改变 View Model 的值时,需要把相关处理的语句放在 process.nextTick() 或者 setTimeout(fn, 0) 内。
- 这里要注意必须是
- AfterViewChecked:每当组件完成变化检测后调用
- 和 AfterViewInit 相同,也要注意单向数据流
- OnDestroy:销毁组件、指令时调用
- 放置一些清理逻辑,释放 gc 不自动回收的资源
- 取消 observable 和 DOM event 的订阅
- 停止定时器
- 注销该指令曾注册到全局服务或应用级服务中的各种回调函数
组件交互
- @Input()
- @Output()
- setter
- ngOnChanges()
- 在父组件中使用模板引用变量指向子组件:
<child-component #child></child-component>
- 在父组件中使用
@ViewChild(ChildComponent)
- 通过服务通讯: 服务的依赖注入会形成一棵树,因此在父组件中注入一个 Service,子组件不用重新注入,这时两者获取的是同一个服务实例。如果子组件也重新注入了,则不是同一个实例。
组件样式
- 范围化的样式:不影响子组件和内容投影的组件
- :host, :host(.active)
- :host-context, :host-context(.className)
- /deep/, »>, ::ng-deep (deprecated)
- styles, styleUrls
- template 中的 HTML 也可以引用外部文件
<link rel='stylesheet' href='/url'>
- @imports
- 全局样式文件 styles.css
- 视图的封装模式:
- Native: 使用浏览器原生的 Shadow DOM 的实现样式封装
- Emulated: (默认),通过预处理CSS代码来模拟 Shadow DOM 的行为,以达到把 CSS 样式局限在组件视图中的目的
- None: 不适用视图封装,CSS会添加到全局样式中,没有作用域隔离和保护
Angular 自定义元素
动态组件
- 对于动态加载的组件,需要添加到 @NgModule 的装饰器的 entryComponents 元数据数组中
- Angular 会对 entryComponents 数组中的组件生成一个 ComponentFactory 类
举个例子:
import { Component, Input, OnInit, ViewChild, ComponentFactoryResolver, OnDestroy } from '@angular/core';
import { AdDirective } from './ad.directive';
import { AdItem } from './ad-item';
import { AdComponent } from './ad.component';
@Component({
selector: 'app-ad-banner',
template: `
<div class="ad-banner">
<h3>Advertisements</h3>
<ng-template ad-host></ng-template>
</div>
`
})
export class AdBannerComponent implements OnInit, OnDestroy {
@Input() ads: AdItem[];
currentAdIndex = -1;
@ViewChild(AdDirective) adHost: AdDirective;
interval: any;
constructor(private componentFactoryResolver: ComponentFactoryResolver) { }
ngOnInit() {
this.loadComponent();
this.getAds();
}
ngOnDestroy() {
clearInterval(this.interval);
}
loadComponent() {
this.currentAdIndex = (this.currentAdIndex + 1) % this.ads.length;
let adItem = this.ads[this.currentAdIndex];
// 参数是组件类名
let componentFactory = this.componentFactoryResolver.resolveComponentFactory(adItem.component);
let viewContainerRef = this.adHost.viewContainerRef;
viewContainerRef.clear();
let componentRef = viewContainerRef.createComponent(componentFactory);
(<AdComponent>componentRef.instance).data = adItem.data;
}
getAds() {
this.interval = setInterval(() => {
this.loadComponent();
}, 3000);
}
}
属性型指令
属性型指令用于改变组件的外观和行为。
- 使用 @HostListener(‘eventName’) 绑定事件
- 使用 @Input(‘alias’) 绑定输入属性
- 可以在构造函数中注入 ElementRef 改变 DOM 的样式
结构型指令
结构型指令用于改变组件的 DOM 结构
NgIf
语法糖翻译前后的代码:
<div *ngIf="hero" class="name"></div>
<ng-template [ngIf]="hero">
<div class="name"></div>
</ng-template>
NgFor
ngFor里的 traceBy
:对于重新从服务器获取数据时,可能只有部分数据时更新的,但是不加 TraceByFunction(index, item) 时,会将所有DOM重新渲染,而使用此函数时,会根据对象的属性跟踪对象,只渲染新增或变化的部分 DOM。 参考这里
语法糖翻译前后的代码:
<div *ngFor="let hero of heroes; let i=index; let odd=odd; trackBy: trackById" [class.odd]="odd">
()
</div>
<ng-template ngFor let-hero [ngForOf]="heroes" let-i="index" let-odd="odd" [ngForTrackBy]="trackById">
<div [class.odd]="odd">() </div>
</ng-template>
NgSwitch
语法糖翻译前后的代码:
<div [ngSwitch]="hero?.emotion">
<app-happy-hero *ngSwitchCase="'happy'" [hero]="hero"></app-happy-hero>
<app-sad-hero *ngSwitchCase="'sad'" [hero]="hero"></app-sad-hero>
<app-confused-hero *ngSwitchCase="'app-confused'" [hero]="hero"></app-confused-hero>
<app-unknown-hero *ngSwitchDefault [hero]="hero"></app-unknown-hero>
</div>
<div [ngSwitch]="hero?.emotion">
<ng-template [ngSwitchCase]="'happy'">
<app-happy-hero [hero]="hero"></app-happy-hero>
</ng-template>
<ng-template [ngSwitchCase]="'sad'">
<app-sad-hero [hero]="hero"></app-sad-hero>
</ng-template>
<ng-template [ngSwitchCase]="'confused'">
<app-confused-hero [hero]="hero"></app-confused-hero>
</ng-template >
<ng-template ngSwitchDefault>
<app-unknown-hero [hero]="hero"></app-unknown-hero>
</ng-template>
</div>
自定义结构型指令
接收的参数:constructor(private templateRef: TemplateRef<any>, private viewContainer: ViewContainerRef) { }
,在条件为真时会将内嵌的视图插入(TemplateRef)到视图容器(ViewContainerRef)中。
管道
动画
风格指南
单一规则
每个文件只定义一样东西,把文件大小限制在400行代码之内: 易阅读;维护;防止团队合作冲突;可以用路由按需加载;避免了代码中共享变量的bug
命名
feature.type.ts
踩过的坑
- 使用jqWidget中的jqxMenu和jqxTree,在绑定属性时,视图没有随着数据模型更新。然后将数据模型深度拷贝后更新在赋值过去,就神奇的解决了。目前还是不清楚原因
- 在angular中使用jquery等第三方库:
npm install @types/jquery --save-dev npm install jquery --save import * as jQuery from 'jquery';
- ngInit在页面第一次加载时会进行初始化,但是第二次不会走ngInit的方法。