Angular 入门笔记
版本
Angular: 11.2.7
基础概念
模块(Module)
模块顾名思义,承担容器的作用,通过 @NgModule 装饰。一个模块的基本组成如下。
1 2 3 4 5 6 7 8@NgModule({ imports: [ BrowserModule ], // imports: 导入其他所需模块 providers: [ Logger ], // providers: 导入服务(Service) declarations: [ AppComponent ], // declarations:声明组件(Component)、指令(Directive)、管道(Pipe) exports: [ AppComponent ], // exports:导出能在其他模块的组件模板中使用的可声明对象 bootstrap: [ AppComponent ] // bootstrap:主视图(只有根模块可以设置) }) export class AppModule { }
一个 Angular 应用一定会有 根模块(通常命名为 AppModule),除此之外还可以引入其他模块,如常用模块 BrowserModule(使应用可在浏览器中运行),CommonModule(使 ngFor, ngIf 可被识别)等。
指令(Directive)
可以修改 DOM、组件数据模型中某些属性的类,由 @Directive 装饰。指令可分为三类:
- 组件(由
@Component()装饰) - 属性型指令(如
[...],(...)) - 结构型指令(如
*ngIf,*ngFor)
通过指令,可以控制 DOM,扩展模板语法等。
属性型指令
属性型指令看起来像是 HTML 属性,会监听与修改其他 HTML 元素和组件的行为,常见的内置属性型指令有 ngClass, ngStyle, ngModel 等。
创建新的属性型指令
通过 @Directive({ selector: [...] }) 修饰类,其中方括号 [] 是必须的,表示这是一个属性型指令选择器。在类中:
- 引用 DOM 元素:通过
constructor(el: ElementRef); - 增加模板数据绑定项:使用
@Input(); - 使自己可被数据绑定:使用
@Input(alias),alias 与 selector 方括号中的内容相同; - 处理事件:使用
@HostListener(eventName);
最后,该类需要在 module 的 declarations 中被声明。
官方给出了一个 hover 高亮 的例子。
结构型指令
结构型指令负责 HTML 布局,维护 DOM 结构,常用的结构型指令有 ngIf, ngFor, ngSwitch 等。
星号(*)前缀
星号前缀用于简化结构型指令中绑定的 微语法,是语法糖,如
1<div *ngIf="hero" class="name">{{hero.name}}</div>
被简化为
1 2 3<ng-template [ngIf]="hero"> <div class="name">{{hero.name}}</div> </ng-template>
微语法
Angular 微语法形式如:
1*<prefix>="(<let> | <expression>) (';' | ',')? (<let> | <as> | <keyExp>)*"
其中,
1 2 3<keyExp> => <key> ":"? <expression> ("as" <local>)? ";"? <let> => "let" <local> "=" <export> ";"? <as> = <export> "as" <local> ";"?
<prefix>,<key>:HTML 属性键<local>:模板中使用的局部变量名<export>:指令使用该名称作为导出名称<expression>:Angular 标准表达式
创建新的结构型指令
通过 @Directive({ selector: [...] }) 修饰类,其中方括号 [] 是必须的,表示这是一个属性型指令选择器(上述与“创建新的属性型指令”相同)。在类中:
- 构造函数:
constructor(private templateRef: TemplateRef<any>, private viewContainer: ViewContainerRef) {}(TemplateRef可以访问<ng-template>中的内容,ViewContainerRef是当前节点视图容器的引用); - 监听:使用
@Input()装饰 setter - 显示子组件:使用
this.viewContainer.createEmbeddedView(this.templateRef) - 销毁子组件:使用
this.viewContainer.clear()
模板(Template)
- 插值:
{{ ... }}; - 绑定:
- 数据源到视图(属性):
[...]="...",bind-...="..." - 视图到数据源(事件):
(...)="...",on-...="..." - 双向:
[(...)]="...",bindon-...="..."
- 数据源到视图(属性):
模板引用变量
模板引用变量形如 #...,可以引用模板中的 DOM 元素、指令、元素、TemplateRef、Web 组件,作用域被 <ng-template> 分隔。
管道
管道为 pipe,在模板中使用,形式如 <exp> '|' 'pipe' (:<exp-as-arg>)*,模板中的值会经过管道转换后再输出。常用管道有 AsyncPipe, DatePipe, SlicePipe 等。
管道分为 纯的(pure) 与 非纯:
- 纯管道:当值发生变化时(浅比较)才会重新执行
- 非纯管道:当按键或鼠标移动时会重新执行
创建一个管道
以 @Pipe({ name: ..., pure: ... }) 修饰类,并实现 PipeTransform 接口,其中:
name:模板中管道的名称;pure:指示管道是否是纯的,默认为true;
类中需要实现 transform 方法,参数即为管道的传参,返回运算结果。
组件(Component)
组件负责 暴露数据 与 处理交互逻辑,由 @Component(其继承 @Directive)装饰。组件与模板(Template)共同组成视图(View)。
数据传输
@Input():父组件传给子组件数据
- 子组件类:通过
@Input()修饰成员,成员可以是变量、getter/setter(此方法可通过ngOnChanges生命周期检测值变动); - 父组件模板:使用 数据绑定 传输数据;
@Output():父组件绑定子组件事件
- 子组件类:通过
@Output()修饰EventEmitter类示例,需要时通过该实例上的emit方法发出事件; - 父组件模板:使用 事件绑定 绑定事件(如
(childCompEvEmitterInstanceName)=handleEvent($event),其中$event是固定的);
#localVar:父组件模板访问子组件数据与方法
- 父组件模板:在子组件标签上使用
#xxx新建一个本地变量代表子组件,在模板中便使用形如xxx.f1(),xxx.x1访问子组件的数据与方法;
@ViewChild():父组件访问子组件数据
- 父组件类:通过
@ViewChild(ChildComponentClass)修饰子组件类,便可在类中访问子组件的数据(被注入的子组件只有在父组件视图显示后才能被访问,对应生命周期函数为ngAfterViewInit);
服务:双向数据流
可通过父子组件共享一个服务来做到双向数据。
样式
Angular 会自动做 CSS 模块化,不需要担心类名污染的问题,同时支持 .scss, .less 等。
特殊的选择器
:host:用于选择宿主元素;:host-context(className):用于选择视图外的条件,会沿根查找className直到根;
配置
@Component({ styleUrls }):配置 CSS 文件;@Component({ style }):配置 CSS,只支持纯 CSS;@Component({ template: '<style>...</style>' }):内联 CSS;@Component({ template: '<link href="...">' }):配置 CSS 文件,只支持相对路径;- CSS
@imports
动态组件
以动态广告为例:
- 指令:新建指令
@Directive({ selector: '[adHost]' }) class AdHostDirective { constructor(public viewContainerRef: ViewContainerRef) {} }; - 模板:插入
<ng-template adHost>; - 类成员:
@ViewChild(AdHostDirective, { static: true }) adHost: BannerDirective;,这使得类可以控制 template 的内容显示; - 构造函数:
constructor(private componentFactoryResolver: ComponentFactoryResolver) { },其中ComponentFactoryResolver用于将组件类转为解析为工厂方法; - 动态组件实例化:导入目标组件类,通过
componentFactoryResolver.resolveComponentFactory(component)解析为工厂方法componentFactory,adHost.viewContainerRef.clear()清空视图内容,adHost.viewContainerRef.createComponent<...>(componentFactory)实例化组件并插入到视图中;
服务(Service)
封装了非 UI 逻辑的类,由 @Injectable 装饰,用于加强逻辑复用性。
其他
- 伪指令
ngNonBindable可以禁止子元素的插值与任何类型的绑定;
常用特性
表单
响应式表单与校验
ReactiveFormsModule, FormsModule 需要导入用于处理响应式表单逻辑。编写表单通常使用以下类:
FormControl类用于创建表单控件,绑定在表单控件上(如input);FormGroup类用于创建表单控件分组,通常绑定在form上;(如果使用FormGroup,则其包含的表单控件不需要在模板上绑定);FormBuilder服务是 util 类,用于快速生成FormGroup,FormControl,FormArray;
对于表单校验,有如下选择:
- 内置校验:通过
Validators对象,如Validators.require,Validators.maxlength(...)等; - 自定义同步校验:通过实现
ValidatorFn工厂(见 Demo 中fbcv.component.ts及对应的模板) - 自定义异步校验:通过实现
AsyncValidator,然后将validate成员函数设置在表单控件初始化处
另外,FormGroup 的 options 中可设置校验器以实现 交叉校验。
动态响应式表单
动态表单实际上是由响应式表单灵活实现。
路由
一般情况下,路由机制需要考虑的问题及 Angular 的支持如下
- 路由注册:通过
@angular/router中的RouterModule,编写路由列表时通过属性component指定组件(见 Demo 中的 app-routing.module.ts); - 路由路径:一般路径如
path/to;带参数路径如path/:param;通配符**,一般用于 404 页面(见 xxx-routing.module.ts); - 获取路由信息与更改路由:通过
@angular/router中的ActivatedRoute,Router服务 DashBoard/Datalist); - 懒加载(Lazy Loading,文档中表述为 “惰性加载”):将原先的 component 封装为 module,原属性
component换为loadChildren并改为import()动态导入模块(见 DashBoard); - 路由模式(History API 或 Hash):通过在
RouterModule.forRoot的LocationStrategy指定为PathLocationStrategy(History API)或HashLocationStrategy; - 路由守卫:通过实现
CanActivate,CanActivateChild等接口的服务后,在路由列表的canLoad,canActivate等属性上注册; - 重定向:通过配置路由列表中的
redirectTo属性(见 app-routing.module.ts); - 嵌套路由:通过配置
children属性(见 DashBoard)
网络请求
一般情况下,网络请求需要考虑的问题及 Angular 的支持如下
- 基础 AJAX 请求与内容解析:使用
HttpClientModule模块,通过http[method](url, options)实现(其中http为HttpClient服务),其中 options 为如下 因此可以设置请求头,相应数据类型等。另外,这种方式返回 observable 对象,后续可做错误处理等。 - JSONP 请求:使用
HttpClientJsonpModule模块,通过http.jsonp(...)实现; - 错误处理:使用
rxjs/operators中的catchError; - 请求重试:使用
rxjs/operators中的retry; - 拦截器:使用实现
HttpInterceptor接口的服务类实现; - 安全:使用
HttpClientXsrfModule模块,配合后端完成 CSRF 攻击防护;