Angular的两表单|三管道|三绑定|三指令|五通信|五守卫|八周期

784 阅读7分钟

1 Angular的两大表单

1.1 模板驱动表单

🍑 模板驱动表单:引入FormsModule模块,表单的控制逻辑都是写在模板里的,每一个表单元素都是和一个负责管理内部表单模型的指令关联起来的。

import { Component, OnInit } from '@angular/core';
import { NgForm } from '@angular/forms';

@Component({
  selector: 'app-module-form',
  template:`
  <form #moduleForm="ngForm" (submit)="OnSubmit(moduleForm)">
    <label>用户名:
      <input type="text" name="username" ngModel required minlength="2" maxlength="6" #username="ngModel"><br>
      <ng-container *ngIf="username.touched&&username.errors">
        <div *ngIf="username.errors.required">请输入用户名</div>
        <div *ngIf="username.errors.minlength">最少2个字符</div>
        <div *ngIf="username.errors.maxlength">最多6个字符</div>
      </ng-container>
    </label>

    <label>
      密&nbsp;&nbsp;码:
      <input style="margin-top: 10px;" type="text" name="password" ngModel required minlength="6" pattern="[a-zA-Z0-9]+" #password="ngModel"><br>
      <ng-container *ngIf="password.touched&&password.errors">
        <div *ngIf="password.errors.required">请输入密码</div>
        <div *ngIf="password.errors.minlength">最少6个字符</div>
        <div *ngIf="password.errors.pattern">至少包含大小写、数字</div>
      </ng-container>
    </label>

    <button style="margin-top: 20px;" type="submit" [disabled]="!moduleForm.valid">提交</button>
  </form>
  `,
  styleUrls: ['./module-form.component.less']
})
export class ModuleFormComponent implements OnInit {
  constructor() { }
  ngOnInit() {
  }
  OnSubmit(moduleForm:NgForm){
    console.log(moduleForm.value);
  }
}

1.2 响应式表单

🍑 响应式表单:需要引入ReactiveFormsModule模块,在响应式表单中,视图中的每个表单元素都直接链接到一个表单模型。

  • FormControl:是构成表单的基本单位。实例用于追踪单个表单控件的值和验证状态
  • FormGroup:用于追踪一个表单控件组的值和状态。
  • FormGroup和FormArray的区别:formgroup既可以代表表单一部分也可以代表整个表单;formarray有一个额外的长度属性,它的formControl是没有相关的key的,只能通过访问formarray中的元素。
<input [formControl]="username">
<!--不带表单的input-->
//原始的定义方法
export class ReactiveRegistComponent implements OnInit {
  formModel:FormGroup;
  constructor() {
    this.formModel=new FormGroup({
      username:new FormControl(),
      mobile:new FormControl(),
      passwordsGroup: new FormGroup({
        password:new FormControl(),
        pconfirm:new FormControl(),
      })
    });
  }
}

//使用formBuilder后的定义
constructor(fb:FormBuilder) {
  this.formModel=fb.group({
    username:[''],
    mobile:[''],
    passwordsGroup: fb.group({
      password:[''],
      pconfirm:[''],
    })
  });
}
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, NgForm, Validators } from '@angular/forms';

@Component({
  selector: 'app-module-form',
  // templateUrl: './module-form.component.html',
  template:`
    <form [formGroup]="form">
    <label>用户名:
      <input type="text"  formControlName="username"><br>
      <ng-container *ngIf="form.controls['username'].errors?.['required']"><div style="color: red;">请输入用户名</div></ng-container>
      <ng-container *ngIf="form.controls['username'].errors?.['maxlength']"><div style="color: red;">最多只能有6个字符</div></ng-container>
    </label>

    <label>
      密&nbsp;&nbsp;码:
      <input style="margin-top: 10px;"  formControlName="password"><br>
      <ng-container *ngIf="form.controls['password'].errors?.['required']"><div style="color: red;">请输入密码</div></ng-container>
    </label>
  </form>
  `,
  styleUrls: ['./module-form.component.less']
})
export class ModuleFormComponent implements OnInit {
  form!: FormGroup;
  constructor(private fb: FormBuilder) { }

  ngOnInit() {
    this.form = this.fb.group({
      username:[null,[Validators.required,Validators.maxLength(6)]],
      password:[null,[Validators.required]],
    })
  }
}
  • 自定义验证器:
export function whiteSpaceValidator(): ValidatorFn {
  // 不能全输入空格,验证
  return (control: FormControl): { [s: string]: boolean } => {
    const reg = new RegExp(/\s/g);
    if (reg.test(control.value)) {
      return { required: true };
    }
  };
}

2 Angular管道

  • 管道:把数据作为输入,然后转换它并给出输出。

2.1 内置管道

🍑 Angular的一些内置管道:如date、uppercase、lowercase、currency、percent等,不用引入,直接在任何组件中使用。注意:用date转化的数据不能为字符串,必须为Date类数据

<!-- curDate = new Date(); -->
<!-- 当前时间日期为Apr 20, 2022 -->
<p>当前时间日期为{{curDate|date}}</p>

<!-- 管道参数化:当前时间日期为2022/04/20 -->
<p>当前时间日期为{{curDate|date:"yyyy/MM/dd"}}</p>

<!-- title = "Hello World!"; -->
<!-- 转大写HELLO WORLD! -->
<p>转大写{{title|uppercase}}</p>
<!-- 转小写hello world! -->
<p>转小写{{title|lowercase}}</p>

<!-- 转换金额字符串$0.26 -->
<p>转换金额字符串{{0.259|currency}}</p>

<!-- 转换金额百分比26% -->
<p>转换金额百分比{{0.259|percent}}</p>

2.2 链式管道

🍑 链式管道:管道以多个条件指定格式输出。

<!-- 当前时间日期为wednesday, april 20, 2022 -->
<p>当前时间日期为{{curDate|date:'fullDate'|lowercase}}</p>

2.3 自定义管道

🍑 自定义管道:在app.module文件的declarations中进行声明就可以在任何一个组件中使用了。

<!-- 600000毫秒真实时间:00天00时10分00秒 -->
<p>600000毫秒真实时间:{{600000|TimeFormater}}</p>
import { PipeTransform, Pipe } from '@angular/core';

@Pipe({
  name: 'TimeFormater',
})
export class TimeFormaterPipe implements PipeTransform {
// // 传入的是一个一毫秒为单位的时间数
  transform(value) {
    if (!value || value <= 0) {
      return '00天00时00分00秒';
    }
    const time = Math.abs(value);
    const transecond = Math.round(time / 1000); // 转化为秒
    const day = Math.floor(transecond / 3600 / 24); // 获取天数
    const hour = Math.floor((transecond / 3600) % 24); // 获取小时,取余代表不满一天那部分
    const minute = Math.floor((transecond / 60) % 60); // 获取分,取余代表不满小时那部分
    const second = transecond % 60;
    return `${this.formatTime(day)}${this.formatTime(hour)}${this.formatTime(minute)}${this.formatTime(second)}秒`;
  }

  formatTime(t) {
    return t < 10 ? '0' + t : '' + t;
  }
}

3 Angular的三大绑定

3.1属性绑定

🍑 属性绑定的属性指的是元素、组件、指令的属性。属性的绑定是单向绑定,从组件的属性流动到目标元素的属性。

<!--属性绑定图片路径,动态获取-->
<img [src]="imgUrl">

3.2 attribute、class和style绑定

🍑attribute绑定:并非所有属性都有可供属性绑定。是HTML标签上的特性,它的值只能够是字符串。通过attr.特性名绑定。而比如标签中的id、src等这些属于Property(属性,DOM中的属性,是JavaScript里的对象),这些可以直接绑定就可。而attribute绑定如下:

<table [border]="1">
  <tr><td id="attr" [attr.colspan]="1+1">One-Two</td></tr>
  <tr><td>Five</td><td>Six</td></tr>
</table>

🍑class的绑定:静态绑定、单一属性动态绑定方式、多属性动态绑定方式、ngCLass指令绑定。

  .test1{
    width: 100px;
    height: 100px;
    border: 1px solid;
  }

  .test2{
    width: 100px;
    height: 100px;
    background-color: yellowgreen;
  }

(1)静态绑定:可以是一个,也可以是多个,多个的class就会融合起来。

<div class="test1"></div>
<div class="test1 test2"></div>
<!--多个样式会重叠-->
<div [class]="'test1'"></div>
<div [class]="'test1 test2'"></div>

(2)单一属性动态绑定方式:取在css文件中定义好的样式进行绑定,class.样式的class名

<div [class.test1]="true"></div>
<div [class.test2]="true"></div>

(3)多属性动态绑定方式:class的绑定

<div [class]="moreClass"></div>
<div [ngClass]="moreClass"></div>
moreClass:object = {
    'test1':true,
    'test2':true
}

🍑style的绑定:单一样式绑定、带单位的单一样式绑定、多个样式绑定。 (1)单一样式的绑定

<div [style.width]="'200px'"></div>

(2)带单位的单一样式绑定

<div [style.width.px]="'200'"></div>

(3)多个样式绑定

`<div [style]="moreStyle">绑定多个形式的style</p>`
moreStyle:string =  'width: 100px;height: 200px';

3.3 事件绑定

🍑事件绑定:带()的特性就是事件绑定。括号内代表的是目标事件。而下面例子的事件绑定就是点击事件的绑定。

<button (click)="save()"></button>

(1)目标事件是原生DOM元素事件,input是DOM元素input的原生事件。

//html
<input [value]="currentUser.name" (input)="getValue($event)">

//js
currentUser={
    'name':''
}
getValue(ev:any){
    this.currentUser.name = ev.target.value;
    console.log(this.currentUser.name);
}

(2)目标事件是指令:比如ngClickngDblclick等。

(3)目标事件是自定义事件。目标事件 (子组件的EventEmitter实例变量)="父组件的方法(子组件数据)" 下文的父子组件通信已经有详解👇。

4 Angular的三大指令

4.1 属性型指令

🍑 属性型指令:该指令可改变元素、组件或其他指令的外观和行为。比如:

  • ngClass指令:可以通过动态地添加或移除css类来控制css来如何显示。
  • ngStyle指令:可以同时设置多个内联样式。
  • ngModel指令:可以双向绑定到HTML表单中的元素。
  • 创建属性型指令:在app.module文件中的declarations数组中进行声明就可以在任何组件的标签元素中调用,如下:
<p appHighlight>看一下指令</p>
// Directive:在declarations
import { Directive, ElementRef } from '@angular/core';

@Directive({
  selector: '[appHighlight]'
})
export class HighlightDirective {

  constructor(el:ElementRef) {
    el.nativeElement.style.backgroundColor = 'yellow';
  }
}
import { Directive, ElementRef, HostListener } from '@angular/core';

@Directive({
  selector: '[appHighlight]'
})
export class HighlightDirective {
  constructor(private el:ElementRef) {
  }
  
  // 监听鼠标悬浮背景颜色设置
  @HostListener('mouseenter') onMouseEnter(){
    this.hightlight('yellow');
  }
  // 监听鼠标离开背景颜色设置
  @HostListener('mouseleave') onMouseLeave(){
    this.hightlight('');
  }

  private hightlight(color:string){
    this.el.nativeElement.style.backgroundColor = color;
  }
}

4.2 结构型指令

🍑 结构型指令:该指令通过添加或移除DOM元素来改变DOM布局。每一个宿主元素都只能有一个结构型指令。比如ngif和ngfor不能在同一元素出现。如ngif、ngfor、ngSwitch(本身是属性型指令,它控制了两个结构型指令ngSwitchCase和ngSwitchDefault),结构型指令一般都是带***符号的**。

  • 自定义结构型指令:
import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';

@Directive({
  selector: '[appTagdisplay]'
})
export class TagdisplayDirective {
  private hasView = false;
  // ViewContainerRef访问视图容器
  constructor(private templateRef:TemplateRef<any>,private viewContainer:ViewContainerRef) { }

  @Input() set appTagdisplay(condition:boolean){
    if(!condition&&!this.hasView){
      // 视图容器创建一个内嵌的视图
      this.viewContainer.createEmbeddedView(this.templateRef);
      this.hasView = true;
    }else if(condition&&this.hasView){
      // 清除该容器,销毁该视图
      this.viewContainer.clear();
      this.hasView = false;
    }
  }
}
<div>
  <h1>自定义结构指令</h1>
  <p *appTagdisplay="false">
    该段显示,因为appTagdisplay为false
  </p>
  <p *appTagdisplay="true">
    该段不显示,因为appTagdisplay为true
  </p>
</div>

4.3 组件

🍑 组件:也是一种指令,该指令拥有模板。

5 Angular的五大组件通信

5.1 Input与Output实现父子组件通信

通过下面的例子我们会发现Input和Output的操作都在子组件中。父传子:在父组件中动态绑定属性,在子组件中Input获取父组件传来的属性。子传父:子组件创建一个实例化EventEmitter对象,EventEmitter 的核心就是事件触发与事件监听器功能的封装;父组件:通过事件绑定调用带参自定义函数接受子组件传来的数据(自定义函数的参数)。

🍑 父组件:双向绑定fatherData也就是当前输入框输入的信息,点击发送事件触发传给子组件的currentData添加数据并清空当前输入框的信息。

<div class="chan">
  <div class="father">
    <ng-container *ngIf="!isClear">
      <div *ngFor="let item of currentData">
      <span>{{item}}</span>
    </div>
    </ng-container>
    <div class="footer">
      <input style="width:178px" [(ngModel)]="fatherData"/>
      <button (click)="send()">发送</button>
      <button (click)="isClear=true">清空记录</button>
    </div>
  </div>
  <app-son [dataSource]="currentData" (sonSend)="getSonInfo($event)"></app-son>
</div>
import { Component, OnInit } from '@angular/core';

export class FatherComponent implements OnInit {
  isClear:boolean=false;
  fatherData:any;
  currentData:Array<any>=[];
  constructor() { }

  ngOnInit() {
    this.fatherData = this.currentData;
  }

  send(){
    this.currentData.push("我是父组件的数据:"+this.fatherData);
    this.fatherData='';
  }

  getSonInfo(event:any){
    this.currentData.push("我是子组件的数据:"+event);
  }
}

🍑 子组件:输入框输入sonData,点击发送事件触发子组件事件发射数据,然后父组件就可以通过子组件绑定的事件发射从父组件通过事件方法获取当前子组件发送的数据。

<div class="son">
  <ng-container *ngIf="!isClear">
    <div *ngFor="let item of currentData">
    <span>{{item}}</span>
    </div>
  </ng-container>
  <div class="footer">
    <input  style="width:178px" [(ngModel)]="sonData"/>
    <button (click)="send()">发送</button>
    <button (click)="isClear=true">清空记录</button>
  </div>
</div>
import {  OnInit } from '@angular/core';

export class SonComponent implements OnInit{
  @Input() dataSource:any=0;
  @Output() sonSend = new EventEmitter<any>();
  isClear:boolean=false;
  sonData:any;
  currentData:Array<any>=[];
  constructor() { }

  ngOnInit(): void {
    this.currentData = this.dataSource;
  }

  send(){
    this.sonSend.emit(this.sonData);
    // this.currentData.push("我是子组件的数据:"+this.sonData);
    this.sonData='';
  }
}

image.png

5.2 通过本地变量实现父子组件的通信

在父组件的模板中创建一个代表子组件的本地变量,通过调用这个变量就可以调用子组件中的属性和方法。

<div>我是父组件</div>
<div>子组件:{{sonTpl.myName}}</div>
<app-son #sonTpl></app-son>

5.3 通过@ViewChild实现父子组件的通信

父组件的js中定义@ViewChild(SonComponent) childTpl: any;,注意在html必须要调用子组件元素,不然会直接报错,且不能直接调用childTpl.myName获取子组件中的变量。

<div>我是父组件</div>
<button (click)="childTpl.getName()">子组件按钮</button>
<app-son></app-son>
// 子组件中的getName
 getName(){
    console.log('我是子组件的getName方法');
 }

5.4 通过共享服务Service实现非父子组件通信

栗子来自书籍《Angular企业级应用开发实战》-p143:

image.png 🍑 Service服务:主要控制准备开始、和确认按钮的动作进行消息的传递。注意这个服务的定义一定是共享的,不要在各个组件下独自注入providers中,因为单独引入service只是在当前组件有效,每个组件调用一次都是独立的,互不影响,这就不是组件通信需要的。

import { Injectable } from '@angular/core';
import { Subject } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class MissionService {
  // 源
  private missionAnnounceSource = new Subject<string>();
  private misssionConfirmedSource = new Subject<string>();

  // 流
  // asObservable:可观察的序列,隐藏源序列的身份。
  missionAnnounce$ = this.missionAnnounceSource.asObservable();
  missionConfirm$ = this.misssionConfirmedSource.asObservable();
  constructor() {}

  announceMission(mission:string){
    this.missionAnnounceSource.next(mission);
  }

  confirmMission(astronaut:string){
    this.misssionConfirmedSource.next(astronaut);
  }
}

🍑 MissioncontrolComponent:这是一个主要界面的组件,在界面中调用了astronaut组件。当前组件就是父组件,而astronaut组件就是一个子组件。

import { Component, OnInit } from '@angular/core';
import { MissionService } from 'src/app/service/mission.service';

@Component({
  selector: 'app-missioncontrol',
  template: `<h2>导弹控制器</h2>
  <button (click)="announce()">准备开始</button>
  <app-astronaut *ngFor="let item of astronauts" [astronaut]="item">
  </app-astronaut>
  <h3>日志</h3>
  <ul>
    <li *ngFor="let event of history">{{event}}</li>
  </ul>
  `,
})
export class MissioncontrolComponent implements OnInit {
  astronauts = ['操作员1','操作员2','操作员3'];
  history:string[] = [];
  missions = ['发射导弹'];
  nextMession = 0;
  constructor(private misssionSvc:MissionService) {
     // 获取子组件保存的信息,获取是哪一个操作员点击确认了
     misssionSvc.missionConfirm$.subscribe((astronaut)=>{
       this.history.push(`${astronaut}已经确认`);
     })
  }

  announce(){
    let mission = this.missions[this.nextMession++];
    this.misssionSvc.announceMission(mission);
    this.history.push(`任务"${mission}"进入准备`);
    if(this.nextMession>=this.missions.length){
      this.nextMession = 0;
    }
  }
  ngOnInit(): void {
  }
}

🍑 AstronautComponent:点击确认,向父组件传递确认的操作员信息。

import { Component, Input, OnInit, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs';
import { MissionService } from 'src/app/service/mission.service';

@Component({
  selector: 'app-astronaut',
  template: `
  <p>
  {{astronaut}}:<strong>{{mission}}</strong><button (click)="confirm()" [disabled]="!announced||confirmed">确认</button>
  </p>
  `,
})
export class AstronautComponent implements OnInit,OnDestroy{
  @Input() astronaut:string='';
  mission = '<没有任务>';
  confirmed = false;
  announced = false;
  subscription:Subscription;
  constructor(private missionSvc:MissionService) {
    // 获取父组件的数据
    this.subscription = missionSvc.missionAnnounce$.subscribe((mission)=>{
      this.mission = mission;// 发射导弹
      this.announced = true;// 激活确认按钮
      this.confirmed = false;
    })
  }
  confirm(){
    // 禁用按钮
    this.confirmed = true;
    // 点击确认,保存当前的操作员数据
    this.missionSvc.confirmMission(this.astronaut);
  }
  ngOnDestroy(): void {
    // 防止内存泄漏
    this.subscription.unsubscribe();
  }
  ngOnInit() {
  }
}

5.5 路由传值

🍑 按钮点击跳转:路由传参数由分号隔开。

import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';

@Component({
  selector: 'app-father',
  template:`<button (click)="linkSon()">按钮跳到儿子组件中</button>`
})
export class FatherComponent implements OnInit {
  obj:any=[
    '书悟空',
    '唐僧',
  ];
  constructor(private router:Router) {}

  ngOnInit() {}

  linkSon(){
    this.router.navigate(['/son',{ name:this.obj,id:10010}]);
    //http://localhost:4209/son;name=书悟空,唐僧;id=10010
  }
}

// private route:ActivatedRoute
this.route.params.subscribe((route)=>{
       console.log(route);
})

🍑 链接点击跳转:路由传参通过queryParams属性控制,由?、&符号分隔开。

<a [routerLink]="['/son']" [queryParams]="{id:10010,name:this.obj}">链接跳到儿子组件中</a>
// http://localhost:4209/son?id=10010&name=书悟空&name=唐僧
// private route:ActivatedRoute
this.route.queryParams.subscribe((route)=>{
       console.log(route);
})

🍑 链接点击跳转:直接是用/分割路由传参。

{ path: 'son/:id', component: SonComponent },
<a [routerLink]="['/son',10010]">链接跳到儿子组件中</a>
// http://localhost:4209/son/10010
// private route:ActivatedRoute
this.route.params.subscribe((route)=>{
       console.log(route);
})

还有其他通信方式:浏览器本地传值(localStorge、SessionStorge)、cookie

6 Angular的五大路由守卫

守卫接口守卫作用应用场景
CanActivate控制是否导航到某路由该用户可能无权导航到目标组件
CanActivateChild控制是否导航到某子路由
CanDeactivate控制从当前路由离开的情况在离开组件前,你可能要先保存修改
Resolve在路由激活之前获取路由数据在现实目标组件前,你可能得先获取某些数据
CanLoad来处理异步导航到某特性模块,保护对特性模块的未授权加载

🍑 CanActivate:控制是否导航到某个路由的一个属性。

🍑 CanActivateChild:有一些业务是只是控制当前路由下的一些子路由,如果用CanActivate来控制路由的就要每个子路由都要加CanActivate,而用CanActivateChild的话只是在当前路由下添加就行。

const routes: Routes = [
  { path: '', redirectTo: 'login', pathMatch: 'full' },
  { path: 'login', component: LoginComponent },
  {
    path: 'admin',
    component: AdminComponent,
    canActivate: [AuthGuard],
    children: [
      {
        path: '',
        canActivateChild: [AuthGuard],
        children: [
          { path: 'daughter', 
            component: AdminDaughterComponent 
          },
          {
            path: 'son',
            component: AdminSonComponent,
          },
        ],
      },
    ],
  },
];

等价于:

const routes: Routes = [
  { path: '', redirectTo: 'login', pathMatch: 'full' },
  { path: 'login', component: LoginComponent },
  {
    path: 'admin',
    component: AdminComponent,
    canActivate: [AuthGuard],
    children: [
      {
        path: '',
        children: [
          { path: 'daughter',
            canActivate: [AuthGuard], 
            component: AdminDaughterComponent 
          },
          {
            path: 'son',
            canActivate: [AuthGuard],
            component: AdminSonComponent,
          },
        ],
      },
    ],
  },
];

其他的核心代码如下,ActivatedRouteSnapshot包含url、params、queryParams等参数;RouterStateSnapshot主要包含一个url属性和toString()函数。

export class AuthGuard implements CanActivate {
  constructor(private authService: AuthService, private router: Router) {}

  canActivate(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): true | UrlTree {
    const url: string = state.url;

    return this.checkLogin(url);
  }

  canActivateChild(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): true | UrlTree {
    // 从这里可以看出调用canActivate就简化了每个子路由都要加
    return this.canActivate(route, state);
  }

  checkLogin(url: string): true | UrlTree {
    if (this.authService.isLoggedIn) {
      return true;
    }

    // 存下当前路由
    this.authService.redirectUrl = url;

    // 如果未登录定向到login
    return this.router.parseUrl('/login');
  }
export class AuthService {
  // 是否登录
  isLoggedIn = false;

  // store the URL so we can redirect after logging in
  redirectUrl: string | null = null;
  
  // 登录
  login(): Observable<boolean> {
    return of(true).pipe(
      // 延迟加载
      delay(1000),
      // Observable对象源的副作用,只要调用这个函数,就会触发
      tap(() => (this.isLoggedIn = true))
    );
  }
  
  // 退出登录
  logout(): void {
    this.isLoggedIn = false;
  }
}

🍑 CanDeactivate:当我们在一个编辑表单的页面的时候,我们不小心点击其他导航了,这是就会在离开当前导航前会提示用户一些信息。比如编辑页面是通过监听当前编辑页面是否有变动输入属性或绑定属性是否变化,当路由变化判断监听事件是否触发来是否设置提示信息。

export class LeaveGuard implements CanDeactivate<AdminSonComponent> {
  canDeactivate(
    component: AdminSonComponent,
    currentRoute: ActivatedRouteSnapshot,
    currentState: RouterStateSnapshot
  ): Observable<boolean> | Promise<boolean> | boolean {
    // return false;
    return window.confirm('还没保存,确定离开!');
  }
}
{
    path: 'admin',
    component: AdminComponent,
    canActivate: [AuthGuard],
    children: [
      {
        path: '',
        canActivateChild: [AuthGuard],
        children: [
          { path: 'daughter', component: AdminDaughterComponent },
          {
            path: 'son',
            component: AdminSonComponent,
            canDeactivate: [LeaveGuard],
          },
        ],
      },
    ],
  },

🍑 Resolve:使用解析器预先从服务器上获取数据。这样在路由激活的那一刻数据就准备好了,还要在路由到此组件之前处理好错误。也就希望只有当所有必要数据都已经拿到之后,才渲染这个路由组件。

// user.ts,创建一个User对象
export class User {
  constructor(public id: number, public login: string) {}
}

创建resolve守卫文件,对预先获取数据的请求做相关的操作。

export class ResolveGuard implements Resolve<User> {
  constructor(private router: Router) {}
  resolve(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): Observable<any> | Promise<any> | User {
    // route.paramMap.get('id')可以这样获取路由id
    return ajax.getJSON('https://api.github.com/users?per_page=5').pipe(
      mergeMap((user) => {
        if (user) {
          return of(user);
        } else {
          this.router.navigateByUrl('/');
          return EMPTY;
        }
      })
    );
  }
}

在路由中定义守卫,在有需要的组件路由加载前获取到当前组件需要的数据。

{
    path: 'admin',
    component: AdminComponent,
    canActivate: [AuthGuard],
    children: [
      {
        path: '',
        canActivateChild: [AuthGuard],
        children: [
          { path: 'daughter', component: AdminDaughterComponent },
          {
            path: 'son',
            component: AdminSonComponent,
            resolve: {
              user: ResolveGuard,
            },
          },
        ],
},

在当前组件获取组件路由加载前获取到的数据。

constructor(private route: ActivatedRoute) {}

  ngOnInit() {
    this.route.data.subscribe((item) => {
      console.log(item);// user数据
    });
  }

🍑 CanLoad:比如使用canActivate保护登录组件的访问,它会阻止未授权用户访问管理特性区,如果用户未登录,它就会跳转到登录页。理想情况下,只有用户已登录的情况下才去加载登录组件

canLoad(route: Route): boolean {
let url = `/${route.path}`;
return this.checkLogin(url);
}

7 Angular的八大生命周期

7.1 ngOnChanges()

当angular检测到组件(或指令)重新设置数据绑定输入属性时响应。在被绑定的输入属性的值发生变化时调用,首次调用一定会发生在ngOnInit()之前,每一次改变绑定数据就调用一次这个钩子。OnChanges() 对应的函数 ngOnChanges(),该函数获取了一个对象,对象把每个发生变化的属性名映射在SimpleChange对象中,对象有属性当前值currentValue前一个值previousValue

🍑 父组件

<!--父组件html-->
<div>
  <span>我是父组件:</span>
  <input  [(ngModel)]="dataSource"/>
  <span style="margin-left: 20px;">{{dataSource}}</span>
</div>
<div style="height: 20px;"></div>
<app-two [data]="dataSource"></app-two>

🍑 子组件

<!--子组件TwoComponent html-->
<div>
  <span>我是子组件:</span>
  <button (click)="increment()">+</button>
  <span style="margin: 0 20px;">{{data}}</span>
  <button (click)="decrement()">-</button>
</div>
import { Component, Input, OnInit, SimpleChanges, OnChanges } from '@angular/core';
// 子组件js
export class TwoComponent implements OnInit,OnChanges {
  @Input() data:any=0;
  constructor() { }
  ngOnInit(): void {
    console.log('ngOnInit',this.data);
  }

  increment(){
    this.data++;
  }
  decrement(){
    this.data--;
  }

  ngOnChanges(changes: SimpleChanges): void {
    console.log("previousValue:",changes['data'].previousValue);
    console.log("currentValue:",changes['data'].currentValue);
  }
}

注意地,在子组件中操作是不能触发Onchanges钩子函数地,它是控制组件上属性的改变而触发

image.png

image.png

7.2 ngOnInit()

在Angular第一次显示数据绑定和设置指令/组件的输入属性之后,初始化指令/组件,在第一轮ngOnChanges()完成之后调用,只调用一次

7.3 ngDoCheck()

检测变化,在每一个Angular变更检测周期(变化监测)中调用,在执行ngOnChangesngOnInit()方法之后调用。不管是数据绑定还是鼠标的点击事件,只要触发了都会触发这个钩子的调用。

<div>
  <input  [(ngModel)]="dataSource"/>
  <span style="margin-left: 20px;">{{dataSource}}</span>
</div>
import { DoCheck,OnInit } from '@angular/core';

export class SonComponent implements OnInit,DoCheck {
  dataSource:any;
  constructor() { }

  ngOnInit(): void {
    console.log("ngOnInit!");
  }

  ngDoCheck(): void {
    console.log("DoCheck:",this.dataSource);
  }
}

image.png

7.4 ngAfterContentInit()

把内容投影进组件之后调用,在第一次执行ngDoCheck方法之后调用,只调用一次

import { AfterContentChecked, AfterContentInit,DoCheck, Input, OnInit } from '@angular/core';

export class SonComponent implements OnInit,DoCheck,AfterContentInit{
  @Input() data:any=0;
  dataSource:any;
  constructor() { }

  ngOnInit(): void {
    console.log("ngOnInit!");
  }

  ngAfterContentInit(): void {
    console.log("ngAfterContentInit!",this.dataSource);
  }

  ngDoCheck(): void {
    console.log("DoCheck:",this.dataSource);
  }
}

image.png

7.5 ngAfterContentChecked()

在每次完成被投影组件内容的变更检测之后调用。在执行ngAfterContentInit()ngDoCheck()方法之后调用。

import { AfterContentChecked, AfterContentInit, DoCheck, Input,  OnInit } from '@angular/core';

export class SonComponent implements OnInit,DoCheck,AfterContentInit,AfterContentChecked{
  @Input() data:any=0;
  dataSource:any;
  constructor() { }

  ngOnInit(): void {
    console.log("ngOnInit!");
  }

  ngAfterContentInit(): void {
    console.log("ngAfterContentInit!",this.dataSource);
  }

  ngAfterContentChecked(): void {
    console.log("ngAfterContentChecked!",this.dataSource);
  }

  ngDoCheck(): void {
    console.log("DoCheck:",this.dataSource);
  }
}

image.png

7.6 ngAfterViewInit()

在初始化完组件视图及其子视图之后调用,在第一次执行ngAfterContentChecked()方法之后调用,只调用一次

export class SonComponent implements OnInit,DoCheck,AfterContentInit,AfterContentChecked,AfterViewInit{
  dataSource:any;
  constructor() { }

  ngOnInit(): void {
    console.log("ngOnInit!");
  }

  ngAfterContentInit(): void {
    console.log("ngAfterContentInit!",this.dataSource);
  }

  ngAfterContentChecked(): void {
    console.log("ngAfterContentChecked!",this.dataSource);
  }

  ngAfterViewInit(): void {
    console.log("ngAfterViewInit!");
  }

  ngDoCheck(): void {
    console.log("DoCheck:",this.dataSource);
  }
}

image.png

7.7 ngAfterViewChecked()

在每次完成组件视图和子视图的变更检测之后调用。在执行ngAfterViewInit()ngAfterContentChecked()方法之后调用。

image.png

7.8 ngOnDestroy()

在Angular每次销毁指令/组件之前调用并清扫,在这里反订阅可观察对象和分离事件处理器,以防内存泄漏。什么时候会调用这个生命周期呢?也就是平时我们切换组件或从一个组件跳转到另一个组件的时候,这时候就会触发组件的销毁。