1、概述

  • 项目结构
  1. npm install -g @angular/cli
  2. ng new my-app

生成的项目结构与 react,vue区别较大
Angular - 语法基础 - 图1
cli 隐藏了 webpack 配置文件,取而代之多了一个angular.json 文件,该文件指定了应用的入口文件,静态资源等,有点儿类似小程序的 project.config.json

  • 主要文件
    1、main.ts 为主 js 入口,其内容基本是固定的,不需要修改
  1. import { enableProdMode } from '@angular/core';
  2. import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
  3. import { AppModule } from './app/app.module';
  4. import { environment } from './environments/environment';
  5. if (environment.production) {
  6. enableProdMode();
  7. }
  8. platformBrowserDynamic().bootstrapModule(AppModule)
  9. .catch(err => console.error(err));

2、app.module.ts
一个应用一般由很多模块(组件、服务等)组成,angular中需要将这些模块在一个集中的地方统一声明,可以看作是组装应用核心模块的地方

  1. import { BrowserModule } from '@angular/platform-browser';
  2. import { NgModule } from '@angular/core';
  3. import { AppRoutingModule } from './app-routing.module';
  4. import { AppComponent } from './app.component';
  5. import { TestComponent } from './test/test.component';
  6. @NgModule({
  7. declarations: [ // 引入组件的地方
  8. AppComponent,
  9. TestComponent
  10. ],
  11. imports: [ // 引入模块的地方
  12. BrowserModule,
  13. AppRoutingModule
  14. ],
  15. providers: [],
  16. bootstrap: [AppComponent]
  17. })
  18. export class AppModule { }

3、路由模块写法

  1. import { NgModule } from '@angular/core';
  2. import { Routes, RouterModule } from '@angular/router';
  3. const routes: Routes = [];
  4. @NgModule({
  5. imports: [RouterModule.forRoot(routes)],
  6. exports: [RouterModule] // 必须 exports
  7. })
  8. export class AppRoutingModule {}

4、组件写法
使用 ng g c test2 命令将会在src/app 下生成 test2 目录并包含四个文件:模板test2.component.html, 样式test2.component.scss,逻辑 test2.component.ts 以及一个测试文件 test2.component.spec.ts

  1. // test2.component.ts
  2. import { Component, OnInit } from '@angular/core';
  3. @Component({
  4. selector: 'app-test2',
  5. templateUrl: './test2.component.html',
  6. styleUrls: ['./test2.component.scss']
  7. })
  8. export class Test2Component implements OnInit {
  9. constructor() { }
  10. ngOnInit() {
  11. }
  12. }

5、服务写法
使用 ng g s test 命令将会在src/app 下生成 test.service.ts 服务及对应的测试文件,可以加 —spec false 来避免生成测试文件

  1. import { Injectable } from '@angular/core';
  2. @Injectable({
  3. providedIn: 'root'
  4. })
  5. export class TestService {
  6. constructor() { }
  7. }

2、基本语法

1、# 语法
# 是获取 dom 引用,类似 react 的 ref

  1. import {Component, OnInit} from '@angular/core';
  2. @Component({
  3. selector: 'app-simple-form',
  4. template: `
  5. <div>
  6. <input #myInput type="text">
  7. <button (click)="onClick(myInput.value)">点击</button>
  8. </div>
  9. `,
  10. styles: []
  11. })
  12. export class SimpleFormComponent implements OnInit {
  13. onClick(value) {
  14. console.log(value);
  15. }
  16. ngOnInit() {}
  17. }

2、事件绑定
(eventName)=”onXX()” 是进行事件绑定的语法,如上例。如果想获得原生事件对象,可传入 $event 到处理函数中,位置随意。注意要写成调用的形式,而不仅仅是函数名

3、属性绑定

  1. <show-title [title]="title"></show-title> // 等价于
  2. <show-title bind-title="title"></show-title>

4、双向绑定
Angular 中 [] 实现了模型到视图的数据绑定,() 实现了视图到模型的事件绑定。把它们两个结合在一起 [()] 就实现了双向绑定。也被称为 banana in the box 语法

5、插值
可以使用{{}}语法将模型中数据绑定到模板中,其内还可以使用管道来格式化输出

  1. import { Component } from '@angular/core';
  2. @Component({
  3. selector: 'my-app',
  4. template: `<h1>Hello {{name}}</h1>`,
  5. })
  6. export class AppComponent {
  7. name = 'Angular';
  8. }

6、常用结构指令
ngFor , ngSwitch, ngIf ,使用指令需要在其前加 *

  1. @Component({
  2. selector: 'sl-user',
  3. template: `
  4. <h2>大家好,我是{{name}}</h2>
  5. <p>我来自<strong>{{address.province}}</strong>省,
  6. <strong>{{address.city}}</strong>市
  7. </p>
  8. <div *ngIf="showSkills">
  9. <h3>我的技能</h3>
  10. <ul>
  11. <li *ngFor="let skill of skills">
  12. {{skill}}
  13. </li>
  14. </ul>
  15. </div>
  16. `
  17. })
  18. // ngIf 的 else,必须平级
  19. <ng-template #hidden>
  20. <p>You are not allowed to see our secret</p>
  21. </ng-template>
  22. <p *ngIf="shown; else hidden">
  23. Our secret is being happy
  24. </p>
  25. // ngSwitch
  26. <div class="container" [ngSwitch]="myVar">
  27. <div *ngSwitchCase="'A'">Var is A</div>
  28. <div *ngSwitchCase="'B'">Var is B</div>
  29. <div *ngSwitchCase="'C'">Var is C</div>
  30. <div *ngSwitchDefault>Var is something else</div>
  31. </div>

7、ngClass vs ngStyle

  1. // 数组语法
  2. <button [ngClass]="['btn', 'btn-primary']">提交</button>
  3. // 对象语法
  4. <button [ngClass]="{ btn: true, 'btn-primary': true }">提交</button>
  5. <button [ngStyle]="{background: 'red'}">提交</button>

8、ng-template vs ng-container
:是一个逻辑容器,可用于对节点进行分组,它将被渲染为 HTML中的 comment 元素,它可用于避免添加额外的元素来使用结构指令,ng-template 一般配合 # 语法一起使用

  1. <ng-template [ngIf]="true"> // 不用 * 而是 []
  2. <p> ngIf with a template.</p>
  3. </ng-template>
  4. <ng-container>
  5. <p> ngIf with an ng-container.</p>
  6. </ng-container>

9、样式

  • :host 伪类选择器,选择当前的组件
  1. :host {
  2. border: 2px solid dimgray;
  3. display: block;
  4. padding: 20px;
  5. }
  • :host-context 向上寻找,当祖先有某个类时,样式生效
  1. :host-context(.red-theme) .btn-theme {
  2. background: red;
  3. }
  4. :host-context(.blue-theme) .btn-theme {
  5. background: blue;
  6. }

Angular 的样式具有隔离的效果,即一个组件内的CSS样式只会影响自己的模版部分,不会影响子组件和其它任意组件。这是基于属性选择器做到的。(Angualr编译模版时,给html元素加了类似 _ngcontent,_nghost 之类的自定义属性,再对关联的css文件做处理,将原本的纯类式写法改成类式+属性选择器的写法)注意不是基于 Shadow Dom 的哦,因为Shadow Dom 的兼容性还不太好。

3、组件语法

1、组件的 props down,events up

  • Input
    父组件向子组件传递数据。
  1. // counter.component.ts
  2. import { Component, Input, SimpleChanges, OnChanges } from '@angular/core';
  3. @Component({
  4. selector: 'exe-counter',
  5. template: `
  6. <p>当前值: {{ count }}</p>
  7. <button (click)="increment()"> + </button>
  8. <button (click)="decrement()"> - </button>
  9. `
  10. })
  11. export class CounterComponent implements OnChanges{
  12. @Input() count: number = 0;
  13. ngOnChanges(changes: SimpleChanges) {
  14. console.dir(changes['count']);
  15. }
  16. increment() {
  17. this.count++;
  18. }
  19. decrement() {
  20. this.count--;
  21. }
  22. }
  23. // app.component.ts
  24. import { Component } from '@angular/core';
  25. @Component({
  26. selector: 'exe-app',
  27. template: `
  28. <exe-counter [count]="initialCount"></exe-counter>
  29. `
  30. })
  31. export class AppComponent {
  32. initialCount: number = 5;
  33. }

当数据绑定输入属性的值发生变化的时候,Angular 将会主动调用 ngOnChanges 方法。它会获得一个 SimpleChanges 对象,包含绑定属性的新值和旧值,它主要用于监测组件输入属性的变化

  • Output

子组件将信息通过事件的形式通知到父级组件

  1. // counter.component.ts
  2. import { Component, Input, Output, EventEmitter } from '@angular/core';
  3. @Component({
  4. selector: 'exe-counter',
  5. template: `
  6. <p>当前值: {{ count }}</p>
  7. <button (click)="increment()"> + </button>
  8. <button (click)="decrement()"> - </button>
  9. `
  10. })
  11. export class CounterComponent {
  12. @Input() count: number = 0;
  13. @Output() change: EventEmitter<number> = new EventEmitter<number>();
  14. increment() {
  15. this.count++;
  16. this.change.emit(this.count);
  17. }
  18. decrement() {
  19. this.count--;
  20. this.change.emit(this.count);
  21. }
  22. }
  23. // app.component.ts
  24. import { Component } from '@angular/core';
  25. @Component({
  26. selector: 'exe-app',
  27. template: `
  28. <p>{{changeMsg}}</p>
  29. <exe-counter [count]="initialCount"
  30. (change)="countChange($event)"></exe-counter>
  31. `
  32. })
  33. export class AppComponent {
  34. initialCount: number = 5;
  35. changeMsg: string;
  36. countChange(event: number) {
  37. this.changeMsg = `子组件change事件已触发,当前值是: ${event}`;
  38. }
  39. }

2、ngOnInitconstructor 区别
constructor 中拿不到父组件传递给子组件的值,一般只是在 constructor 中做依赖注入的操作。初始化操作放在 ngOnInit 钩子中去执行

  1. import { Component, ElementRef } from '@angular/core';
  2. @Component({
  3. selector: 'my-app',
  4. template: `
  5. <h1>Welcome to Angular World</h1>
  6. <p>Hello {{name}}</p>
  7. `,
  8. })
  9. export class AppComponent implements OnInit {
  10. name: string = '';
  11. constructor(public elementRef: ElementRef) { // 使用构造注入方式注入依赖对象
  12. }
  13. ngOnInit() {
  14. this.name = 'Semlinker'; // 执行数据初始化操作
  15. }
  16. }

3、ng-content 类似 slot

  1. import { Component } from '@angular/core';
  2. @Component({
  3. selector: 'wrapper',
  4. template: `
  5. <div class="box red">
  6. <ng-content></ng-content>
  7. </div>
  8. <div class="box blue">
  9. <ng-content select="counter"></ng-content>
  10. </div>
  11. `,
  12. styles: [`
  13. .red {background: red;}
  14. .blue {background: blue;}
  15. `]
  16. })
  17. export class Wrapper { }
  18. //
  19. <wrapper>
  20. <span>This is not a counter</span>
  21. <counter></counter>
  22. </wrapper>

4、 ngAfterViewInit 父级组件模板中引用了子组件,且在父组件中想调用子组件的某些功能时使用:

  1. //
  2. template: `
  3. <div>-- child view begins --</div>
  4. <app-child-view></app-child-view>
  5. <div>-- child view ends --</div>`
  6. //
  7. export class AfterViewComponent implements AfterViewChecked, AfterViewInit {
  8. private prevHero = '';
  9. // Query for a VIEW child of type `ChildViewComponent`
  10. @ViewChild(ChildViewComponent, {static: false}) viewChild: ChildViewComponent;
  11. ngAfterViewInit() {
  12. // viewChild is set after the view has been initialized
  13. this.logIt('AfterViewInit');
  14. this.doSomething();
  15. }
  16. ngAfterViewChecked() {
  17. // viewChild is updated after the view has been checked
  18. if (this.prevHero === this.viewChild.hero) {
  19. this.logIt('AfterViewChecked (no change)');
  20. } else {
  21. this.prevHero = this.viewChild.hero;
  22. this.logIt('AfterViewChecked');
  23. this.doSomething();
  24. }
  25. }
  26. // ...
  27. }

5、ngContentInit 当在组件标签中嵌入其他html或者其他组件时,想获取嵌入的内容时使用,常与 ng-content标签一起使用:

  1. // after-parent-template
  2. `<after-content>
  3. <app-child></app-child>
  4. </after-content>`
  5. // after-template
  6. <div>-- projected content begins --</div>
  7. <ng-content></ng-content>
  8. <div>-- projected content ends --</div>
  9. //
  10. export class AfterContentComponent implements AfterContentChecked, AfterContentInit {
  11. private prevHero = '';
  12. comment = '';
  13. // Query for a CONTENT child of type `ChildComponent`
  14. @ContentChild(ChildComponent, {static: false}) contentChild: ChildComponent;
  15. ngAfterContentInit() {
  16. // contentChild is set after the content has been initialized
  17. this.logIt('AfterContentInit');
  18. this.doSomething();
  19. }
  20. ngAfterContentChecked() {
  21. // contentChild is updated after the content has been checked
  22. if (this.prevHero === this.contentChild.hero) {
  23. this.logIt('AfterContentChecked (no change)');
  24. } else {
  25. this.prevHero = this.contentChild.hero;
  26. this.logIt('AfterContentChecked');
  27. this.doSomething();
  28. }
  29. }
  30. // ...
  31. }

4、HttpClient 使用

1、导入 HttpClientModule 模块

  1. import { BrowserModule } from "@angular/platform-browser";
  2. import { NgModule } from "@angular/core";
  3. import { HttpClientModule } from "@angular/common/http";
  4. import { AppComponent } from "./app.component";
  5. @NgModule({
  6. declarations: [AppComponent],
  7. imports: [BrowserModule, HttpClientModule],
  8. providers: [],
  9. bootstrap: [AppComponent]
  10. })
  11. export class AppModule {}

2、发送get

  1. export class AppComponent implements OnInit {
  2. todos$: Observable<Todo[]>;
  3. constructor(private http: HttpClient) {}
  4. ngOnInit() {
  5. this.todos$ = this.http
  6. .get<Todo[]>(
  7. "https://jsonplaceholder.typicode.com/todos?_page=1&_limit=10"
  8. )
  9. .pipe(tap(console.log));
  10. }
  11. }
  12. // 设置 params 和headers 参数
  13. import { HttpClient, HttpParams } from "@angular/common/http";
  14. const params = new HttpParams().set("_page", "1").set("_limit", "10");
  15. const headers = new HttpHeaders().set("token", "iloveangular");
  16. ngOnInit() {
  17. this.todos$ = this.http
  18. .get<Todo[]>("https://jsonplaceholder.typicode.com/todos", { paramsheaders })
  19. .pipe(tap(console.log));
  20. }
  21. // 获取完整响应
  22. this.http.get("https://jsonplaceholder.typicode.com/todos/1", {
  23. observe: "response"
  24. })
  25. .subscribe(res => {
  26. console.dir("Response: " + res.status);
  27. });

3、拦截器

  1. // auth.interceptor.ts
  2. import { Injectable } from "@angular/core";
  3. import { HttpEvent, HttpRequest, HttpHandler, HttpInterceptor } from "@angular/common/http";
  4. import { Observable } from "rxjs";
  5. @Injectable()
  6. export class AuthInterceptor implements HttpInterceptor {
  7. intercept(
  8. req: HttpRequest<any>,
  9. next: HttpHandler
  10. ): Observable<HttpEvent<any>> {
  11. const clonedRequest = req.clone({
  12. headers: req.headers.set("X-CustomAuthHeader", "iloveangular")
  13. });
  14. console.log("new headers", clonedRequest.headers.keys());
  15. return next.handle(clonedRequest);
  16. }
  17. }
  18. import { AuthInterceptor } from "./interceptors/auth.interceptor";
  19. @NgModule({
  20. declarations: [AppComponent],
  21. imports: [BrowserModule, HttpClientModule],
  22. providers: [
  23. { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }
  24. ],
  25. bootstrap: [AppComponent]
  26. })
  27. export class AppModule {}

5、指令 directive

指令有三种:组件指令、属性指令、结构指令。组件本身就是一种指令,只不过组件指令拥有模板,而属性指令和结构指令都没有模板
1、可以使用 HostBinding 装饰器,实现元素的属性绑定

  1. import { Directive, HostBinding} from '@angular/core';
  2. @Directive({
  3. selector: '[greet]'
  4. })
  5. export class GreetDirective {
  6. @HostBinding() innerText = 'Hello, Everyone!';
  7. }
  8. import { Component } from '@angular/core';
  9. @Component({
  10. selector: 'app-root',
  11. template: `
  12. <h2>Hello, Angular</h2>
  13. <h2 greet>Hello, Angular</h2>
  14. `,
  15. })
  16. export class AppComponent { }

2、使用 Input 装饰器可以定义指令的输入属性

  1. import { Directive, HostBinding, Input } from '@angular/core';
  2. @Directive({
  3. selector: '[greet]'
  4. })
  5. export class GreetDirective {
  6. @Input() greet: string;
  7. @HostBinding() get innerText() {
  8. return this.greet;
  9. }
  10. }
  11. import { Component } from '@angular/core';
  12. @Component({
  13. selector: 'app-root',
  14. template: `
  15. <h2>Hello, Angular</h2>
  16. <h2 [greet]="'Hello, Semlinker!'">Hello, Angular</h2>
  17. `,
  18. })
  19. export class AppComponent { }

3、使用 HostListener 属性装饰器,实现元素的事件绑定

  1. import { Directive, HostBinding, HostListener, Input } from '@angular/core';
  2. @Directive({
  3. selector: '[greet]'
  4. })
  5. export class GreetDirective {
  6. @Input() greet: string;
  7. @HostBinding() get innerText() {
  8. return this.greet;
  9. }
  10. @HostListener('click',['$event'])
  11. onClick(event) {
  12. this.greet = 'Clicked!';
  13. }
  14. }
  15. import { Component } from '@angular/core';
  16. @Component({
  17. selector: 'app-root',
  18. template: `
  19. <h2>Hello, Angular</h2>
  20. <h2 [greet]="'Hello, Semlinker!'">Hello, Angular</h2>
  21. `,
  22. })
  23. export class AppComponent { }

4、通过 Attribute 装饰器来获取指令宿主元素的属性值

  1. import { Directive, HostBinding, HostListener, Input, Attribute } from '@angular/core';
  2. @Directive({
  3. selector: '[greet]'
  4. })
  5. export class GreetDirective {
  6. @Input() greet: string;
  7. @HostBinding() get innerText() {
  8. return this.greet;
  9. }
  10. @HostListener('click',['$event'])
  11. onClick(event) {
  12. this.greet = 'Clicked!';
  13. console.dir(event);
  14. }
  15. constructor(@Attribute('author') public author: string) {
  16. console.log(author);
  17. }
  18. }
  19. import { Component } from '@angular/core';
  20. @Component({
  21. selector: 'app-root',
  22. template: `
  23. <h2>Hello, Angular</h2>
  24. <h2 [greet]="'Hello, Semlinker!'"
  25. author="semlinker">Hello, Angular</h2>
  26. `,
  27. })
  28. export class AppComponent { }

5、通过 ViewChild 装饰器来获取视图中定义的模板元素,然后利用 ViewContainerRef 对象的 createEmbeddedView() 方法,创建内嵌视图

  1. import { Component, TemplateRef, ViewContainerRef, ViewChild,
  2. AfterViewInit } from '@angular/core';
  3. @Component({
  4. selector: 'app-root',
  5. template: `
  6. <ng-template #tpl>
  7. Hello, Semlinker!
  8. </ng-template>
  9. `,
  10. })
  11. export class AppComponent implements AfterViewInit{
  12. @ViewChild('tpl') tplRef: TemplateRef<any>;
  13. constructor(private vcRef: ViewContainerRef) {}
  14. ngAfterViewInit() {
  15. this.vcRef.createEmbeddedView(this.tplRef);
  16. }
  17. }

6、ElementRefTemplateRefViewContainerRef

6、路由

1、使用 base 指定路由根地址

  1. <!doctype html>
  2. <html>
  3. <head>
  4. <base href="/">
  5. <title>Application</title>
  6. </head>
  7. <body>
  8. <app-root></app-root>
  9. </body>
  10. </html>

2、在 AppModule 模块中,导入 RouterModule

  1. import { RouterModule } from '@angular/router';
  2. import { AppComponent } from './app.component';
  3. @NgModule({
  4. imports: [
  5. BrowserModule,
  6. RouterModule
  7. ],
  8. bootstrap: [
  9. AppComponent
  10. ],
  11. declarations: [
  12. AppComponent
  13. ]
  14. })
  15. export class AppModule {}

3、使用 RouterModule 对象为我们提供的静态方法:forRoot() 和 forChild() 来配置路由信息(根模块中使用 forRoot(),子模块中使用 forChild())

  1. export const ROUTES: Routes = [];
  2. @NgModule({
  3. imports: [
  4. BrowserModule,
  5. RouterModule.forRoot(ROUTES)
  6. ],
  7. // ...
  8. })
  9. export class AppModule {}

4、定义 routes
通过 path 属性定义路由的匹配路径,而 component 属性用于定义路由匹配时需要加载的组件

  1. export const ROUTES: Routes = [
  2. { path: '', component: HomeComponent }
  3. ];

5、使用 router-outlet 的指令告诉 Angular 在哪里加载组件

  1. import { Component } from '@angular/core';
  2. @Component({
  3. selector: 'app-root',
  4. template: `
  5. <div class="app">
  6. <h3>Our app</h3>
  7. <router-outlet></router-outlet>
  8. </div>
  9. `
  10. })
  11. export class AppComponent {}

6、动态路由

  1. export const ROUTES: Routes = [
  2. { path: '', component: HomeComponent },
  3. { path: '/profile/:username', component: ProfileComponent }
  4. ];

获取username需要先从 @angular/router 模块中导入 ActivatedRoute ,然后在组件类的构造函数中注入该对象,最后通过订阅该对象的 params 属性,来获取路由参数

  1. import { Component, OnInit } from '@angular/core';
  2. import { ActivatedRoute } from '@angular/router';
  3. @Component({
  4. selector: 'profile-page',
  5. template: `
  6. <div class="profile">
  7. <h3>{{ username }}</h3>
  8. </div>
  9. `
  10. })
  11. export class SettingsComponent implements OnInit {
  12. username: string;
  13. constructor(private route: ActivatedRoute) {}
  14. ngOnInit() {
  15. this.route.params.subscribe((params) => this.username = params.username);
  16. }
  17. }

7、子路由

  1. export const ROUTES: Routes = [
  2. {
  3. path: 'settings',
  4. component: SettingsComponent,
  5. children: [
  6. { path: 'profile', component: ProfileSettingsComponent },
  7. { path: 'password', component: PasswordSettingsComponent }
  8. ]
  9. }
  10. ];

8、按需加载

  1. export const ROUTES: Routes = [
  2. {
  3. path: 'settings',
  4. loadChildren: './settings/settings.module#SettingsModule'
  5. }
  6. ];
  7. @NgModule({
  8. imports: [
  9. BrowserModule,
  10. RouterModule.forRoot(ROUTES)
  11. ],
  12. // ...
  13. })
  14. export class AppModule {}

9、router-link

  1. // 传递路由信息
  2. <a [routerLink]="['/profile', username]">
  3. Go to {{ username }}'s profile.
  4. </a>
  5. // 当前激活路由类名
  6. <nav>
  7. <a routerLink="/settings" routerLinkActive="active">Home</a>
  8. <a routerLink="/settings/password" routerLinkActive="active">Change password</a>
  9. <a routerLink="/settings/profile" routerLinkActive="active">Profile Settings</a>
  10. </nav>

10、编程式导航

  1. constructor(private router: Router) {}
  2. handleSelect(event) {
  3. this.router.navigate(['/profile', event.name]);
  4. }

7、服务

  1. // 1、局部注入法
  2. export class UserService {}
  3. import { UserService } from './user.service';
  4. ...
  5. @NgModule({
  6. imports: [ BrowserModule],
  7. declarations: [ AppComponent],
  8. bootstrap: [ AppComponent],
  9. providers: [UserService]
  10. })
  11. // 2、全局注入法
  12. // 在 Angular 6 之后,我们也可以利用 @Injectable 的元数据来配置服务类
  13. import { Injectable } from '@angular/core';
  14. @Injectable({
  15. providedIn: 'root',
  16. })
  17. export class UserService { }

8、动画

  • 基本步骤

1、启用动画模块 BrowserAnimationsModule

  1. import { NgModule } from '@angular/core';
  2. import { BrowserModule } from '@angular/platform-browser';
  3. import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
  4. @NgModule({
  5. imports: [
  6. BrowserModule,
  7. BrowserAnimationsModule
  8. ],
  9. declarations: [ ],
  10. bootstrap: [ ]
  11. })
  12. export class AppModule { }

2、导入动画功能函数到组件文件

  1. import {
  2. trigger,
  3. state,
  4. style,
  5. animate,
  6. transition,
  7. // ...
  8. } from '@angular/animations';

3、添加动画的元数据属性

  1. @Component({
  2. selector: 'app-open-close',
  3. animations: [
  4. trigger('openClose', [
  5. // ...
  6. state('open', style({
  7. height: '200px',
  8. opacity: 1,
  9. backgroundColor: 'yellow'
  10. })),
  11. state('closed', style({
  12. height: '100px',
  13. opacity: 0.5,
  14. backgroundColor: 'green'
  15. })),
  16. transition('open => closed', [
  17. animate('1s')
  18. ]),
  19. transition('closed => open', [
  20. animate('0.5s')
  21. ]),
  22. ]),
  23. ],
  24. templateUrl: 'open-close.component.html',
  25. styleUrls: ['open-close.component.css']
  26. })
  27. export class OpenCloseComponent {
  28. isOpen = true;
  29. toggle() {
  30. this.isOpen = !this.isOpen;
  31. }
  32. }

4、触发动画

  1. <div [@openClose]="isOpen ? 'open' : 'closed'" class="open-close-container">
  2. <p>The box is now {{ isOpen ? 'Open' : 'Closed' }}!</p>
  3. </div>
  • 进阶

1、用 state 函数可以定义一个动画状态,但还有一些特殊的状态:* 代表任意状态;void 代表空状态。它们不需要用 state 函数来定义

  1. animations: [
  2. trigger('openClose', [
  3. // ...
  4. state('open', style({
  5. height: '200px',
  6. opacity: 1,
  7. backgroundColor: 'yellow'
  8. })),
  9. state('closed', style({
  10. height: '100px',
  11. opacity: 0.5,
  12. backgroundColor: 'green'
  13. })),
  14. transition('* => closed', [
  15. animate('1s')
  16. ]),
  17. transition('* => open', [
  18. animate('0.5s')
  19. ]),
  20. ]),
  21. ],

当元素离开视图时,就会触发 => void 转场,而不管它离开前处于什么状态。
当元素进入视图时,就会触发 void =>
转场,而不管它进入时处于什么状态。
通配符状态 * 会匹配任何状态 —— 包括 void。
注意:
a、 => void 等价于 :leave,void => 等价于 :enter
b、:increment 和 :decrement 泛指某个增加和减少的场景,如:
[@menuActivateSubmenu]=”count”,可以通过控制 count 值的++或—来触发对应的 :increment 或 :decrement 动画

2、转场 transition 会按照其定义的顺序进行匹配。因此,你可以在 => 转场的前面定义其它转场。比如,定义只针对 open => closed 的状态变更或动画,或 closed => open,而使用 => 作为匹配不上其它状态对时的后备。

3、样式通配符

  1. transition ('* => open', [
  2. animate ('1s',
  3. style ({ opacity: '*' }),
  4. ),
  5. ]),

这个 * 代表动画过程中 opacity 的值自动计算
4、:enter,:leave 结合 ngIf 和 ngFor
当任何 ngIfngFor 中的视图放进页面中时,会运行 :enter 转场;当移除这些视图时,就会运行 :leave 转场。

  1. <div @myInsertRemoveTrigger *ngIf="isShown" class="insert-remove-container">
  2. <p>The box is inserted</p>
  3. </div>
  4. trigger('myInsertRemoveTrigger', [
  5. transition(':enter', [
  6. style({ opacity: 0 }), // 初始状态,没有用 state 定义
  7. animate('100ms', style({ opacity: 1 })), // 终止状态
  8. ]),
  9. transition(':leave', [
  10. animate('100ms', style({ opacity: 0 }))
  11. ])
  12. ]),

5、动画回调

  1. <div [@openClose]="isOpen ? 'open' : 'closed'"
  2. (@openClose.start)="onAnimationEvent($event)"
  3. (@openClose.done)="onAnimationEvent($event)"
  4. class="open-close-container">
  5. </div>
  6. export class OpenCloseComponent {
  7. onAnimationEvent ( event: AnimationEvent ) {
  8. // openClose is trigger name in this example
  9. console.warn(`Animation Trigger: ${event.triggerName}`);
  10. // phaseName is start or done
  11. console.warn(`Phase: ${event.phaseName}`);
  12. // in our example, totalTime is 1000 or 1 second
  13. console.warn(`Total time: ${event.totalTime}`);
  14. // in our example, fromState is either open or closed
  15. console.warn(`From: ${event.fromState}`);
  16. // in our example, toState either open or closed
  17. console.warn(`To: ${event.toState}`);
  18. // the HTML element itself, the button in this case
  19. console.warn(`Element: ${event.element}`);
  20. }
  21. }

6、关键帧动画

  1. transition('* => active', [
  2. animate('2s', keyframes([
  3. style({ backgroundColor: 'blue', offset: 0}), // offset 标志动画过渡到哪儿了
  4. style({ backgroundColor: 'red', offset: 0.8}),
  5. style({ backgroundColor: 'orange', offset: 1.0})
  6. ])),
  7. ]),
  8. transition('* => inactive', [
  9. animate('2s', keyframes([
  10. style({ backgroundColor: 'orange', offset: 0}),
  11. style({ backgroundColor: 'red', offset: 0.2}),
  12. style({ backgroundColor: 'blue', offset: 1.0})
  13. ]))
  14. ]),
  • 进阶2

1、动画复用
使用 [animation()](https://angular.cn/api/animations/animation) 方法来在独立的 .ts 文件中定义动画,并把该动画的定义声明为一个导出的 const 变量。然后你就可以在应用的组件代码中通过 [useAnimation()](https://angular.cn/api/animations/useAnimation) 来导入并复用它了。

  1. // 定义
  2. import {
  3. animation, trigger, animateChild, group,
  4. transition, animate, style, query
  5. } from '@angular/animations';
  6. export const transAnimation = animation([
  7. style({
  8. height: '{{ height }}',
  9. opacity: '{{ opacity }}',
  10. backgroundColor: '{{ backgroundColor }}'
  11. }),
  12. animate('{{ time }}')
  13. ]);
  14. // 使用
  15. import { Component } from '@angular/core';
  16. import { useAnimation, transition, trigger, style, animate } from '@angular/animations';
  17. import { transAnimation } from './animations';
  18. @Component({
  19. trigger('openClose', [
  20. transition('open => closed', [
  21. useAnimation(transAnimation, {
  22. params: {
  23. height: 0,
  24. opacity: 1,
  25. backgroundColor: 'red',
  26. time: '1s'
  27. }
  28. })
  29. ])
  30. ])
  31. })

2、复杂序列
2.1、query & stagger

  1. animations: [
  2. trigger('pageAnimations', [
  3. transition(':enter', [
  4. query('.hero, form', [
  5. style({opacity: 0, transform: 'translateY(-100px)'}),
  6. stagger(-30, [
  7. animate('500ms cubic-bezier(0.35, 0, 0.25, 1)', style({ opacity: 1, transform: 'none' }))
  8. ])
  9. ])
  10. ])
  11. ]),
  12. ]
  13. })

a、用 query 查阅正在进入或离开页面的任意元素。该查询会找出那些符合某种匹配 CSS 选择器的元素。

b、 对每个元素,使用 style 为其设置初始样式。使其不可见,并使用 transform 将其移出位置,以便它能滑入后就位。

c、使用 stagger 来在每个动画之间延迟 30 毫秒。

d、对屏幕上的每个元素,根据一条自定义缓动曲线播放 0.5 秒的动画,同时将其淡入,而且逐步取消以前的位移效果。

2.2、group
对同一个元素的不同属性做不同的动画,动画是并行执行的

  1. animations: [
  2. trigger('flyInOut', [
  3. state('in', style({
  4. width: 120,
  5. transform: 'translateX(0)', opacity: 1
  6. })),
  7. transition('void => *', [
  8. style({ width: 10, transform: 'translateX(50px)', opacity: 0 }),
  9. group([
  10. animate('0.3s 0.1s ease', style({
  11. transform: 'translateX(0)',
  12. width: 120
  13. })),
  14. animate('0.3s ease', style({
  15. opacity: 1
  16. }))
  17. ])
  18. ]),
  19. transition('* => void', [
  20. group([
  21. animate('0.3s ease', style({
  22. transform: 'translateX(50px)',
  23. width: 10
  24. })),
  25. animate('0.3s 0.2s ease', style({
  26. opacity: 0
  27. }))
  28. ])
  29. ])
  30. ])
  31. ]

2.3、过滤动画

  1. @Component({
  2. animations: [
  3. trigger('filterAnimation', [
  4. transition(':enter, * => 0, * => -1', []), // 忽略进入动画
  5. transition(':increment', [
  6. query(':enter', [ // 查询进入的元素并应用动画
  7. style({ opacity: 0, width: '0px' }),
  8. stagger(50, [
  9. animate('300ms ease-out', style({ opacity: 1, width: '*' })),
  10. ]),
  11. ], { optional: true })
  12. ]),
  13. transition(':decrement', [
  14. query(':leave', [
  15. stagger(50, [
  16. animate('300ms ease-out', style({ opacity: 0, width: '0px' })),
  17. ]),
  18. ])
  19. ]),
  20. ]),
  21. ]
  22. })
  23. export class HeroListPageComponent implements OnInit {
  24. heroTotal = -1;
  25. }

3、路由转场动画

  1. // 1、定义路由,注意 data 参数定义了动画的一个状态,后面会用上
  2. { path: 'home', component: HomeComponent, data: {animation: 'HomePage'} },
  3. { path: 'about', component: AboutComponent, data: {animation: 'AboutPage'} },
  4. // 2、定义路由出口
  5. <div [@routeAnimations]="prepareRoute(outlet)" >
  6. <router-outlet #outlet="outlet"></router-outlet>
  7. </div>
  8. prepareRoute(outlet: RouterOutlet) {
  9. return outlet && outlet.activatedRouteData && outlet.activatedRouteData['animation']; // 这里取出路由定义中data值
  10. }
  11. // 3、定义动画
  12. export const slideInAnimation =
  13. trigger('routeAnimations', [
  14. transition('HomePage <=> AboutPage', [
  15. style({ position: 'relative' }), // 宿主元素初始样式
  16. query(':enter, :leave', [ // 子元素动画过程中的样式
  17. style({
  18. position: 'absolute',
  19. top: 0,
  20. left: 0,
  21. width: '100%'
  22. })
  23. ]),
  24. query(':enter', [ // 刚进入时隐藏进入元素
  25. style({ left: '-100%'})
  26. ]),
  27. query(':leave', animateChild()), // 刚离开时允许允许子元素动画
  28. group([ // 一个元素进入同时另一个元素退出
  29. query(':leave', [
  30. animate('300ms ease-out', style({ left: '100%'}))
  31. ]),
  32. query(':enter', [
  33. animate('300ms ease-out', style({ left: '0%'}))
  34. ])
  35. ]),
  36. query(':enter', animateChild()), // 元素进入后,开始执行子动画
  37. ]),
  38. ]);
  39. // 4、应用动画
  40. @Component({
  41. selector: 'app-root',
  42. templateUrl: 'app.component.html',
  43. styleUrls: ['app.component.css'],
  44. animations: [
  45. slideInAnimation
  46. ]
  47. })

9、主要模块

Angular - 语法基础 - 图2

  • 注意事项

1、使用 ngModel 时必须先引入 FormsModule( ngModel 是一个有效的 Angular 指令,不过它在默认情况下是不可用的)

2、Angular 的自定义元素是如何被浏览器识别的?
Angular 编译时,会用到一个浏览器原生支持的自定义元素API:customElements.define

  1. customElements.define('element-details',
  2. class extends HTMLElement {
  3. constructor() {
  4. super();
  5. const template = document
  6. .getElementById('element-details-template')
  7. .content;
  8. const shadowRoot = this.attachShadow({mode: 'open'})
  9. .appendChild(template.cloneNode(true));
  10. }
  11. });

3、生命周期
Angular - 语法基础 - 图3
Angular - 语法基础 - 图4
Angular - 语法基础 - 图5

  • ngOnChanges: 当组件数据绑定的输入属性发生变化时触发, 该方法接受一个SimpleChanges对象,包括当前值和上一属性值.首次调用一定发生在ngOnInit前,注意的是该方法仅限于对象的引用发生变化时,也就是说,如果对象的某个属性发生变化,Angular是不会触发onChanges的.
  • ngOninit:初始化指令或组件, 在angular第一次展示组件的绑定属性后调用,该方法只调用一次.
  • ngDocheck: 检测,并在发生Angular无法或不愿意自己检测的变化时作出反应。
    在每个Angular变更检测周期中调用,ngOnChanges()和ngOnInit()之后。
  • ngAfterContentInit: 当把内容投影进组件之后调用。
    第一次ngDoCheck()之后调用,只调用一次. 只适用于组件。
  • ngAfterContentChecked: 每次完成被投影组件内容的变更检测之后调用。
  • ngAfterContentInit()和每次ngDoCheck()之后调用,只适合组件。
  • ngAfterViewInit: 在angular初始化组件及其子组件的视图之后调用, 只调用一次,第一次ngAfterContentChecked()之后调用.只适合组件。
  • ngAfterViewChecked: 每次做完组件视图和子视图的变更检测之后调用。
    ngAfterViewInit()和每次ngAfterContentChecked()之后调用。只适合组件。
  • ngOnDestory: 在angular每次销毁组件或指令之前调用, 通常用于移除事件监听,退订可观察对象等.

4、变更检测

  • 单组件模式
    Angular - 语法基础 - 图6
  • default 自动刷新UI的机制会被 runOutsideAngualr 方法所影响
  • 父级 default or onPush + 子级 onPush 模式
    子级 detectChange 不能更新父级UI
    子级 markForCheck 可以更新父级UI
    父级onPush,子级default时,子级也不会更新
  • 应该全部用 onPush + detectChange 策略,各组件只关心自己的UI更新情况,不必关心父级和子级。若子级的变化想引起父级的变化,不应该用 markForCheck。而是类似发起一个 action,更新store,父级监听 store 的变化,然后自己 detectChange
  • click这种用户触发的事件是一个特例,会引起全局的 UI 刷新
  • 子组件中 HostBinding 这种绑定的属性不会随UI一起刷新,除非显示调用 markForCheck
    5、JS和CSS共享变量的一种方式:
  1. // ts
  2. @HostBinding('attr.style')
  3. get valueAsStyle(): any {
  4. return this.sanitizer.bypassSecurityTrustStyle(`--some-var: ${this.test}`);
  5. }
  6. test = '100%';
  7. constructor(
  8. private readonly sanitizer: DomSanitizer,
  9. ) {
  10. }
  11. // scss
  12. :host {
  13. width: calc(var(--some-var));
  14. height: 100%;
  15. position: fixed;
  16. z-index: 2000;
  17. }

10、最佳实践

  • 组件更新策略

onPush + detectChanges

  • 最好不要在宿主元素上绑定属性,如 hidden,这会使 detectChanges 失效,因为宿主模板本质上可以理解为父级的DOM,需用 markForCheck 才能得到更新,但 markForCheck 代价较大

  • rxjs 订阅与取消

尽量使用 async pipe 自动管理退订,而不是 destroy$ or subscribtion

  • ngrx 用 typeof 定义默认值类型,减少模板代码 ```javascript // good export const initialState = { showRank: false, rankItems: [], scoreCommited: true, }; export type State = typeof initialState

// bad export interface State { showRank: boolean; rankItems: FullAttendanceRankItem[]; scoreCommited: boolean; } export const initialState: State = { showRank: false, rankItems: [], scoreCommited: true, }; ```

  • 逻辑容器用 ng-container 而不是 div,可以减少dom嵌套层级

参考