Angular1.0
即angular.js
诞生于2010年底,在2014-2016这几年大火,由于其双向数据绑定带来的开发便利,迅速淘汰了当初因MVC思想红极一时的BackboneJS
。但是它的脏检查机制以及重量级框架带来的性能问题广为诟病。然后前端有开始流行以ReactJS
和VueJS
为主的轻量级框架。面对市场份额不断被蚕食,Angular
团队于2015年推出了2.0版本,即现在的Angular
。由于改动很大,引入TS和模块化等,框架由重量级调整为轻量级,API变动也很大,可以看作是两个框架。尽管有着昔日的辉煌,但流行度却再也赶不上ReactJS
和VueJS
了,,,然而它仍在前端江湖排行榜前三,所以仍旧不可忽视。
Angular
虽然在国内用的比较少,但在国外却很流行,很多外企都是使用Angular
,结合当下大环境,国内萧条,国外繁荣、外企招人,所以是时候学习Angular
了,这个不能拖了。(注意文本中的内容不包含Angular1.0
)
不过我还是推荐你先去官方学习,先把照官方教程的三个案例、理解Angular篇 和 开发指南篇都学完,再来看我这篇笔记。
Angular
是一个基于TypeScript
构建的开发平台。它包括
总之Angular可以理解为“保姆级框架”,只是通过模块化的重构可以插拔式集成使用了。
服务提供者作为依赖被注入到组件中,这能使你的代码更加模块化、更加可复用、更加高效
模块、组件和服务都是使用装饰器的类,这些装饰器会标出它们的类型并提供元数据,以告知 Angular 该如何使用它们。
NgModule 与 ES2015的模块不同而且有一定的互补性。NgModule作为一个组件集声明了编译的上下文环境,它专注于某个应用领域、某个工作流或一组紧密相关的能力。NgModule可以将其组件和一组相关代码(如服务)关联起来,形成功能单元。
ts@ngModel({
decarations: [组件、指令或管道,],
imports: [模块,],
providers: [service,],
bootstrap: [启动的组件,]
})
export class YouModule {}
Angular 需要知道如何把应用程序的各个部分组合到一起,以及该应用需要哪些其它文件和库。这些信息被称为元数据(metadata)。 (注解中的内容)
模块的坑
Angular CLI
是开发Angular
应用程序的最快、直接和推荐的方式。Angular CLI
能简化许多任务。
创建一个项目
npm install -g @angular/cli
ng new my-app
cd my-app && ng serve --open
ng new
做了哪些事情?
ng serve
做了哪些事情?
构建本应用
启动开发服务器
监听源文件,发生变化时构建本应用
ng build
把Angular应用编译到一个输出目录中
ng server
构建你的应用并启动开发服务器,当有文件变化时就重新构建
ng generate
基于schematic 生成或修改某些文件
ng generate component compName
ng generate service serName
ng generate person
ng generate module app-routing --flat --module=app
--skip-tests
--standalone
--inline-template
ng g c compName
ng test
在指定的项目上运行单元测试。
ng e2e
构建一个Angular应用并启动开发服务器,然后运行端到端的测试。
@Component()
中的主要功能如下:
selector(app-product-alerts)
用于标识组件。按照惯例,Angular
组件选择器以前缀 app-
开头,后跟组件名称。HTML
和 CSS
。tsimport {component} from '@angular/core';
@component({
selector: 'app-xx', // 用于标识组件 以前缀 `app-` 开头
templateUrl: './xx.html',
stylesUrls: ['./xx.css'],
standalone: true, // 描述组件是否需要 ngModule。
template: '<h1>Hello World!</h1>',
styles: ['h1 { font-weight: normal; }']
encapsulation: ViewEncapsulation.Emulated
})
export class Xx {}
当 Angular 实例化组件类并渲染组件视图及其子视图时,组件实例的生命周期就开始了。生命周期一直伴随着变更检测,Angular 会检查数据绑定属性何时发生变化,并按需要更新视图和组件实例。当Angular销毁组件实例并从DOM中移除它渲染的模版时,生命周期就结束了。
当Angular创建、更新、销毁实例时,指令就有了类似的生命周期.
你的应用可以使用生命周期钩子方法来触发组件或指令生命周期中的关键事件,以初始化新势力需要时启动变更检测,在变更检测过程中响应更新并在删除势力之前进行清理。
生命周期的顺序
constructor
构造函数永远首先被调用ngOnChanges()
ngOnInit()
组件初始化时被调用
constructor
ngDoCheck()
脏值检测时被调用ngAfterContentInit()
当内容投影(插槽)完成时调用(在子组件中)ngAfterContentChecked()
检测投影内容时调用(多次)(在子组件中)ngAfterViewInit()
当组件视图(子视图)初始化完成时ngAfterViewChecked()
当检测时图变化时(多次)ngOnDestroy()
当组件销毁时
注: Init 钩子 只调用一次
DestoryRef
钩子,用于 替代 ngOnDestroy()
优化代码逻辑
ts@Component(...)
class Counter {
count = 0;
constructor() {
const id = setInterval(() => this.count++, 1000);
const destroyRef = inject(DestroyRef);
destroyRef.onDestroy(() => clearInterval(id));
}
}
takeUntilDestroyed
参考这篇文章
{{}}
[attr]="attr"
attr={{}}
(click)="onclick()"
[(ngModel)]="input"
双向绑定 需要引入FormsModule
<input type='text' [ngModel]='username' (ngModelChange)="username= $event">
<input type='text' [value]='username' (input)="username= $event.target.value">
*ngIf='isLogin'
html<div *ngIf="条件表达式 else elseContent">
条件为真显示的内容
<div>
<ng-template #elseContent>
条件为假显示的内容
</ng-template>
<div *ngIf="条件表达式; then thenTemplate; else elseTemplate">
</div>
<ng-template #thenTemplate>
条件为真显示的内容
</ng-template>
<ng-template #elseTemplate>
条件为假显示的内容
</ng-template>
*ngSwitch
、ngSwitchCase
、ngSwitchDefault
html<div [ngSwitch]="conditionExpression">
<ng-template [ngSwitchCase]="case1Exp">...</ng-template>
<ng-template ngSwitchCase="case2LiteralString">...</ng-template>
<ng-template ngSwitchDefault>...</ng-template>
</div>
循环 *ngFor="let item of arr"
*ngFor="let item of arr; let i = index"
同上; let first=first; let last=last"
同上; let odd=odd; let even=even
同上; trackBy: trackElement
获取元素的值 <input type='text' #Input (input)="onInput(Input.value)">
获取元素的值(在JS中) @ViewChild()
@ViewChild('eleRef', {static: true})
static的意思是没有在那个 If或ngFor当中<ng-content>
内容投影(插槽)
样式绑定
html<div [class.className]="条件表达式">...</div>
<div [class.className2]="条件表达式">...</div>
<div [ngClass]="{'One': true, 'Two': false}"></div>
<div [ngStyle]="{'color': someColor, 'font-size': fontSize}"></div>
[attr]="attr"
父组件属性传递给子组件, 子组件@Input()
接收getter
和存值取setter
监听数据变更,当然也可以通过ngOnChanges
周期钩子(event)="pEvent($event)"
,子组件@Output() event = new EventEmitter()
再通过 event.emit('xxx')
触发ts// 父子组件通过本地变量互动
@component({
//...
template: `<button type="button" (click)="child.start()">Start</button>
<app-child #child>`
})
export class Parent{
}
// 父组件本身的代码对子组件没有访问权。
@ViewChild()
, 类似vue或react中的refts// 父级调用 @ViewChild()
@component({
//...
template: `<app-child>`
})
export class Parent {
@ViewChild(ChildComponent)
private childComponent!: ChildComponent;
}
这里在Angular1.0和Vue中称为过滤器
比如 1234.88
=> ¥1,234.88
{{obj | json}}
{{data | data: 'MM-dd'}}
{{price | currency: 'CNY' : 'symbol' : '0.0-2'}}
{{data | slice:1:3}}
自定义管道
tsimport { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'appAgo'
})
export class AgoPipe implements PipeTransform {
transform(value: any, ...args: any[]): any {
if(value) {
const second = (Date.now() - (new Date(value)).getTime())/1000
if(second < 30) {
return '刚刚'
}
return new Date(value).toLocaleDateString();
}
}
}
在 module中注入AgoPipe
,然后可以在模版中使用了 {{date | appAgo}}
<ng-content select="样式类/html标签/指令"></ng-content>
内容投影 可类比 Vue的插槽,(Vue的插槽有 默认插槽、具名插槽、作用域插槽),但是angular好像没有作用域插槽一说,个人理解需要service模拟
*ngIf
,书写起来比较麻烦条件内容投影示例
ts<app-child>
<ng-template appExampleZippyContent>
It depends on what you do with it.
</ng-template>
</app-child>
ts@Directive({
selector: '[appExampleZippyContent]',
})
export class ZippyContentDirective {
constructor(public templateRef: TemplateRef<unknown>) {}
}
@Component({
selector: 'app-child',
template: `
<div *ngIf="expanded">
<ng-container [ngTemplateOutlet]="content.templateRef"></ng-container>
</div>
`,
})
export class ChildComponent {
@Input() expanded = false;
@ContentChild(ZippyContentDirective) content!: ZippyContentDirective;
}
<ng-container>
不增加节点组件不应该直接获取或保存数据,它们不应该了解是否在展示假数据。它们应该聚焦于展示数据,而把数据访问的职责委托给某个服务。
两种用法
ts@Injectable({
providedIn: 'root' // 这是什么意思?
})
这其实是按需引入的意思, 如果你没有使用这个service 就不会参与打包
todo ajax示例
todo ajax拦截器
允许您以声明性和可充用的方式向元素添加新行为。
*ngIf="hasPrivileges"
*ngFor="let task of taskList"
自定义Angular指令可以通过指令后缀 如 my-custom-name.directive.ts
ts@Directive({
selector: '[appHighlight]',
})
export class HighLightDirective {
private el = inject(ElementRef);
constructor(private el: ElementRef, private rd2: Renderer2) {
this.el.nativeElement.style.backgroundColor = 'yellow'
// rd2.setStyle(el.nativeElement, 'display', 'grid');
}
}
// <p appHighLight>Look</p>
Angular中的指令分为三类
指令也有生命周期 比组件少
.forRoot
.forChild
<a routerLink="/home/1">
constructor(private router: Router){}
+ this.router.navigate(['home', tab.id])
this.router.navigate(['home', tab.id, {name: 'zs'}])
对应 /home/1;name=zs
通过paramsMap
取值this.router.navigate(['home', {queryParams: {name: 'zs'}}])
对应 /home?name=zs
通过queryParams
取值<a [routerLink]="['/home']" [queryParams]="{name: 'val1'}">
this.router.paramMap.subscribe(params => {...})
this.router.queryParamMap.subscribe(params => {...})
routerLinkActive="active"
+ .active{}
tsconst routes: Routes = [{
path: 'home',
component: HomeContainerComponent,
children: [ // 嵌套路由
// 重定向路由
{path: '', redirectTo: 'hot', pathMatch: 'full'},
// 动态路由
{path: ':tabLink', component: HomeDetailComponent}
]
}, {
path: '**',
component: 404Component
}]
<router-outlet>
配合使用
<router-outlet name='first'>
<===> {path: 'xx', component: Comp, outlet: 'first'}
这就是辅助路由, 辅助路由可以很奇怪如http://xxx.x/xx(grand//second:aux)?name=zs
了解即可懒加载
tsconst routes: Routes = [
{path: '', redirectTo: 'home', pathMatch: 'full'},
{path: 'my', loadChildren: () => import('./my').then(m => m.MyModule)}
];
标题 | |
---|---|
Angular Router | 基于Angular组件的路由机制。支持惰性加载、嵌套路由、自定义路径匹配规则等 |
Angular Forms | 统一的表单填报与验证体系 |
Angular HttpClient | 健壮的HTTP客户端库,支持更高级的客户端-服务器通讯 |
Angular Animations | 丰富的动画体系,用于驱动基于用户状态的动画 |
Angular PWA | 一些用于构建渐进式Web应用PWA的工具 |
Angular Schematice | 一些搭建脚手架、重构和升级的自动化工具 |
import {HttpClientModule} from '@angular/common/http'
@angular/core
@angular/forms
@angular/common/http
@angular/router
VSCode Angular插件 Angular Extension Pack
Q: 什么是脏值检测?
A: 当数据改变时更新视图(DOM)
Q: 什么时候会触发脏值检测?
A: 有三类
1 浏览器事件(如click、mouseover、keyup等)
2 setTimeout() 和 setInterval()
3 HTTP请求
Q:如何进行检测?
A:比较当前状态和新状态的值
Q:Angular内部如何得知组件内部变化需要更新视图的呢?
A:Angular在读取视图模板时进行绑定
对于每个视图有多个绑定,angular是一个视图的树状结构,
当触发脏值检测时会 同步检查单向数据流
Q:组件的生命周期和脏检测有什么关系
A:
在钩子AfterViewChecked
和AfterViewInit
中是不能改变有绑定关系的数据值,否则会造成无限循环的脏值检测
这个检查过程是十分高效的,但是当树的结构太大(几万几十万个节点),仍旧会出现性能瓶颈,优化方案是OnPush策略
默认策略是,不管任何时候发生数据变化,都会检测整棵树,
OnPush策略是, 只对组件当中的有input注解的属性进行检测,这个注解的属性发生改变,我们会进行一次脏值检测,如果它不改变,我们就不检测,而且** **。
当然这种优化的方案带来的问题是,有很多时候需要手动操作进行脏值检测
所以推荐笨组件(无状态组件或者只接收prop自身无state的组件),业务逻辑放在父组件处理
Q: 为什么脏值检测进行两次? A: 第一次是全量检查整棵树, 第一遍检查完立即进行第二遍检查, 第二次并不是完整的检查,而是看第一遍的值有没有新变化,如果有变化,为避免死循环,Angular会抛出异常
有些情况(秒杀、限时抢购)必须在钩子AfterViewChecked
和AfterViewInit
中更新视图,怎么办?这就需要用到ngZone
,但是在学习ngZone之前,我们先来了解下什么是Zone
?
Zone其实是一个第三方的类库 Zone.js
, 它是一个JS的运行时,我们可以把浏览器划分为若干个区域,每个区域独立运行自身程序,彼此互不干扰, angular程序本身运行在一个Zone中,如果我们把某些更新放在另一个Zone中,这样就绕过了angular的脏值检测。
案例 毫秒级倒计时
代码如下
tsimport { formatDate } from '@angular/common';
import { AfterViewChecked, ChangeDetectionStrategy, Component, ElementRef, NgZone, Renderer2, ViewChild } from '@angular/core';
@Component({
selector: 'app-ngzone',
template: `<span #timeRef></span>`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class NgzoneComponent implements AfterViewChecked {
@ViewChild('timeRef', {static: true})
timeRef: ElementRef| null = null;
constructor(private ngZone: NgZone, private rd: Renderer2) {
}
ngAfterViewChecked(): void {
this.ngZone.runOutsideAngular(() => {
setInterval(() => {
this.rd.setProperty(
this.timeRef!.nativeElement,
'innerText',
formatDate(Date.now(), 'HH:mm:ss:SSS', 'en-US')
)
}, 100)
})
}
}
ChangeDetectionStrategy.OnPush
告知angualar 它只接收父组件的状态变化,自身不维护状态不会触发脏检查。
那如果我们既想用OnPush,又需要在某个方法更新视图怎么办?
tsimport { ActivatedRoute } from '@angular/router';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit } from "@angular/core";
@Component({
selector: 'app-handle',
template: `...`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class NgzoneComponent implements OnInit{
constructor(
private route: ActivatedRoute,
private cd: ChangeDetectorRef) { }
selectedTab = 'aa';
ngOnInit(): void {
this.route.paramMap.subscribe(params => {
this.selectedTab = params.get('xx') as string
this.cd.markForCheck()
})
}
}
afterRender
afterNextRender
Vite
和 esbuild
angular/ssr
包rxjs ==> Reactive Extensions For Javascript
RxJS
是一个编写异步和基于事件的程序的库。RxJS
是一个使用可观察序列编写异步和基于事件的程序的库。Observable
、一些周边类型(Observer
、Scheduler
、Subjects
)和类似于 Array 方法(map
、filter
、reduce
、every
等)的操作符,以便将异步事件作为集合进行处理。学习rxjs,思维观念要转变:rx要把事件或数据看成一个流,所谓的响应式编程即随着流中的元素变化随之做出相应的动作
可以将 RxJS
视为处理事件的 Lodash
。
ReactiveX 将观察者模式与迭代器模式和使用集合的函数式编程相结合,以便让你更好地管理事件序列。
RxJS 中解决异步事件管理的基本概念有:
Observable
(可观察者):表示未来(future)值或事件的可调用集合的概念。Observer
(观察者):是一个回调集合,它知道如何监听 Observable 传来的值。Subscription
(订阅):表示 Observable 的一次执行,主要用于取消执行。Operator
(操作符):是纯函数,可以使用 map、filter、concat、reduce 等操作来以函数式编程风格处理集合。Subject
(主体):相当于一个 EventEmitter,也是将一个值或事件多播到多个 Observers 的唯一方式。Scheduler
(调度器):是控制并发的集中化调度器,允许我们在计算发生时进行协调,例如 setTimeout 或 requestAnimationFrame 或其它。Q: 前端有哪些处理异步的方式?
tsimport { Observable } from 'rxjs';
// 1. 创建Observers
const observable = new Observable(
/* subscribe函数 */
(subscriber) => {
subscriber.next(1);
subscriber.next(2);
// subscriber.next(3 + (12n as any));
// 报错触发error(),后续不在执行
setTimeout(() => {
subscriber.complete();
// complete之后不再会触发next
subscriber.next(4);
}, 1000);
}
);
console.log('just before subscribe');
// 订阅
observable.subscribe({
next(x) {
console.log('got value ' + x);
},
error(err) {
console.error('something wrong occurred: ' + err);
},
complete() {
console.log('done');
},
});
console.log('just after subscribe');
// Observable是惰性求值, 而EventEmmitter是急性执行
// Observable和普通函数的区别是,可以随着时间的推移返回多个值
// 与事件处理器的区别是,它不会注册监听器,所以不需要销毁
tsconst myObservable = new Observable((subscriber) => {
const timerId = setInterval(() => {
console.log('---');
subscriber.next('hello');
}, 1000);
return function unsubscribe() {
console.log('unsubscribe', arguments);
clearInterval(timerId);
};
});
const mySubscription = myObservable.subscribe(console.log);
setTimeout(() => {
mySubscription.unsubscribe();
}, 3400);
tsconst observer = {
next: (x) => console.log('Observer got a next value: ' + x),
error: (err) => console.error('Observer got an error: ' + err),
complete: () => console.log('Observer got a complete notification'),
};
next()
error()
comple()
是可选的
observable.subscribe(fn)
; fn
默认是next()
操作符是函数。有两种操作符
obs.pipe(op1, op2);
即op2(op1(obs))
除了将单播Observer转换为多播,Subject 类型还有一些特化: BehaviorSubject
、ReplaySubject
和 AsyncSubject
。
BehaviorSubject
tsconst subject = new BehaviorSubject(0); // 0 is the initial value
subject.subscribe({
next: (v) => console.log(`observerA: ${v}`),
});
setTimeout(() => {
subject.next(1);
setTimeout(() => {
subject.subscribe({
next: (v) => console.log(`observerB: ${v}`),
});
setTimeout(() => {
subject.next(2);
}, 1000);
}, 2000);
}, 1000);
// 打印 observerA:0
// 1s后 打印 observerA:1
// 再隔2s后打印 observerB: 1 // 不会打印 observerB: 0
// 再隔2s后打印 observerA: 2 observerB: 2
/*
如果改成 Subject
1. 不能携带初始化参数
2. 不可以将旧值发给订阅者
效果: 先打印 observerA:1 隔2s后打印 observerA: 2 observerB: 2
*/
ReplaySubject
与BehaviorSubject类似,ReplaySubject会记录自Observable执行的多个值,并将他们重播给新订阅者
除了设置缓存区大小外, 还可以指定窗口时间(毫秒)
AsyncSubject
仅将Observable执行的最后一个值发送给器Observer,并且仅在执行完成时发送。
调度器控制某个订阅何时开始,以及何时传送通知。它由三部分组成
使用调度器 .pipe(observeOn(asyncScheduler))
调度器的类型
看一个示例
由上图可知rxjs不仅仅能简化代码而且还优化代码,有以下两点
OnPush
, 则也不需要使用this.cd.markForCheck()
, Angular内部已经协调好问题。再来看一个倒计时的案例
tsimport {
Component,
OnInit,
Input,
ChangeDetectionStrategy
} from '@angular/core';
import { Observable, interval } from 'rxjs';
import { map, takeWhile } from 'rxjs/operators';
@Component({
selector: 'app-count-down',
template: `<div>{{ countDown$ | async }}</div>`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CountDownComponent implements OnInit {
@Input() startDate = new Date();
@Input() futureDate: Date;
private _MS_PER_SECOND = 1000;
countDown$: Observable<string>;
constructor() {}
ngOnInit() {
this.countDown$ = this.getCountDownObservable(
this.startDate,
this.futureDate
);
}
private getCountDownObservable(startDate: Date, futureDate: Date) {
return interval(1000).pipe(
map(elapse => this.diffInSec(startDate, futureDate) - elapse),
takeWhile(gap => gap >= 0),
map(sec => ({
day: Math.floor(sec / 3600 / 24),
hour: Math.floor((sec / 3600) % 24),
minute: Math.floor((sec / 60) % 60),
second: Math.floor(sec % 60)
})),
map(({ hour, minute, second }) => `${hour}:${minute}:${second}`)
);
}
private diffInSec = (start: Date, future: Date): number => {
const diff = future.getTime() - start.getTime();
return Math.floor(diff / this._MS_PER_SECOND);
}
}
如果两个流(异步)有依赖关系怎么处理?
首先不需要嵌套, rxjs提供了高级流
tsthis.data1$ = this.service.getData1().pipe(...)
this.data2$ = this.data1$.pipe(
switchMap(data1 => this.service.getData2(data1)),
// ... your operator
)
本文作者:郭郭同学
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!