新东家项目目前基于 Angular,拿到项目的时候还是有点懵逼的,毕竟和当初的 angularJS 可完全不一样了,没办法呀,只能继续学习 Angular 咯!
历史
16 年 9 月正式发布 Angular2,截止到今天查看 release,发现在 3 天前发布了一个 v8.0.0 的 beta 版了,版本迭代还是飞快哇。看看 Angular2 后主要有哪些变更呢
- 组件式开发,抛弃 controller + $scope 设计
- 变化检测效率更高
- 贴合未来标准
核心概念:组件、元数据、模板、数据绑定、服务、指令、依赖注入、模块
提供全生命周期支持
什么是装饰器?在 Angular 中,提供众多装饰器 + 传递元数据的方式,来赋予一个类更丰富的信息
- 附加到类、方法、访问符属性或参数上,本质是函数,并返回另一个函数,参数指向被修饰的类。
- 装饰器 + 类:装饰器赋予一个类更丰富的信息(元数据)
数据绑定
数据绑定几种方式
- 插值
{{target}}
- 属性绑定
[target]
- 事件绑定
(target)
- 双向绑定
[(target)]
angular 提供了 ngModel 来实现文本框双向绑定,该指令依赖 angular/Forms
的 FormsModule,同时我们可以通过监听 ngModelChange 事件来定制。
在双向绑定中,数据属性值通过属性绑定从组件流到输入框。用户的修改通过事件绑定流回组件,把属性值设置为最新的值。
angular 子组件通过事件修改父组件传递过来的属性是在一个 event loop 中完成的
安全导航操作符 ( ?. )
严格空值检查与非空断言操作符(!)
组件
父组件要能使用子组件定义的元素标签,需要借助模块实现
父子组件通信通过如下方式,装饰器需要在 angular/core
导入
- 自上而下通过属性绑定 @Input 装饰器
- 自下而上通过事件绑定 @OutPut 装饰器 + EventEmitter
模版引用变量:用来引用模板中的某个 DOM 元素,模版中通过 #paramName 声明模版引用变量,此时 paramName 可以传回组件实例中。
双向绑定:[(target)]
,双向绑定语法实际上是属性绑定和事件绑定的语法糖。
操作属性:[attr.attrName]
样式相关
- [class]
- [ngClass]
- [class.special]
- [ngStyle]
- [style.color.unit]
$event 的值
- 原生事件:$event 就是 DOM 事件对象
- 组件自定义事件:指的就是 payload
获取子组件实例
- @ViewChild:参数可以 className (获取第一个)或模版引用变量(获取指定)
- @ViewChildren:参数是 className,获取一个列表
- @ContentChild
组件交互
- 通过 setter 截听输入属性值的变化
- 通过 ngOnChanges() 来截听输入属性值的变化:当需要监视多个、交互式输入属性的时候,本方法比用属性的 setter 更合适。
- 父组件监听子组件的事件
- 父组件与子组件通过本地变量互动:通过模板引用变量,局限就是只能在父模板中使用
- 通过 @ViewChild() 将子组件注入到父组件中
- 父组件和子组件通过服务来通讯
CSS 部分
- :host:把宿主元素作为目标的唯一方式
- :host-context:当前组件宿主元素的祖先节点中查找 CSS 类, 直到文档的根节点为止。
ng-template 与 ng-container
- ng-template:默认就是注释节点做占位符,永远不会直接显示出来,结构型指令会让其正常工作
- ng-container:常用来结构上分组,不会产生任何 DOM 元素
加载动态组件的步骤
- 动态组件需要在 ngModule entryComponents 声明
- componentFactoryResolver.resolveComponentFactory(component)
- viewContainerRef.createComponent(componentFactory)
- 修改 props 数据:componentRef.instance.propName = value
- 如果我们手动注册 @Output,我们需要手动 unsubscribe,可以使用
ngx-take-until-destroy
包 - 组件销毁,手动调用 componentRef.destroy()
不完整的生命周期:ngOnChanges 不会触发,此时如果要监听 props 改动,需借助 setter/getter 方式。
生命周期
具体生命周期如下(按初始化顺序),组件只是特殊的指令,因此指令也是有生命周期的。
- OnChanges:父组件传递给子组件值,配合 SimpleChanges 使用,初始化和改变都会进入,初始化包含所有属性列表,改变仅有当前发生改变的属性列表
- OnInit:组件初始化调用,组件获取初始数据的好地方。
- DoCheck:调用频繁,应用级改变检测周期,检测那些 Angular 自身无法捕获的变更并采取行动。
- AfterContentInit:插槽系统
ng-content
使用 - AfterContentChecked:调用频繁,应用级改变检测周期
- AfterViewInit:这里才能访问到具体 DOM 以及组件实例
- AfterViewChecked:调用频繁,应用级改变检测周期
- OnDestroy:组件销毁,取消那些对可观察对象和 DOM 事件的订阅。停止定时器。注销该指令曾注册到全局服务或应用级服务中的各种回调函数。
构造函数中获取不到父组件传给子组件的值,构造函数中除了使用简单的值对局部变量进行初始化之外,什么都不应该做。
AfterContent 钩子和 AfterView 相似。关键的不同点是子组件的类型不同。
- AfterView 钩子所关心的是 ViewChildren,这些子组件的元素标签会出现在该组件的模板里面。
- AfterContent 钩子所关心的是 ContentChildren,这些子组件被 Angular 投影进该组件中。
生命周期
- ngOnChanges 先于 onInit 触发,因此首次并不能正确获取到 props 值
- constructor 中无法获取父组件传递给子组件的值
- @ViewChild @ViewChildren:用于获取组件实例,需要在 ngAfterViewInit 后才能正确获取到
- @ContentChild @ContentChildren,对应的插槽系统,在 ngAfterContentInit 才能正确获取
- ngAfterContentInit 先于 ngAfterViewInit 触发
指令与管道
组件本质上继承指令,只不过指令自身有模板,在模板中,指令通常作为属性出现在元素标签上,可能仅仅作为名字出现,也可能作为赋值目标或绑定目标出现。
指令常用 ElementRef、Renderer、HostListener(@HostListener 装饰器让你订阅某个属性型指令所在的宿主 DOM 元素的事件)
指令也可以通过 @Input 获取传值
@Directive 装饰器修饰,[]
表示执行使用在元素属性上,主要分为两类
- 属性指令(外观或行为),如
ngSwitch
- 结构指令(DOM 结构),均有
*
前缀,比如*ngIf/*ngFor
- 你可以在一个宿主元素上应用多个属性型指令,但只能应用一个结构型指令。
星号是一个用来简化更复杂语法的“语法糖”。 从内部实现来说,Angular 把 *ngIf 属性 翻译成一个 <ng-template>
元素 并用它来包裹宿主元素
当没有合适的宿主元素放置指令时,可用 <ng-container>
对元素进行分组,同时以免引入一个新的 HTML 层级。
NgFor 指令上下文中的 index 属性返回一个从零开始的索引,表示当前条目在迭代中的顺序。trackBy 提升性能。
自定义结构指令:导入符号 Input、TemplateRef 和 ViewContainerRef,你在任何结构型指令中都会需要它们。
管道类似与 Vue 的 filter,通过 @Pipe 装饰器,实现 PipeTransform 接口的 transform 函数。
有两类管道,纯和非纯的,默认情况下,管道都是纯的。通过把它的 pure 标志设置为 false,你可以制作一个非纯管道。
- 纯管道:只有在它检测到输入值发生了纯变更时才会执行纯管道。
- 非纯管道:每个组件的变更检测周期中执行非纯管道。 非纯管道可能会被调用很多次,和每个按键或每次鼠标移动一样频繁。
Angular 的 AsyncPipe 是一个有趣的非纯管道的例子。 AsyncPipe 接受一个 Promise 或 Observable 作为输入,并且自动订阅这个输入,最终返回它们给出的值。
服务
实现专一目的的逻辑单元,组件应该把诸如从服务器获取数据、验证用户输入或直接往控制台中写日志等工作委托给各种服务。通过把各种处理任务定义到可注入的服务类中,你可以让它被任何组件使用。
为什么需要服务?组件不应该直接获取或保存数据,它们应该聚焦于展示数据,而把数据访问的职责委托给某个服务。同时服务是在多个“互相不知道”的类之间共享信息的好办法。
对于要用到的任何服务,你必须至少注册一个提供商。服务可以在自己的元数据中把自己注册为提供商,这样可以让自己随处可用。或者,你也可以为特定的模块或组件注册提供商。
在 angularJS 中,service 是全局单例,但在 angular 中,其实是有差别的。angular 的注册方式有三种,
- @ngModule 的 providers 元数据
- @ngComponent 的 providers 元数据,把这个提供商的范围限定到该组件及其子组件。
- @Injectable 的 providerIn 元数据为 root 值,直接将服务自身注册在根模块,此时根模块无需再注入
providedIn 还可以设置为某个特定的 NgModule 包含的所有注入器中,一般来说,这和在 NgModule 本身的装饰器上配置注入器没有多少不同,主要的区别是如果 NgModule 没有用到该服务,那么它就是可以被摇树优化掉的。
注入器冒泡机制:当一个组件申请获得一个依赖时,Angular 先尝试用该组件自己的注入器来满足它。 如果该组件的注入器没有找到对应的提供商,它就把这个申请转给它父组件的注入器来处理。 如果那个注入器也无法满足这个申请,它就继续转给它在注入器树中的父注入器。 这个申请继续往上冒泡 —— 直到 Angular 找到一个能处理此申请的注入器或者超出了组件树中的祖先位置为止。如果你在不同的层级上为同一个 DI 令牌注册了提供商,那么 Angular 所碰到的第一个注入器就会用来提供该依赖。
在组件中引用服务时,可以在构造函数入参中直接声明服务类型,angular 会自动寻找合适的 service 注入,注意:不是在 prividers 声明时创建实例,而是依赖注入时。
当你在顶层提供该服务时,Angular 就会为 Service 创建一个单一的、共享的实例,并把它注入到任何想要它的类上。
特点
- 如果注入在组件中,组件自身以及所有子组件都能使用该服务。如果子组件需要修改服务行为,则在子组件中重新注入(多实例)
- 作用在模块的服务能在应用全局使用
模块
分为文件模块和应用模块
文件模块包括:核心模块、通用模块、表单模块、网络模块等
应用模块:将零散的指令,服务、组件分类归纳,彼此不影响,通过 @ngModule 声明
- declaration:组件或指令
- providers:依赖注入
- inputs:导入其他模块(对应 exports)
- bootstrap:设置根组件,只在根模块使用
- exports:导出组件或指令等
应用模块实际意义
- 根模块(必须),启动
- 核心模块,来源于根模块的抽离,全局组件或服务,只要在应用启动时,初始化一次即可
- 特性模块(支持懒加载)
- 共享模块(多个特性模块抽离出来)
TypeScript
特性:
- 参数类型声明,冒号 + 类型声明
- 类与接口:class/extends interface/implements
- 可选参数(?)
- as 指明动态生成对象的类型
- 泛型:参数化的类型,一般用来限制集合的内容
- 接口:用处一般有两个
- 参数化的类型
- 配合 implements
- 类型定义文件(*.d.ts):帮助开发者在 TS 中复用已有的 JS 工具包
是 ES6 的超集,ES6 有的,TS 都是支持的,比如
- generator 函数(*、yield、next)
- 扩展符
- 解构表达式
- 注解(装饰器)
访问控制符,严谨使用访问控制符,有利于代码维护和重构
- public:默认
- private: 仅类内部可以访问
- protected:类及子类可以访问
构造函数简洁声明属性语法(构造函数的妙用,此时不可省略访问控制符,public 也不可以)
格式、类型推断与联合类型
- .ts 或 .tsx 文件
- 类型推断,未赋值推断为 any
- 联合类型,使用按位或,此时只能访问联合类型共有的属性和方法
接口
- 可描述类的抽象行为,也可以描述对象的结构形状
- 首字母大写,同时建议加上I字母,表示接口
- 可定义可读属性(readonly),可选属性(?),任意属性
- 属性个数不确定,添加 [propName:string]: any 即可以,propName 并不是语法规定哈
数组
- 类型 + 中括号
- 数组泛型
- 接口表示
函数约束
- 输入参数个数限制、输入参数类型约束、返回值类型约束
- 可采用重载的方式才支持联合类型的函数关系
函数重载
function getValue(value: string): string;
function getValue(value: number): number;
function getValue(value: string|number): string|number {
return value;
}
类型断言
- 手动指定一个值类型
- 语法:
<类型>
或值 as 类型 - 在 tsx 语法中必须用后一种
- 类型断言不是类型转换,断言成一个联合类型中不存在的类型是不允许的
声明文件
- 使用第三方库需要引入.d.ts声明文件
- 使用三斜线指令表明引入了声明文件 Reference path 属性
类型别名
- 使用 type 关键字给新类型起一个别名
- 也可以用 type 来约束只能是某些字符串中的一个
- 通常用来配合联合类型使用,达到简写目的
枚举
- 用于取值被限定在一定范围内的场景
- 使用关键字 enum
- 枚举成员会被赋值从 0 开始递增的数字,也会对枚举值和枚举名进行反向映射
泛型
- 定义接口,类或函数的时候,不预先指定具体的类型,而是在使用的时候再指定类型的一种特性
- 使用大写 T 表示
具体实践
BrowserModule 包含浏览器启动应用的关键逻辑
platformBrowserDynamic 动态引导启动,静态引导需要预先编译
额外的 polyfill
- 动态引导要求引入 reflect-metadata
- zone.js 捕获异步事件,帮助实现高效的变化检测特性
数据获取在组件构造函数中还是 ngOnInit 中呢,答案是 ngOnInit 中
- 可以在构造函数中调用 getHeroes(),但那不是最佳实践。
- 让构造函数保持简单,只做初始化操作,比如把构造函数的参数赋值给属性。
RxJS
首先回顾下异步常见问题
- 回调地狱
- 竞态条件
- 内存泄漏
- 复杂状态管理
- 错误处理
Promise 存在的问题:Promise 解决了错误处理和回调地狱等问题,但方案存在其他问题,一是只有一个结果,二是不可以取消。
RxJS 特点
- 一切数据包装成流的形式,比如 HTTP 请求,DOM 事件或者普通数据
- 强大操作符:每一个操作符都会产生一个新的 Observable,不会对上游的 Observable 做任何修改
RxJS 使用场景
- 事件处理
- 异步编程
- 处理多个值
RxJS 实现原理:观察者模式 + 迭代器模式
- 观察者模式
- 订阅:Observer 通过 Observable 提供的 subscribe() 方法订阅 Observable。
- 发布:Observable 通过回调 next 方法向 Observer 发布事件。
- 迭代器表现:Observer 除了有 next 方法来接收 Observable 的事件外,还可以提供了另外的两个方法:error() 和 complete(),与迭代器模式一一对应。
RxJS 有一个核心和三个重点,一个核心是 Observable(可被观察者),再加上相关的 Operators,三个重点分别是 Observer(观察者)、Subject、Schedulers。
RxJS 中的流以 Observable 对象呈现,获取数据需要订阅 Observable。Observable 序列必须被“订阅”才能够触发上述过程,也就是 subscribe。
subscribe 支持传入函数的形式,第一个函数是 next,这是必须的,后面的 error 和 complete 可选,也可以传入完整的 observer 对象,具体如下
const myObserver = {
next: d => console.log(d),
error: err => console.error(err),
complete: () => console.log('end of the stream')
}
观察者想退订,只要调用订阅返回的对象的 unsubscribe 方法,这样观察者就再也不会接受到 Observable 的信息了。
创建可观察序列,从rxjs
导入,而不是rxjs/operators
- of(...items)可以将普通 JavaScript 数据转为可观察序列
- from(iterable)
- fromPromise(promise)
- fromEvent(elment, eventName)
- ajax(url | AjaxRequest)
- interval、timer
- range
- empty、throwError、never:目前官方不推荐使用 empty 和 never 方法,而是推荐使用常量 EMPTY 和 NEVER
- defer
Observable 是 RxJS 库中的一个关键类。
- 所有的 HttpClient 方法都会返回某个值的 RxJS Observable。
- HTTP 是一个请求/响应式协议。你发起请求,它返回单个的响应。通常,Observable 可以在一段时间内返回多个值。 但来自 HttpClient 的 Observable 总是发出一个值,然后结束,再也不会发出其它值。
- pipe 函数:继续返回 Observable
- subscribe 函数:作为一条通用的规则,Observable 在有人订阅之前什么都不会做。
- 如果某个属性是个 Observable,按照惯例以
$
结尾 - 如果在 *ngFor 中需要迭代 Observable,Angular 有提供 AsyncPipe 管道符。AsyncPipe 会自动订阅到 Observable,这样你就不用再在组件类中订阅了。
常用操作符
合并类操作符
- concat:N 个请求按顺序串行发出(前一个结束再发下一个)
- merge:N 个请求同时发出并且要求全部到达后合并为数组,触发一次回调
- forkJoin:N 个请求同时发出,对于每一个到达就触发一次回调
注意最新的官方文档和 RxJS v5.x 到 6 的更新指南中指出不推荐使用 merge、concat、combineLatest、race、zip 这些操作符方法,而是推荐使用对应的静态方法。
过滤操作符
- take 是从数据流中选取最先发出的若干数据
- takeLast 是从数据流中选取最后发出的若干数据
- takeUntil 是从数据流中选取直到发生某种情况前发出的若干数据
- first 是获得满足判断条件的第一个数据
- last 是获得满足判断条件的最后一个数据
- skip 是从数据流中忽略最先发出的若干数据
- skipLast 是从数据流中忽略最后发出的若干数据
concatMap、switchMap 与 mergeMap
- concatMap = map() + concatAll()
- switchMap = map() + switchAll()
- mergeMap = map() + mergeAll()
forkJoin, zip, combineLatest
- 准备用来合并的流是那种只会发射一次数据就关闭的流时(比如 http 请求),就结果而言这三个操作符没有任何区别。
- 用 forkJoin 合并的流,会在每个被合并的流都发出结束信号时发射一次也是唯一一次数据。
- 当每个传入 zip 的流都发射完毕第一次数据时,zip 将这些数据合并为数组并发射出去;当这些流都发射完第二次数据时,zip 再次将它们合并为数组并发射。以此类推直到其中某个流发出结束信号,整个被合并后的流结束,不再发射数据。
- combineLatest 一开始也会等待每个子流都发射完一次数据,但是在合并时,如果子流1在等待其他流发射数据期间又发射了新数据,则使用子流最新发射的数据进行合并,之后每当有某个流发射新数据,不再等待其他流同步发射数据,而是使用其他流之前的最近一次数据进行合并。
管道模式:之前版本的 RxJS 各种操作符都挂载到了全局 Observable 对象上,可以链式调用,现在需要单独从rxjs/operators
导入,在 pipe 函数中传参使用
catchError 、tap、map 、debounceTime、distinctUntilChanged、switchMap
- catchError:参数需要一个错误处理函数
- map:类似 ES6 map 函数,参数是迭代函数,会改变值
- tap:查看 Observable 中的值,使用那些值做一些事情,并且把它们传出来。 这种 tap 回调不会改变这些值本身。
- debounceTime:防抖
- distinctUntilChanged:会确保只在过滤条件变化时才发送请求。
- switchMap:它会取消并丢弃以前的搜索可观察对象,只保留最近的。
异常处理
对错误处理的处理可以分为两类,即恢复(recover)和重试(retry)。
恢复是虽然发生了错误但是让程序继续运行下去。重试,是认为这个错误是临时的,重试尝试发生错误的操作。实际中往往配合使用,因为一般重试是由次数限制的,当尝试超过这个限制时,我们应该使用恢复的方法让程序继续下去。
多播
多播:一个数据流被多个 Observable 订阅
对已错过数据的两种处理方式
- 错过的就让它过去,只要订阅之后生产的数据就好,类似于直播,Hot Observable
- 不能错过,订阅之前生产的数据也要,类似于点播,Cold Observable
RxJS 中如 interval、range 这些方法产生的 Observable 都是 Cold Observable,产生 Hot Observable 的是由 Promise、Event 这些转化而来的 Observable。
为了防止每次订阅都重新生产一份数据流,我们可以使用中间人,让这个中间人去订阅源数据流,观察者都去订阅这个中间人。这个中间人能去订阅数据流,所以是个 Observer,又能被观察者订阅,所以也是 Observable。
BehaviorSubject、ReplaySubject、AsyncSubject
- BehaviorSubject 有点类似于状态,一开始可以提供初始状态,之后订阅都可以获取最新的状态。
- ReplaySubject 表示重放,在新的观察者订阅时重新发送原来的数据,可以通过参数指定重放最后几个数据。
- AsyncSubject 会在 subject 完结后送出最后一个值。
RxJS 资料
路由
在 Angular 中,最好在一个独立的顶级模块中加载和配置路由器,它专注于路由功能,然后由根模块 AppModule 导入它。
需要用到 @angular/router
中的 RouterModule 和 Routes,大致两个步骤
- 配置具体的 path 和对应的 component
- 调用 forRoot 开启地址变化监听
其他
- 添加路由出口,内置的 router-outlet 组件
- 路由链接 routerLink 属性
- 默认路由 redirectTo
- 路由参数通过冒号标识
- ActivatedRoute 用来获取当前路由组件实例信息,常用来获取路由参数
补充
windows 平台要注意,node-gyp 可能需要安装 visual studio
ng serve --prod --aot
ng build --prod --aot
ng test
单向数据流、数据不可变性(相比 angularJS 性能提升的地方)
生成组件树工具:angular2-dependencies-graph
为什么需要 ngModule
- 资源按需加载
- 文件体积和请求数量需要取得一个平衡
- 利用 Router 和 ngModule 配合实现异步模块
路由配置:path: '**',作为一个 fallback router 必须配置在最后
- 静态路由(path + component)会导致资源被加载都一个文件中
- 动态路由(path + loadChildren)
核心架构思想
- 依赖注入 DI
- 构造器注入
- 每个 HTML 标签都会有一个注射器实例
- @Injectable 是 @Component 的子类
- 数据绑定
- 组件化
cnpm vs npm,npm 会遇到三个问题
- 需要 python 环境,可能依赖某些 .net framework
- node-sass 包被墙了
- node-gyp 编译错误
组件通讯
- @Input @Output
- 借助 service & rxjs(subject)
- 路由参数传递
生命周期,其中 4 - 7 会反复
- onInit
- afterContentInit
- afterViewInit
- onChanges
- doCheck
- afterContentChecked
- afterViewChecked
- onDestroy
模块与共享模块
- 模块是组织业务代码利器(组件、指令、服务、路由)
- 共享模块:针对跨模块的共享组件,此时 angular-cli 打包时,就不会将同一个组件重复打包仅不同的 module,而是仅存在于共享模块中
- 模块懒加载:loadChildren
路由与动态加载
- 基本使用
- path、component、loadChildren、children
- router-outlet
- routerLink
- 路由参数
- activeRoute.params
- 嵌套路由
- 路由守卫
- canActivate 属性配置服务,服务实现 CanActivate 方法
- …………
- PushState 需要服务端配置
表单与数据校验
- 模板驱动型表单:所有内容都写在 HTML 里,包括数据校验与变量定义
#
号定义模板内部变量
- 响应式表单:把功能性代码移植到代码里,比如数据检验
- 动态表单:表单几乎全部是用代码创建
表单数据校验 - 内置规则
- required
- requiredTrue
- minLength
- maxLength
- pattern
- nullValidator
- compose
- composeAsync
自定义校验规则
- 实质上是实现了一个指令
- 实现 Validator
服务器通讯和 RxJS
RxJS 与 Promise 类比
- Observable 可以中途取消,Promise 一旦触发不能取消
- Observable 可以返回一个函数,使用 unsubscribe 可以执行,从而用来取消
- Observable 可以连续发射多个值,而 Promise 可以只能发生一个值
- Observable 提供了很多的工具函数,用来对结果进行处理
RxJS 典型场景
- HTTP服务
- 事件处理
RxJS 可以处理一系列值,因此使用 next 函数,表示下一个
i18n
- TranslateModule 的使用
- translate 管道
前端自动化测试
- 单元测试
- ng test
- Karma + Jasmine 可以测试任意前端框架
- 集成测试
- ng e2e
- Protractor 专门针对 angular 设计
表单验证
响应式表单
- 注册 ReactiveFormsModule
- 组件类:FormControl/FromGroup,支持嵌套
- setValue() 设值
- value 取值
- valid 校验通过
- invalid 校验不通过
- patchValue()
- get() 获取实例
- reset() 重置
- 模板指令
- formControl/formGroup
- formControlName/formGroupName/formArrayName
- FormBuilder:control()、group() 和 array()。用于在组件类中分别生成 FormControl、FormGroup 和 FormArray。
- FormArray 是 FormGroup 之外的另一个选择,用于管理任意数量的匿名控件。
模板驱动表单
- 注册 FormsModule
- NgForm 指令:Angular 会在
<form>
标签上自动创建并附加一个 NgForm 指令。NgForm 指令为 form 增补了一些额外特性。 它会控制那些带有 ngModel 指令和 name 属性的元素,监听他们的属性(包括其有效性)。 它还有自己的 valid 属性,这个属性只有在它包含的每个控件都有效时才是真。 - 当在表单中使用
[(ngModel)]
时,必须要定义 name 属性。在内部,Angular 创建了一些 FormControl,并把它们注册到 Angular 附加到<form>
标签上的 NgForm 指令。 注册每个 FormControl 时,使用 name 属性值作为键值。 - NgModel 指令不仅仅跟踪状态。它还使用特定的 Angular CSS 类来更新控件,以反映当前状态。
- ng-touched ng-untouched
- ng-dirty ng-pristine
- ng-valid ng-invalid
- 显示和隐藏错误消息,#param="ngModel",指令的 exportAs 属性告诉 Angular 如何链接模板引用变量到指令。 这里把 name 设置为 ngModel 是因为 ngModel 指令的 exportAs 属性设置成了 “ngModel”。
- ngSubmit
表单验证
- 导入 Validators
- 内置验证器:required、minlength、maxlength、min、max、email、pattern 等
- 响应式表单的验证 - FormControl 添加验证器函数
- 模板驱动验证 - 验证属性
- 自定义验证器
- 添加到响应式表单 - 直接把函数传入 FormControl
- 添加到模板驱动表单 - 注册成为 NG_VALIDATORS 提供商
- 跨字段交叉验证 - 挂在到 Group 上
- 异步验证 - AsyncValidatorFn 和 AsyncValidator
- 异步验证提示
- model.pending
- 性能注意:把 updateOn 属性从 change(默认值)改成 submit 或 blur 来推迟表单验证的更新时机
依赖注入
依赖注入修饰器
- @Optional():声明可选依赖
- @Host():禁止在宿主组件以上的搜索
- @Self():只在该组件的注入器中查找提供商
- @SkipSelf():跳过局部注入器,并在注入器树向上查找
定义提供商
- useClass:类提供商,创建并返回指定类的新实例。可以使用这类提供商来为公共类或默认类换上一个替代实现
- useExisting:别名提供商,可以使用别名接口来窄化 API。
- useValue:值提供商,为 DI 令牌关联到一个固定的值,通常用来进行运行期常量设置
- useFactory:工厂提供商,提供一个键,让你可以通过调用一个工厂函数来创建依赖实例,第三个键 deps,它指定了供 useFactory 函数使用的那些依赖。
非类依赖
- 不能用 TypeScript 的接口作为令牌,但可以使用抽象类接口,不但可以获得像接口一样的强类型,而且可以像普通类一样把它用作提供商令牌。
- 替代方案之一:在 NgModule 中提供并注入这个配置对象
- 另一个为非类依赖选择提供商令牌的解决方案是定义并使用 InjectionToken 对象。
即便开发者极力避免,仍然会有很多视觉效果和第三方工具需要访问 DOM。通过 ElementRef
前向引用 forwardRef 打破循环
组件直接引用另一个组件
- 查找已知类型的父组件:通过 DI 注入父组件类型,为避免报错,请使用 @Optional() 修饰器
- 如果不知道类型呢?根据父组件的类接口查找,需要父组件合作,以类接口令牌为名,为自己定义一个别名提供商
NgModules
NgModules
- 用于配置注入器和编译器,并帮你把那些相关的东西组织在一起
- NgModule 把组件、指令和管道打包成内聚的功能块,每个模块聚焦于一个特性区域、业务领域、工作流或通用工具。
常用内置模块
- BrowserModule:浏览器运行
- CommonModule:通用模块,比如 ngIf 和 ngFor
- FormsModule:模板驱动表单
- ReactiveFormsModule:响应式表单
- RouterModule:路由功能
- HttpClientModule:通讯功能
BrowserModule 导入了 CommonModule,此外 BrowserModule 重新导出了 CommonModule,以便它所有的指令在任何导入了 BrowserModule 的模块都能使用。
entryComponents
- 大多数情况下不用显示设置入口组件,Angular 会自动把 bootstrap 中的组件以及路由定义的组件添加到入口组件中
- 如果要用其他根据类型来声明式的引导或动态加载某个组件,就必须把它们显示添加到 entryComponents 中了
- 如果一个组件既不是入口组件也不没有在模板中使用过,摇树优化工具就会把它扔出去。 所以,最好只添加那些真正的入口组件,以便让应用尽可能保持精简。
提供单例服务
- 声明该服务应该在应用的根上提供。
- 把该服务包含在 AppModule 或某个只会被 AppModule 导入的模块中。
从 Angular 6.0 开始,创建单例服务的首选方式是在那个服务类上指定它应该在应用的根上提供。只要在该服务的 @Injectable 装饰器上把 providedIn 设置为 root 就可以了
单例服务
- 如果某个模块同时提供了服务提供商和可声明对象(组件、指令、管道),那么当在某个子注入器中加载它的时候(比如路由),就会生成多个该服务提供商的实例。而存在多个实例会导致一些问题,因为这些实例会屏蔽掉根注入器中该服务提供商的实例,而它的本意可能是作为单例对象使用的。
- 处理方式:在模块上创建静态方法 forRoot,把那些服务提供上放进 forRoot 方法中
在使用来自其它模块的组件和来自其它模块的服务时,有一个很重要的区别。 当你要使用指令、管道和组件时,导入那些模块就可以了。而导入带有服务的模块意味着你会拥有那个服务的一个新实例,这通常不会是你想要的结果(你通常会想取到现存的服务)。使用模块导入来控制服务的实例化。
NgModule 是组织 Angular 应用的一种方式,它们通过 @NgModule 装饰器中的元数据来实现这一点。 这些元数据可以分成三类
- 静态的:编译器配置,通过 declarations 配置
- 运行时:通过 providers 数组提供给注入器的配置
- 组合/分组:通过 imports 和 exports 数组来把多个 NgModule 放在一起
模块类型
- SharedModule:为那些可能会在应用中到处使用的组件、指令和管道创建 SharedModule。 这种模块应该只包含 declarations,并且应该导出几乎所有 declarations 里面的声明。
- CoreModule:为你要在应用启动时加载的那些服务创建一个带 providers 的 CoreModule。
- 特性模块:围绕特定的应用业务领域创建的模块
项目结构
Angular 项目结构组织实践 -- TODO