Angular的基本介绍

1,222 阅读1小时+

一、前言

适用版本: Angular16

Angular 是一个流行的开源前端框架,用于构建现代化的、单页面 Web 应用程序和动态 Web 页面。最开始由一个小的团队将其开发,后被Google收购,之后Google负责后期的开发和维护,并被广泛使用在许多大型的企业级应用程序中,我在这里的话分享一些经常使用的,所以肯定没有官方文档那样全面,如有我没分享到的...可以去官方文档吧!

二、项目的初始化

说明: Angular的项目初始化的工具是Angular CLI,使用前需要先安装,不过在安装之前需要先查看自己node的版本,需要LTS稳定的版本,因为使用LTS版本是为了确保稳定性、兼容性和安全性,以提供一个可靠的开发环境和更长期的支持,具体的下载方法可以参考官网也可以参考我下面的内容。

1.安装angular cli

// 先检查自己node的版本,看与官网的LTS版本是否相同
node -v

// node安装完毕之后你就可以使用npm包管理工具了
npm -v
// 全局安装angular cli,方便自己下一次初始化
// 项目的时候就不用再去安装了
npm install -g @angular/cli

2.初始化项目

说明: angular初始化项目的命令是ng new后面加上项目的名字来初始化一个项目,在初始化的过程中,Angular CLI 会安装必要的 Angular npm 包和其它依赖包,花费的时间可能会久一点,毕竟angular的实现也借助了其它的框架

// 这里初始化一个demo的项目
ng new demo

vue cli初始化项目和angular cli初始化项目类似,都是以问答的方式产生的,在angular中,你可能会遇到以下几个问题

// 问题一:就是你想你这个项目中与文件有关的内容能够
// 与它们分享嘛
Would you like to share pseudonymous usage data about 
this project with the Angular Team at Google under 
Google's Privacy Policy at 
https://policies.google.com/privacy. For more details 
and how to change this setting, see 
https://angular.io/analytics
// 问题二:是否使用angular中的路由
Would you like to add Angular routing?
// 问题三:css的预编译工具使用哪一个
Which stylesheet format would you like to use?

3.项目的运行

说明: 在包都安装完毕之后可以使用cd 项目名称这个命令进入到该项目文件夹,之后执行code .就会在编辑器中打开这个项目

// -open会自动的打开浏览器,一般默认的端口号是 4200
ng serve --open

4.项目结构的介绍

说明: 这里介绍一下在项目初始化中常出现的文件,当然angular中的配置文件很多,具体可以点击此处去官方文档查看

// 某个vscode创建的项目文件实例:
src/
  |- app/
  |   |- app.component.ts
  |   |- app.component.html
  |   |- app.component.css
  |   |- app.module.ts
  |- assets/
  |   |- logo.png
  |- environments/
  |   |- environment.ts
  |   |- environment.prod.ts
  |- index.html
  |- main.ts
styles.scss
tsconfig.json
angular.json
  • src/app/: 应用程序的根目录,包含了组件、模块和其他 Angular 相关的代码文件。

    • app.component.ts: 根组件的 TypeScript 文件。
    • app.component.html: 根组件的 HTML 模板。
    • app.component.css: 根组件的 CSS 样式文件。
    • app.module.ts: 应用程序的主模块,用来导入和配置其他模块和组件。
  • src/assets/: 存放静态资源文件的目录,比如图片、字体等。

    • logo.png: 一个示例图片文件。
  • src/environments/: 存放不同环境的配置文件的目录.

    • environment.ts: 开发环境的配置文件。
    • environment.prod.ts: 生产环境的配置文件。
  • src/index.html: 应用程序的主 HTML 文件,在这里加载应用程序的根组件。

  • src/main.ts: 应用程序的主入口文件,启动应用程序的代码。

  • styles.scss: 全局样式文件,可以定义全局的 CSS 样式。

  • tsconfig.json: TypeScript 配置文件,用于配置 TypeScript 编译器的选项。

  • angular.json: Angular 项目的配置文件,包含了构建、部署和其他项目配置选项。

关于样式,angularvue不同在于如果你angular中的样式是像我下面那样写的话,每个组件的样式都是自己独立的,不会与其它组件的样式混合在一起,而vue则会出现样式混合的问题,但可以通过在style上面加上scoped来解决这个问题,如果需要任何组件都可以使用的样式,那么就可以在styles.scss中去设置

5.项目启动的过程

说明:Angular项目是如何启动运行的,需要从angular.json配置文件来入手,在我看来,它和vuevue.config.js配置文件的作用是一致的,然后在angular.json文件中可以找到一个index配置项和main的配置项,它们分别是是HTML的入口以及JavaScript的入口,不过angular2.x以后,都是用TypeScript来编写的,以至于后缀均是.ts

// angular.json
"options": {
    "index": "src/index.html",
    "main": "src/main.ts",
 },
<!-- index配置项所指向的文件(默认) -->

<!doctype html>
<html lang="en"> 
<body>
  <!-- 这里会发现一个自定义的组件,Angular推荐大家: -->
  <!-- 自定义组件的时候加上自己的前缀,以此区分是组件还是标签 -->
  <!-- 其次看到 app- 前缀,均有angular提供 -->
  <app-root></app-root>
</body>
</html>
// main.ts:也就是入口文件

import { AppModule } from './app/app.module';

// platformBrowserDynamic():用于web浏览器的angular平台,
// 这是在浏览器中启动应用的第一步

// bootstrapModule():接受应用程序的angular根模块,默认是AppModule,
// 它提供了进入应用程序的入口

// catch:用于处理任何在启动时遇到的错误,这些错误会写在控制台上
platformBrowserDynamic().bootstrapModule(,默认是AppModule)
  .catch(err => console.error(err));
// app.module.ts

import { AppComponent } from './app.component';

@NgModule({
  // 这个属性表示需要加载哪一个根组件
  bootstrap: [AppComponent]
})
// app.component.ts

import { Component } from '@angular/core';

// 这里会发现很熟悉的东西
@Component({
  // 这个在vue中没见过,但是在JS选择器中看见过selector
  // 所以这个就表示组件所用的标签名,这也就与最开始的
  // 看到的那个组件对应起来了
  selector: 'app-root',
  
  // 看见template,也就联想到vue中的模板了,也就是
  // 存放html的地方,URL就是存放的地址
  templateUrl: './app.component.html',
  
  // 这个就是存放的样式,与上面同理,不过样式可以引用多个,
  // 所以使用数组存放
  styleUrls: ['./app.component.less']
})

// 那export就是写的JavaScript了
export class AppComponent {
  title = 'demo';
}

对于Angular的文件命名,遵循以下格式:文件名.属于什么部分.什么文件

三、模块

说明: 模块有两种,一种是根模块,用于描述angular应用程序的配置,另一种模块是功能模块,用来为应用程序添加结构,这样才能使应用程序的相关功能组合为一个单独的单元

1.根模块

说明: 一个angular程序至少需要一个根模块,默认存在于src/app文件夹中的app.module.ts文件之中,里面包含了一个使用了@NgModule装饰器的类,默认生成的文件是下面这样的,下面介绍这个装饰器的四个属性:importsdecrationsprovideprovidersbootstrap

  • imports:主要用于列出应用程序所需要的其它模块,其次还可以声明对自定义模块的依赖,自定义模块可以管理复杂的angualr程序,以及创建可重用的功能单元
  • declarations:用于提供angular程序所需要的指令、组件以及管道,它们统称可声明类,注意: 内置的可声明类是不可以放在这里的
  • providers:用于定义服务,这个在第七节会详细说明
  • bootstrap:用于指定应用程序的根组件或者是根组件集合,注意: 出现在此处的组件必须在declarations中定义
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule, AppRoutingModule],
  providers: [],
  bootstrap: [AppComponent],
})
export class AppModule {}

2.功能模块

作用: 有选择性的向应用程序的其它部分暴露模块文件夹里面的内容。功能模块同样需要@NgModule来装饰,其属性将bootstrap换成了exports,用于定义模块对外界公开导出的内容,这个内容可以是imports里面的,也可以是declarations里面的

思路: 既然模块是一组功能的集合,那么它们按理论来说应该放在同一个文件夹下面,所以第一步创建一个包含模块的文件夹,第二步需要定义一个模块,这个模块将所有移动到这个文件夹下面的功能进行汇总,之后在需要的地方使用导入导出就好。

四、组件

1.自定义组件

说明: vueangular是类似的,一个组件需要主体部分,而vue是单文件组件,只需要一个.vue格式的文件就足够,但是在angular中规定,一个组件就是一个class,而angular是基于ts开发的,那么一个组件就是一个.ts文件,并且组件是需要供别人使用,从而组件的这一个.ts的文件就是一个ES的模块,若需要供它人使用需要将其导出,其次,单纯一个class,别人会不知道它是用来干嘛的,这时候需要借助@component装饰器了,它的常用属性如下:

  • animations:用于配置动画
  • encapsulation:更改视图封装设置,它可以控制如何将组件样式与html文档的其余部分进行隔离
  • selector:匹配宿主元素的css选择器
  • styles:定义组件模板的样式,内联方式定义
  • styleUrls:定义组件模板的样式,样式单独放在一个文件中
  • template:定义内联模板
  • templateUrl:定义外部模板
  • providers:用于提供服务
  • viewProviders:给子视图提供服务

创建

// src/app/myc01.ts

import { Component } from '@angular/core';

// 装饰器:
// 作用:可以理解为指定class是用来干什么的
// 属性:它的本质是一个函数,参数是一个配置对象,
//       这个配置对象可以指定模板、标签名等等
@Component({
  // 这个就理解为vue中的模板,但是这种书写方式很具有局限性,因此可以拆分
  template: '<h1><h1>',
  
  // 就把.vue文件中的template部分拆分出去,但是组件的模板是唯一的
  templeUrl: '这里写模板存放的地址',
  
  // 这个理解为组件的标签名,这里需要注意selecttor后面的写法
  // (因为在angular中一切皆组件,所以要注意组件表达的内容是什么)
  // myc01:这样写就表示标签名
  // .myc01:这样写就表示一个class
  // [myc01]:这样写就表示一个属性
  selector: 'myc01',
  
  // 样式也是一样的,不过样式会存在很多,并不是唯一的,
  // 所以展示的话使用数组
  styles: [],
  
  // 这个是存放样式文件的地址的,一样可以写多条文件
  styleUrls: []
})

// 这里就是写的js了,因为样式可写可不写,所以这里基本
// 上一个简单的组件就完成了,推荐写类名的时候首字母大写
// 并且类名推荐有意义,这样方便你在模块中很清楚的看清
// 存在那些组件
export class Myc01 {}

如果自己创建自定义组件,那么一个组件就是一个文件夹,因为文件夹里面存放模板样式数据这三个文件,并且文件夹名就是组件名,这样防止组件存在很多以后找很麻烦

注册

说明: 由于是刚初始化的一个项目文件,当然模块就只有主模块,也就是app.module.ts这个文件

// src/app/app.module.ts

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';

// 引入文件
import { Myc01 } from './myc01';

@NgModule({
  // declarations表示官宣,宣布,发布的意思,也就是在这里来
  // 进行组件的注册,注意,组件注册完毕之后就会变成全局组件
  // 了,在哪里都可以去使用
  declarations: [
    AppComponentMyc01 // 进行注册,注册的名字就是引入的类名
  ],
  imports: [
    BrowserModule,
    AppRoutingModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

使用

说明: 在项目初始化的时候会生成一个app-root的组件,这个组件称为跟组件,它是angular模块的入口点,所有自定义组件都在这个组件内部使用。

// src/app/app.component.html

// 标签名就是在selector中的内容
<myc01></myc01>

不要在入口文件index.html中使用自定义组件,这里只能使用跟组件

命令

说明: 在使用下面的命令时,它会创建一个以组件名为名字的文件夹,在文件夹中会分别创建.ts.html.css的文件,并会更新主模块中的内容,将创建的组件注册进去

// 安装好angular cli:

ng generate component 组件名
ng g component 组件名
// 没有安装或安装失败angular cli:

npx ng generate component 组件名
npx ng g component 组件名

上面说到过每一个组件就是一个函数,那么如果在组件自己的模板中使用组件自己,其等价于在函数中调用函数本身,这样搞会形成递归调用,最后浏览器就挂掉了

2.数据传递

说明: 在angular中,父组件可以通过[属性绑定]将数据传递给子组件,子组件通过@Input()装饰器接收数据并进行处理。而子组件可以通过@Output()装饰器和事件发射器EventEmitter将数据发送回父组件,这两个装饰器在下面指令那部分有介绍。

父组件给子组件传递数据

父组件:

<!-- src/app/myc01/myc01.component.html -->

<app-myc02 [demoText]="text"></app-myc02>
// src/app/myc01/myc01.component.ts

import { Component } from '@angular/core';

@Component({
  selector: 'app-myco1',
  templateUrl: './myco1.component.html',
  styleUrls: ['./myco1.component.less']
})
export class Myco1Component {
  // 定义一个需要传递的数据
  text = 3
}

子组件:

<!-- src/app/myc02/myc02.component.html -->

<p>{{ demoText }}</p>
// src/app/myc02/myc02.component.ts

// 注意input装饰器导入的路径
import { Component, Input } from '@angular/core';

@Component({
  selector: 'app-myc02',
  templateUrl: './myc02.component.html',
  styleUrls: ['./myc02.component.less']
})
export class Myc02Component {
  // 普通的属性是不能够被父组件传值的,如果需要的话,需要定义一个
  // 输入属性,很简单,就是使用Input装饰器装饰一下就可以,其次,
  // 一个装饰器只能够装饰一个属性,并且装饰器和输入属性之间是
  // 不能够存在其他的内容的,最后,输入属性和装饰器之间是一一对应
  // 关系的
  @Input()
  demoText:any = null
}

父组件直接获取子组件的引用

父组件:

说明: 这种方式主要是使用#这个标识符来标记子组件,然后在父组件中定义变量,将这个变量通过@ViewChild装饰器与对应的子组件连接起来,此时,这个变量的值会变成指定子组件的引用,当然,装饰器与变量还是一对一的关系

<!-- src/app/myc01/myc01.component.html -->

<!-- 将子组件myc02标记为#c01 -->
<app-myc02 #c01></app-myc02>

<button (click)='print()'>打印出子组件</button>
// src/app/myc01/myc01.component.ts

import { Component, ViewChild } from '@angular/core';

@Component({
  selector: 'app-myco1',
  templateUrl: './myco1.component.html',
  styleUrls: ['./myco1.component.less']
})
export class Myco1Component {
  // 这个装饰器是有参数的,第一个参数表示定义的变量需要与那个子组件的
  // 关联起来,上面定义的名字是#c01,那么这里关联的标识符就是c01,第
  // 二个参数是一个配置对象,里面的static的取值是一个布尔值,表示标识符
  // 所在的这个元素是动态的还是静态的,动态的意思是它可能会显示,也可能
  // 不会显示,比如使用ngif这样的指令时,静态的话就是表示当前这个子组件
  // 一直是显示的状态,
  @ViewChild('c01', { static: true })
  private firstChild
  
  print() {
      // 这里的变量的值就是指定子组件的引用对象了
      console.log(this.firstChild)
  }
}

子组件:

<!-- src/app/myc02/myc02.component.html -->

<p>这是子组件</p>
// src/app/myc02/myc02.component.ts

import { Component } from '@angular/core';

@Component({
  selector: 'app-myc02',
  templateUrl: './myc02.component.html',
  styleUrls: ['./myc02.component.less']
})
export class Myc02Component {}

子组件给父组件传递数据

子组件:

<!-- src/app/myc02/myc02.component.html -->

<!-- 子组件向上传递数据的话还是通过出发事件来完成任务的 -->
<button (click)="demo()">点击向父组件传递数据</button>
// src/app/myc02/myc02.component.ts

import { Component, EventEmitter, Output } from '@angular/core';

@Component({
  selector: 'app-myc02',
  templateUrl: './myc02.component.html',
  styleUrls: ['./myc02.component.less']
})
export class Myc02Component {
  // 其次由于是需要向外传递数据,那就不是使用input
  // 装饰器定义输入属性了,需要使用output定义输出
  // 属性,这个属性的值是一个触发器,通过它来完成
  // 传递数据的任务
  @Output()
  private demoEvent = new EventEmitter()

  private data = 2

  demo() {
    // 通过触发器的emit方法来传递,方法的参数就是
    // 传递的数据
    this.demoEvent.emit(this.data)
  }
}

父组件:

<!-- src/app/myc01/myc01.component.html -->

<span>{{ text }}</span>

<!-- 在父组件上面需要定义一个自定义事件,事件的名字 -->
<!-- 需要和子组件中定义的那个触发器的名字相同, -->
<!-- 这样就接收到了子组件的数据,数据会存放在$event里面 -->
<app-myc02 (demoEvent)="doSomething($event)"></app-myc02>

关于$event:
如果事件是原生事件,$event表示事件对象event
如果事件是自定义事件,$event表示获取的数据

// src/app/myc01/myc01.component.ts

import { Component } from '@angular/core';

@Component({
  selector: 'app-myco1',
  templateUrl: './myco1.component.html',
  styleUrls: ['./myco1.component.less']
})
export class Myco1Component {
  text = 3

  doSomething( e: number ) {
    // 这里的参数名字可以随便写,其值的话就是传递过来的数据
    this.text = e
  }
}

3.投影

内容投影

说明: 这个跟可以理解为vue中的插槽,简单理解就是你定义一个组件,然后给这个组件像下面这样传递内容,这个传递的内容就会被<ng-content>这个元素接住并展示,这就叫内容投影,看下面的例子:

<!-- 假设创建一个product的组件 -->

<ng-content></ng-content>
<!-- 然后在使用的给组件传内容,这个内容就会 -->
<!-- 被上面的元素接住并展示 -->
<product> 传递的内容 </product>

五、指令

指令的分类:

  • *结构型指令:会影响DOM树的结构
  • [属性型指令]:只会影响元素的外观或者行为
  • 组件指令:组件是一种特殊的指令

1.HTML数据绑定

说明: vue中的双括号语法{{ }}就是借鉴于angular中的ng表达式,也叫做大胡子语法,都是用{{ }}来表示,使用方法都差不多

// src/app/myc02/myc02.component.ts

import { Component } from '@angular/core';

@Component({
  selector: 'app-myc02',
  templateUrl: './myc02.component.html',
  styleUrls: ['./myc02.component.less'],
})
export class Myc02Component {
  name = 'zhangsan';
  age = 18;
}
<!-- src/app/myc02/myc02.component.html -->

<!-- zhangsan -->
<div>name属性的值是: {{ name }}</div>
<!-- 18 -->
<div>age属性的值是: {{ age }}</div>
<!-- 20 -->
<div>能够使用表达式吗?: {{ age + 2 }}</div>
<!-- true -->
<div>能够使用条件判断吗?: {{ age >= 18 }}</div>
<!-- 成年 -->
<div>能够使用三木运算符吗?: {{ age >= 18 ? '成年了' : '未成年' }}</div>
<!-- true -->
<div>能够使用连接符吗?: {{ age >= 18 && age <= 20 }}</div>
<!-- 8 -->
<div>能够使用字符串的属性吗?: {{ name.length }}</div>
<!-- zhangsan -->
<div>能够使用字符串的方法吗?: {{ name.toString() }}</div>

<!-- 报错 -->
<div>能够使用new关键字吗? : {{ new Object() }}</div> 
<!-- 报错 -->
<div>能够使用JSON的方法吗? : {{ JSON.stringify({}) }}</div> 

不能使用的情况:

  • new关键字:new关键字在angular中是禁止使用的
  • JSON方法:因为JSON在插值语法中是undefined

2.属性绑定

// src/app/myc03/myc03.component.ts

import { Component } from '@angular/core';

@Component({
  selector: 'app-myc03',
  templateUrl: './myc03.component.html',
  styleUrls: ['./myc03.component.less']
})
export class Myc03Component {
  name = '这是我写的一段文字'
  Name = '我是一个字符串'
  imgUrl = '1.jpg'
}

标准属性绑定

说明: 浏览器使用DOM表示HTML文档,HTML文档中每个元素都会使用DOM中的一个JavaScript对象表示,所以这里指的标准属性就是在JavaScript对象上能够找到的属性,标准语法是[属性名]="表达式/值",当然,也可以使用属性名="{{ 表达式/值 }}"

[attr.属性名]="表达式/值":它表示这个属性属于HTML元素,一般用在元素的属性在DOM API中不存在对应属性的时候,适用于非标准属性的绑定

<!-- src/app/myc03/myc03.component.html -->

<p title ="{{ name }}">你觉得这个是不是我写的</p>
<p [title] ="name">这个呢</p>

字符串插入绑定

<!-- src/app/myc03/myc03.component.html -->

<!-- 需要用到字符串的拼接,注意单双引号交替 -->
<p [title] ="'你是谁' + Name">这个呢</p>
<!-- src/app/myc03/myc03.component.html -->

<!-- 不能使用字符串模板 -->
<p [title] ="`你是谁${Name}`">这个呢</p>
<!-- src/app/myc03/myc03.component.html -->

<!-- 在使用图片的时候,由于图片都会存放在一个文件夹下 -->
<!-- 那么引入图片的路径开头都是一致的, -->
<!-- 比如说:'../assets/' + '图片名称'这种格式的 -->
<!-- 那么在使用的时候就可以只将图片名称定义为变量, -->
<!-- 因为后台可能也不知道你图片会放在什么位置, -->
<!-- 那能够确定的就只有图片的名称,所以这样写 -->
<img [src] ="'../assets/' + 'imgUrl'">图片路径的拼接</img>

3.事件绑定

基本使用

说明: 其语法的格式为(事件名)="事件处理函数/执行语句",如果后面是跟一个函数,那么这个函数必须加上(),否则就会报错,其次执行语句只能是那种逻辑很简单的语句。

<!-- src/app/myc03/myc03.component.html -->

<button (click)="onSave()">Save</button>
// src/app/myc03/myc03.component.ts

import { Component } from '@angular/core';

@Component({
  selector: 'app-myc03',
  templateUrl: './myc03.component.html',
  styleUrls: ['./myc03.component.less']
})
export class Myc03Component {
  onSave() {
    console.log('我写的angular的第一个事件')
  }
}

模板引用变量

说明: 它可以方便地操作模板中的元素或者获取元素的引用,一般使用# 符号来创建模板引用变量,然后在组件类中通过 @ViewChild 或者 @ViewChildren 装饰器来获取对这个模板引用变量的引用,然后进行操作

<!-- src/app/myc03/myc03.component.html -->

<input #inputRef type="text" />

<button (click)="logInputValue()">
    Log Input Value
</button>
// src/app/myc03/myc03.component.ts

import { ComponentViewChild } from '@angular/core';

@Component({
  selector: 'app-myc03',
  templateUrl: './myc03.component.html',
  styleUrls: ['./myc03.component.less']
})
export class Myc03Component {
  @ViewChild('inputRef') input: any;

  logInputValue() {
    // 这里就可以打印出这个元素
    console.log(this.input);
  }
}

绑定键盘事件可以绑定到具体的某一个键上面,例如(keyup.enter)="fn()"将事件绑定到回车键上,详细内容可以点击此处

4.循环

*ngFor

(1)基本使用

说明: 其基本语法为let 变量名 of 可迭代数据源,相当于提供一个循环的模板

<!-- src/app/myc03/myc03.component.html -->

<!-- 一般数据源是一个数组 -->
<ul *ngFor="let item of dataSource">
  <li>{{ item }}</li>
</ul>

<!-- 当然也可以是一个函数返回一个数组 -->
<ul *ngFor="let item of getData()">
  <li>{{ item }}</li>
</ul>
// src/app/myc03/myc03.component.ts

import { Component } from '@angular/core';

@Component({
  selector: 'app-myc03',
  templateUrl: './myc03.component.html',
  styleUrls: ['./myc03.component.less']
})
export class Myc03Component {
  dataSource: number[] = [1, 2, 3, 4, 5, 6];

  getData(): number[] {
    return this.dataSource;
  }
}

注意:

  • 声明变量必须使用let关键字
  • *ngFor这个指令循环只能使用of,不可以使用in

(2)局部变量

说明: 这个指令导出了一系列值,可以指定别名后作为局部变量使用,常用的有如下几个,它们不能直接使用,需要用变量 as 局部变量进行转化

  • index:当前元素的索引
  • count:迭代对象的长度,相当于length
  • odd:当前对象在数据源中的位置是不是奇数(布尔值)
  • even:当前对象在数据源中的位置是不是偶数(布尔值)
  • first:当前对象在数据源中的位置是不是第一个(布尔值)
  • last:当前对象在数据源中的位置是不是最后一个(布尔值)
<!-- src/app/myc03/myc03.component.html -->

<ul *ngFor="let item of dataSource; index as i">
  <li>{{ i }}</li>
</ul>
// src/app/myc03/myc03.component.ts

import { Component } from '@angular/core';

@Component({
  selector: 'app-myc03',
  templateUrl: './myc03.component.html',
  styleUrls: ['./myc03.component.less']
})
export class Myc03Component {
  dataSource: number[] = [1, 2, 3, 4, 5, 6];
}

5.条件

*ngIf

说明: 这个指令的语法是:*ngIf="处理函数/表达式/值",但是前提是能够得到布尔值,最后就是vuev-if跟这个指令差不多,也就是显示隐藏的实质是是否将元素从dom中删除

<!-- src/app/myc03/myc03.component.html -->

<!-- 这样在页面上你就看不见任何元素了,在dom树中你也看不见 -->
<ul *ngIf="true">这里展示与v-if相类似的功能</ul>

注意:

  • *ngIf:会销毁和创建元素,相当于v-if
  • [ngIf]:只是添加或移除元素的属性,相当于v-show

说明: 既然vue中存在v-ifv-else,那么angular也会存在自己的v-else

<!-- src/app/myc03/myc03.component.html -->

<ul *ngIf="false; else elseBlock">这里展示与v-if相类似的功能</ul>
<ng-template #elseBlock>
  <p>这里展示与v-else相类似的功能</p>
</ng-template>

注意:

  • else使用的格式是*ngIf="布尔值表达式; else else展示的内容"
  • else展示的内容是通过 # 来进行关联的
  • else展示的内容和if中间是不允许有其他代码的
  • else展示的部分必须使用ng-template,这个标签是一个容器,这个容器不会在dom结构中生成,但是它的内容可以

ngSwitch

说明: 这个JavaScript中的switch语句类似,判断的条件使用[ngSwitch]="选择语句"放在指定元素上面,这个元素里面通过*ngSwitchCase="值"来进行匹配,当然,也会存在默认值,使用*ngSwitchDefault设置,具体格式如下:

<!-- src/app/myc03/myc03.component.html -->

<div [ngSwitch]="选择语句">
  <div *ngSwitchCase="值1">当选择语句的值为值1时展示</div>
  <div *ngSwitchCase="值2">当选择语句的值为值2时展示</div>
  ...
  <div *ngSwitchDefault>
      当选择语句的值都没有匹配则会使用此处
  </div>
</div>

注意:
放在ngSwitch里面所有的元素都会展示在页面里面

6.样式

标准属性绑定

说明: 这个跟属性绑定中使用的语法是类似的,也就是[class]="表达式/值",一般用在将元素的属性一次性绑定上去

特殊属性绑定

说明: 这种也就是控制单一的样式生效或者失效,其语法格式为[class.样式名]="表达式/值"

ngClass 和 ngStyle

说明: 控制少量样式使用[ngStyle]="样式对象"的方法,这种方法虽然可行但是不太稳妥,一般推荐另外一种方法就是[ngClass]="class配置对象"

<!-- src/app/myc03/myc03.component.html -->

<!-- 样式绑定的是使用ngStyle -->
<p [ngStyle]="styleControl">这里使用style来控制样式</p>

<!-- 方法一定要加(),否则方法不会执行 -->
<button (click)="toggle()">更改样式</button>
// src/app/myc03/myc03.component.ts

import { Component } from '@angular/core';

@Component({
  selector: 'app-myc02',
  templateUrl: './myc02.component.html',
  styleUrls: ['./myc02.component.less'],
})
export class Myc02Component {
  // 一般要控制样式的话需要将样式写在这里,因为如果你讲css写在html里面,
  // 是获取不到的,只有写在这里才能够去写方法去更改
  styleControl = {
    color: 'red',
    // 一般像这种中间存在-的css样式需要用''将其包裹起来
    'border-color': 'red',
    // 这种驼峰命名的css是不被angular所识别的,需要将其改写成上面的-链接的形式
    backgroundColor: 'red',
  };

  toggle() {
    // 更改上面写的样式,达到使用style控制样式的目的
    this.styleControl.color = 'blue';
  }
}

上面这种使用style进行样式控制会存在一个问题,就是我在JavaScript中去书写了很多的css样式代码,这样使样式的可复用性比较差,同时也违背了高内聚低耦合的理论,所以一般推荐使用class来控制样式

<!-- src/app/myc03/myc03.component.html -->

<!-- 样式绑定的是使用ngClass -->
<p [ngClass]="classControl">这里使用class来控制样式</p>

<!-- 方法一定要加(),否则方法不会执行 -->
<button (click)="toggle()">更改样式</button>
// src/app/myc03/myc03.component.less

.tColor {
  background-color: red;
}
// src/app/myc03/myc03.component.ts

import { Component } from '@angular/core';

@Component({
  selector: 'app-myc02',
  templateUrl: './myc02.component.html',
  styleUrls: ['./myc02.component.less'],
})
export class Myc02Component {
  // 在class的这个配置对象里面,每一对键值对的键代表类名,其值是一个布尔值、
  // 一个表达式返回布尔值,一个函数执行返回布尔值等等都可以,前提是能够得到
  // 布尔值,如果是true,则该样式生效,否则就失效,那么用这种方法你就算书写
  // 大量样式而我只需要控制类名就好
  classControl = {
    tColor: this.fn(),
  };

  toggle() {
    // 更改上面写的样式,达到使用class控制样式的目的
    this.classControl.tColor = false;
  }

  // 函数执行返回一个布尔值true
  fn() {
    return true;
  }
}

样式绑定

说明: 使用style也有两种绑定样式的方式,标准的语法是[style.样式名]="表达式/值",特殊的语法是[style.样式名.单位]="表达式/值",它们都只能为单一样式设置,

7.双向绑定

前提: angular中的指令实质上是一个个的class,它们按照功能会分别封装在不同的模块里面,想要使用必须导入才行,导入的话在主模块app.moudle.ts@NgMoudle里面的imports中导入,就像上面介绍的6个指令,它们都是存放在CommonModule这个模块之中,如果没有导入模块你就不能够使用,其次,双向绑定使用的是([ngModel]),这个指令存放在FormsModule模块中,这个模块存放在@angular/forms里面

// src/app/app.moudle.ts

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

// 有这个模块才能够使用双向绑定
import { FormsModule } from "@angular/forms";

@NgModule({
  imports: [
    // 这是一个浏览器的模块,只要你的angular项目是用于这个范围的,
    // 这个模块就必须存在,其次这个模块默认会导入CommonModule,
    // 这也是为什么你可以使用ngif、ngfor这些命令的
    BrowserModule,
    FormsModule
  ],
})
export class AppModule {}
// src/app/myc03/myc03.component.html

<input type="text" [(ngModel)]="pname">
<p>pname的值是: {{ pname }}</p>
// src/app/myc03/myc03.component.ts

import { Component } from '@angular/core';

@Component({
  selector: 'app-myc03',
  templateUrl: './myc03.component.html',
  styleUrls: ['./myc03.component.less'],
})
export class Myc03Component {
  pname = 'zhangsan';
}

简单实现:vue中,v-model的原理默认情况下可以理解为一个value的属性和一个input的事件,既然vue可以,那么angular也可以使用这种方法将其实现出来,模型层到视图层可以使用[value]代替,视图层到模型层可以用(input)来解决,这也说明了为什么用[(ngModel)]来表示双向数据绑定了

<!-- src/app/myc03/myc03.component.html -->

<input type="text" [value]="pname" (input)="changeValue($event)">
<p>pname的值是: {{ pname }}</p>
// src/app/myc03/myc03.component.ts

import { Component } from '@angular/core';

@Component({
  selector: 'app-myc03',
  templateUrl: './myc03.component.html',
  styleUrls: ['./myc03.component.less'],
})
export class Myc03Component {
  pname = '1';

  changeValue(event: any) {
    this.pname = event.target.value;
  }
}

关于$event和event:可以理解为作用一样但长相有区别

  • $event:在Angular模板中用于访问事件对象的特殊变量,在组件类中使用不了
  • event:是JavaScript中的普通事件对象,在组件类中直接使用,但是值与上面是相等的

8.复制

*ngTemplateOutlet

(1)基本使用

说明: 这个指令用于在指定的位置重复一段内容,重复的内容需要写在<ng-template>里面,并使用#名字的方式给这段内容取名字,在使用的时候,使用*ngTemplateOutlet指令来获取,也能绑定ngTemplateOutlet属性来获取基本使用如下:

<ng-template #title>这是一段需要重复的内容</ng-template>

<div [ngTemplateOutlet]="title">copy</div>

(2)提供上下文数据

说明: 这个指令存在一个ngTemplateOutletContext的属性用于给重复的内容提供自定义数据,由于上面使用有两种方式,当然这里也会有两种方式,如下:

<!-- src/app/myc03/myc03.component.html -->

<ng-template #title let-add="name">{{ add }}</ng-template>

<div *ngTemplateOutlet="title;
     context: contextExp"></div>
     
<div [ngTemplateOutlet]="title" 
     [ngTemplateOutletContext]="contextExp"></div>

// src/app/myc03/myc03.component.ts

import { Component } from '@angular/core';

@Component({
  selector: 'app-myc03',
  templateUrl: './myc03.component.html',
  styleUrls: ['./myc03.component.less'],
})
export class Myc03Component {
    contextExp = {
        name: 'zhangsan',
    };
}

注意:

  • 在使用数据的时候需要使用let-变量名="数据中的变量名"这个格式转化后再使用
  • 如果使用*ngTemplateOutlet,那么定义的数据需要使用context:数据的形式才能定义上去

9.自定义指令

命令:

// 完整写法
ng g directive 指令名称

// 简写形式
ng g d 指令名称

属性指令

(1)基本使用

创建:ng g d attr创建一个属性指令为例:

// attr.directive.ts

import { Directive, ElementRef } from '@angular/core';

// 指令使用@Directive装饰器修饰
@Directive({
  // 这个表示在模板中使用指令的名称是appAttr
  // 其次指令初始状态只有selector属性,比组件
  // 初始化的时候少模板和样式两个属性,因此根据
  // 对象间的父子关系,指令是父,组件是子,因此
  // 组件是一种特殊的指令
  selector: '[appAttr]',
  // 可以使用这个属性将指令定义成一个变量,然后在
  // 模板中使用这指令的属性和方法
  exportAs: 'appAttrDirective'
})
export class AttrDirective {
  // 每一次调用指令都会执行一次这个constructor这个构造函数,函数的参数
  // 可以获取到是那个元素使用了这个指令,获得的结果是一个配置对象
  constructor(el: ElementRef) { 
      // 这里就可以拿到上面input的信息了
      console.log(el)
  }
  
  // 假设存在一个这样的方法
  greet() {
    return 'Hello from Custom Directive!';
  }
}

注册: 由于组件是一种特殊的指令,所以注册的方式跟组件是一样的

// src/app/app.model.ts

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { appAttr } from './attr.directive';

@NgModule({
  declarations: [appAttr],
  imports: [
    BrowserModule,
  ],
  providers: [],
  bootstrap: [AppComponent],
})
export class AppModule {}

使用:

<!-- src/app/myc03/myc03.component.html -->

<!-- 指令是作为元素的属性来使用的 -->
<input type="text" appAttr>

<!-- 通过 #变量名="指令导出的名字 的形式来将指令实例赋值给一个模板变量 -->
<div appAttr #myappAttrDirective="appAttrDirective">
  {{ myappAttrDirective.greet() }}
</div>

使用步骤:

  • 将自己写好的指令作为属性调价到一个元素中去
  • 在NgModule装饰器的declarations属性中声明需要使用的指令

(2)@Attribute

说明: 这个装饰器可以用来读取元素的属性,其语法格式为@Attribute('需要获取的属性') 定义的变量: 值的类型,在宿主元素上获取到属性值后,会将其保存在变量里面,可以以此结合指令来使用,以上面的指令举例:

<input type="text" appAttr appAttr-copy="" />
import { Attribute, Directive, ElementRef } from '@angular/core';

@Directive({
  selector: '[appAttr]',
})
export class AttrDirective {
  constructor(@Attribute('appAttr-copy') attr: string) {
    // 这里就可以打印出'appAttr-copy'的属性值了
    console.log(attr);
  }
}

限制: 元素的属性值是静态不变的,如果是可变的就不能这样获取,并且用需要在构造函数里面使用

(3)@Input

说明: 一般通过数据绑定的属性的属性值都可以用这个装饰器创建一个输入属性来获取,其语法格式为@Input('需要获取的属性') 定义的变量: 值的类型,在宿主元素上获取到属性值后一样会保存在变量里面,输入属性与指令结合使用的时候需要注意:指令的构造函数执行完毕其已经生成新的指令对象之后,获取到的值才能赋值给输入属性,也就是构造函数无法访问输入属性的值,因此指令为了响应变化,需要实现ngOnChanges生命周期函数,这里注意,如果获取的属性和定义的变量相同,就可以省略写需要获取的变量了

<input type="text" appAttr [appAttr-copy]="index" />

<button (click)="fn()">点击++</button>
import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.less'],
})
export class AppComponent {
  index: number = 3;

  fn() {
    this.index++;
  }
}
import { Directive, Input, OnChanges } from '@angular/core';

@Directive({
  selector: '[appAttr]',
})
export class AttrDirective implements OnChanges {
  @Input('appAttr-copy') attr!: number;

  ngOnChanges() {
    this.fn();
  }

  // 这里就能打印出每次改变后的值了
  fn() {
    console.log(this.attr);
  }
}

(4)@Output

说明: 这个装饰器能够定义输出属性来创建自定义事件,通过这个特性可以将数据发送到需要的地方,创建事件需要使用到EventEmitter这个类,创建的格式为@Output('自定义事件名') 定义的输出属性 = new EventEmitter<事件触发所传递的数据类型>();,注意,如果获取的属性和定义的变量相同,就可以省略写需要获取的变量了,举例:

import { Directive, ElementRef, EventEmitter, Output } from '@angular/core';

@Directive({
  selector: '[appAttr]',
})
export class AttrDirective {
  // 在指令里面创建一个EventEmitter对象,将其赋值给click变量,
  // 对于appAttr-copy,它可以理解为自定义事件名,通过触发事件
  // 的处理函数来获取发射的数据
  @Output('appAttr-copy') click = new EventEmitter<string>();

  constructor(el: ElementRef) {
    el.nativeElement.addEventListener('click', () => {
      // 发射数据需要触发EventEmitter对象的emit()方法,
      // 方法中的内容就是需要发射的数据,注意类型需要与
      // 定义的相同
      this.click.emit('发射数据');
    });
  }
}
<!-- 这里使用了一个appAttr-copy的自定义事件 -->
<!-- 当点击按钮就会触发这个事件后面的处理函数 -->
<!-- $event用于访问传给emit方法的值 -->

<button appAttr 
        (appAttr-copy)="fn($event)"
>点击++</button>
import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.less'],
})
export class AppComponent {
  fn(e: string) {
    // 点击后这里就会打印出来
    console.log(e);
  }
}

(5)@HostBinding

说明: 通过这个装饰器可以动态的去设置元素的属性,其简单使用像下面这个例子一样

// 还是用上面的指令

import {
  Directive,
  ElementRef,
  HostBinding,
} from '@angular/core';

@Directive({
  selector: '[appAttr]',
})
export class AttrDirective {
  // 这里给宿主元素绑定了一个color:blue的样式,样式值赋给
  // textColor这个变量,可以通过这个变量来修改样式
  @HostBinding('style.color') textColor: string = 'blue';
  @HostBinding('class.active') isActive: boolean = true;

  constructor(el: ElementRef) {
    el.nativeElement.addEventListener('click', () => {
      // 点击按钮后修改文字颜色
      this.textColor = 'red';
    });
  }
}

MKXZBM.png

(6)@HostListener

说明: 通过这个装饰器可以实现在指令或组件上监听宿主元素的事件,看栗子:

// 以上面的指令为例:

import {
  Directive,
  HostListener,
} from '@angular/core';

@Directive({
  selector: '[appAttr]',
})
export class AttrDirective {
  // 这里将一个fn函数用来绑定按钮的click事件,
  // 也就是当我点击按钮的时候,这个函数就会触发,
  // 以此得到监听的效果
  @HostListener('click') fn() {
    console.log('Mouse click the element');
  }
}

结构指令

说明: 结构指令的第一个标志在于其构造函数,它要求angular使用一些新类型来提供参数,这里需要使用ViewContainerRef这个对象,与之相关的一些属性和方法如下:

  • element:返回容器元素的ElementRef对象
  • createEmbeddedView(template):使用模板创建新视图
  • clear():从容器中删除所有视图
  • get(index):返回指定索引处的视图的ViewRef对象
  • indexOf(view):返回指定ViewRef对象的索引
  • insert(view,index):在指定索引处插入视图
  • remove(index):删除并销毁指定索引处的视图
  • detach(index):将视图从指定索引处进行分离,但是不会销毁视图

(1)*ngif 实现

指令:

import {
  Directive,
  Input,
  OnChanges,
  SimpleChange,
  TemplateRef,
  ViewContainerRef,
} from '@angular/core';

@Directive({
  selector: '[myif]',
})
export class StructureDirective implements OnChanges {
  // ViewContainerRef:用于管理视图容器的内容,
  //                   这里也就是ng-template元素出现的区域,
  //                   它有一系列属性和方法来管理视图部分
  // TemplateRef:表示宿主元素的内容
  constructor(
    private container: ViewContainerRef,
    private template: TemplateRef<Object>
  ) {}

  // 用于接收是否显示隐藏
  @Input('myif') expressionResult: boolean = false;

  ngOnChanges(changes: { [property: string]: SimpleChange }) {
    let change = changes['expressionResult'];
    if (!change.isFirstChange() && !change.currentValue) {
      // 清空视图
      this.container.clear();
    } else if (change.currentValue) {
      // 将宿主元素的内容添加到视图容器中
      this.container.createEmbeddedView(this.template);
    }
  }
}

ng-template的元素的内容是会被销毁和重新创建的,而不是简单的显示隐藏

指令使用:

<div>
  <input type="checkbox" [(ngModel)]="show" />
</div>

<ng-template [myif]="show"> 需要显示的内容 </ng-template>

这种不带*的结构指令需要配合ng-template元素才能使用

指令简介语法:

<div>
  <input type="checkbox" [(ngModel)]="show" />
</div>

<div *myif="show"> 需要显示的内容 </div>

数据:

import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.less'],
})
export class AppComponent {
  show: boolean = false;
}

(2)*ngFor实现

指令:

import {
  Directive,
  Input,
  OnInit,
  TemplateRef,
  ViewContainerRef,
} from '@angular/core';

@Directive({
  selector: '[myForOf]',
})
export class IteratorDirective implements OnInit {
  constructor(
    private container: ViewContainerRef,
    private template: TemplateRef<Object>
  ) {}

  @Input('myForOf') dataSource!: any[];

  ngOnInit(): void {
    this.container.clear();
    // 这里使用循环来填充可以得到视图容器可以填充多个视图
    for (let i = 0; i < this.dataSource.length; i++) {
      // 第一个参数对象提供要插入容器的内容
      // 第二个参数对象为隐式值提供数据
      this.container.createEmbeddedView(
        this.template,
        new IteratorContext(this.dataSource[i], i, this.dataSource.length)
      );
    }
  }
}

// 为了以类型安全的方式为模板提供数据,重新定义一个类
// 然后可以使用这个类来提供额外的上下文数据
class IteratorContext {
  // 这里提供一组跟ngFor里面存在的值
  odd: boolean;
  even: boolean;
  first: boolean;
  last: boolean;
  constructor(public $implicit: any, public index: number, total: number) {
    this.odd = index % 2 == 1;
    this.even = !this.odd;
    this.first = index == 0;
    this.last = index == total - 1;
  }
}

指令使用:

<ul>
  <!-- 1.在使用ng-template元素的时候,数据源属性名称-->
  <!-- 需要以Of结尾,以便支持简介语法 -->
  <!-- 2.let-item用于定义隐式值,在指令进行遍历的时候,-->
  <!-- 在ng-template元素中使用该值引用来处理当前对象 -->
  <ng-template
    *myForOf="dataSource"
    let-item
    let-odd="odd"
    let-even="even"
    let-first="first"
    let-last="last"
  >
    <li>
        {{ item }}
        {{ odd }}
        {{ even }}
        {{ first }}
        {{ last }}
    </li>
  </ng-template>
</ul>

简洁语法:

<li *myFor="let item of dataSource;
    let odd = odd"
>
    {{ item }}
    {{ odd }}
</li>

使用简介语法的时候,属性的Of部分会被省略,并在名称前面加上一个*[]消失,最后所有的let-属性会被替换,主数据值成为初始表达式的一部分,而其它上下文值以;来进行分隔

使用时的数据:

import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.less'],
})
export class AppComponent {
  dataSource: number[] = [1, 2, 3, 4];
}

属性级数据更改: 当结构型指令使用的数据源中某一个对象属性发生改变的时候,这会对ng-template元素中包含的数据绑定有影响,angualr会自动处理这些改变。

// 借用上面的指令:

class IteratorContext {
  odd: boolean;
  even: boolean;
  first: boolean;
  last: boolean;
  constructor(public $implicit: any, public index: number, total: number) {
    this.odd = index % 2 == 1;
    this.even = !this.odd;
    this.first = index == 0;
    this.last = index == total - 1;

    // 这里使用定时器改变上面绑定的属性,在页面上会看到这个
    // 属性的变化
    setInterval(() => {
      this.odd = !this.odd;
    }, 2000);
  }
}

六、管道

说明: 管道是用来对数据进行转换的类,转换发生在指令或者组件收到数据之前,管道使用的装饰器是@Pipe,管道默认是纯管道,对于其常用属性如下:

  • name:定义管道的名称
  • pure:当值为true的时候,只有这个管道的输入值或者参数变化的时候,transform函数才会重新执行,这样的管道称为纯管道

1.纯管道

说明: 如果自己来写管道文件,则管道文件的名字的格式为管道名称.pipe.ts

命令

// 完整命令:
ng g pipe 管道名称

// 简写命令:
ng g p 管道名称
// src/app/myc04.pipe.ts

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'myco4'
})

// 关于 implements PipeTransform 这类似的东西:
// 如果是自己手动写指令、组件、管道的文件,都不会存在这个,因为
// 这个不是必须的,它的作用是进行类似检查的功能,就比如说如果这里
// 下面的transform 你写成了 transfrom 的话,这里是会报错的,
export class Myco4Pipe implements PipeTransform {
  transform(value: unknown, ...args: unknown[]): unknown {
    return null;
  }
}

定义

// src/demo.pipe.ts

import { Pipe } from "@angular/core";

// 定义的是管道的话装饰器需要使用Pipe这个,其次导入的地方是从@angular/core
// 这里导入的,而不是从第三方库(例如rxjs)导入的,Pipe这个装饰器函数的参数
// 有一个,就是定义管道的名字,这个名字就是使用在管道符(|)后面的名字
@Pipe({
  name: 'fitterDemo'
})
export class DemoPipe {
  // 管道中,执行过滤任务的是一个固定的函数,这个函数就是transform,可以把
  // 这个函数当做成一个主函数,通过这个函数作扩展的过滤行为,其次,函数的
  // 第一个参数是默认参数,这个参数是不需要传递的,它在进行过滤的时候就会拿到,
  // 值是管道符(|)左边需要进行过滤的值,然后,函数的返回值就是经过过滤后的值,
  // 最后,这个函数的参数可以存在多个,不过在参数对应的时候需要注意一点
  transform(val: any, flag = true) {
    console.log(val, flag)
  }
}

根据管道规范,transform方法的返回值应该是满足所有条件,而不仅仅只是部分条件

// 这样写transform函数就会有问题,当flag传递的是false的时候管道的返回值
// 是undefined,这可能会导致潜在的 bug 或意外的行为
transform(val: any, flag = true) {
    if(flag) {
        console.log(val, flag)
    }
}

注册

// src/app/app.module.ts

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { DemoPipe } from 'src/demo.pipe';

import { Myco1Component } from './myco1/myco1.component';

@NgModule({
  // 管道在使用之前也是需要注册的,注册方式跟组件,指令是一致的,导入 + 注册
  declarations: [
    DemoPipe,
    Myco1Component
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
  ],
  providers: [],
  bootstrap: []
})
export class AppModule { }

使用

说明: 单一管道的使用格式为{{ 数据 | 管道 : 管道参数1 : 管道参数2... }},之后变量和参数都会传递到transform(数据,参数1,参数2...)之中,函数的返回值会替换数据,同时,管道也能组合使用,其格式为{{ 数据 | 管道1 | 管道2... }},然后数据会依次经过管道来过滤

<!-- src/app/app.component.html -->

<app-myco1></app-myco1>
<!-- src/app/myc01/myc01.component.html -->

<input type="text" [(ngModel)]="text">

<!-- 这里传递的false其实对应的是transform函数的第二个参数flag, -->
<!-- 而不是对应第一个参数val -->
<span>{{ text | fitterDemo : false }}</span>
// src/app/myc01/myc01.component.ts

import { Component } from '@angular/core';

@Component({
  selector: 'app-myco1',
  templateUrl: './myco1.component.html',
  styleUrls: ['./myco1.component.less']
})
export class Myco1Component {
  text = 1
}

2.非纯管道

说明: 还有一种管道叫非纯管道,此时存在两种情况,一是angular会让管道拥有自己的状态数据,二是让angular知道管道依赖的数据变化时变更检测无法注意到。此时执行变更检测的时候会将非纯管道视为一个数据源,即使数据值哥参数没有发生变化,也会执行transform方法,所以常常用来处理数字元素发生变更的数组内容,话不多说,看个例子:

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'demo',
  // 如果不是非纯管道下面添加数据页面上的内容是不会变的
  pure: false,
})
export class DemoPipe implements PipeTransform {
  transform(value: number[], ...args: unknown[]): unknown {
    return value.length;
  }
}
<div>{{ data | demo }}</div>

<button (click)="fn()">点击一下,向数组中添加数据</button>
import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.less'],
})
export class AppComponent {
  data = [1, 2, 3, 4, 5];

  fn() {
    this.data.push(1);
  }
}

3.内置管道

说明: 这些管道都是angular已经写好的,直接可以引入拿来用,具体有哪些可以戳此处去文档查看

number

说明: 它使用本地化相关的规则对数值进行格式化,基本使用的语法为:{{ 数值 | number: "整数部分的最少位数(1).小数点后最少位数(0)-小数点后最大位数(3)" : "地区信息"}},格式化的结果会根据地区信息的不同而不同,默认是en-US

<!-- 1 000 -->
<div>{{ 1000 | number : "1.0-0" : "fr" }}</div>

地区信息注册: 在跟模块导入注册就可以了

// 导入需要注册地区的信息
import localFr from '@angular/common/locales/fr';
// 导入注册函数
import { registerLocaleData } from '@angular/common';
// 进行注册
registerLocaleData(localFr);

export class AppModule {}

currency

说明: 用于将number值格式化为货币量,其语法及参数为:{{ number | currency: "货币种类,默认为USD": "code(显示货币代码)/symbol(显示货币符号)/symbol-narrow(当货币存在窄和宽符号时显示其简介形式)": "整数部分的最少位数(1).小数点后最少位数(2)-小数点后最大位数(2)" : "地区信息" }}

<!-- 1 000,00 $CA -->
<div>{{ 1000 | currency : "CAD" : "symbol" : "4.2-2" : "fr" }}</div>

percent

说明: 用于将一个数字格式化为百分比形式,其语法格式为:{{ 数值 | percent: "整数部分的最少位数(1).小数点后最少位数(0)-小数点后最大位数(0)" : "地区信息"}}

<!-- 100 000,000 % -->
<div>{{ 1000 | percent : "4.3-5" : "fr" }}</div>

json

说明: 这是一个非纯管道,用于将数据值变为json格式的数据,其使用了JSON.stringfy方法来创建JSON字符串,它没有参数,其语法格式为{{ data | json }}

slice

说明: 这是一个非纯管道,它可以处理数组/字符串,这个管道的行为跟数组/字符串的slice()的行为是一致的,其语法格式为:{{ array / string | slice: start(表示截取的起始位置) : end(表示截取的终止位置) }}

<!-- 2 -->
<div>{{ [1, 2, 3, 4] | slice : 1 : 2 }}</div>

七、服务与依赖注入

说明: 服务是一种特殊的类,用于封装可复用的功能和数据以供给应用的各个部分共享和重用,其次服务不需要在模块中进行注册,来看下面简单的例子:

1.理解服务

场景导入

说明: 以购物网站为例,与删除商品数据有关的操作是很敏感的,一般的人是没有这个权限的,如果删除了数据的话,这个操作会被记录下来,在开发的过程中,记录这样的信息称为日志,这里简单模拟一下这个行为,比如我添加或者删除数据的时候,记录其操作的相关信息

简单实现

<!-- src/app/myc05/myc05.component.html -->

<h2>商品管理</h2>

<button (click)="doAdd()">添加商品</button>
<button (click)="doDelete()">删除商品</button>
// src/app/myc05/myc05.component.ts

import { Component } from '@angular/core';

@Component({
  selector: 'app-myc05',
  templateUrl: './myc05.component.html',
  styleUrls: ['./myc05.component.less'],
})
export class Myc05Component {
  doAdd() {
    console.log('正在执行数据库的添加');
    let uname = 'admin2';
    let time = new Date().getTime();
    let action = '添加了新的商品:xxxx';
    console.log(`管理员:${uname} 时间:${time} 动作:${action}`);
  }

  doDelete() {
    console.log('正在执行数据库的删除');
    let uname = 'admin3';
    let time = new Date().getTime();
    let action = '删除了新的商品:xxxx';
    console.log(`管理员:${uname} 时间:${time} 动作:${action}`);
  }
}

存在优化

问题: 上面这样写虽然可以实现功能,但是你会发现如果我有很多的地方需要去做日志的话,那么写出来的代码会存在很多相同的内容,不够简洁,并且在后期如果日志需要做出相同的更改,那么需要更改很多地方,这也说明代码的可维护性很差

解决办法:

方案一: 自己把重复的部分提取起来,封装成方法,然后在需要使用的地方进行导入,使用new关键字进行注册,最后调用方法实现功能。

提供方案一

说明: 将日志的公共部分提取出来,并将其封装成一个方法用于调用,这样可以解决前面代码不宜维护的缺点

// src/app/myc06.service.ts

export class Myc06Service {
  // 提供一个做日志的方法,参数为做日志的行为
  doLog(action: any) {
    let uname = 'admin2';
    let time = new Date().getTime();
    console.log(`管理员:${uname} 时间:${time} 动作:${action}`);
  }
}
<!-- src/app/myc05/myc05.component.html -->

<h2>商品管理</h2>

<button (click)="doAdd()">添加商品</button>
<button (click)="doDelete()">删除商品</button>
// src/app/myc05/myc05.component.ts

import { Component } from '@angular/core';
import { Myc06Service } from '../myc06.service';

@Component({
  selector: 'app-myc05',
  templateUrl: './myc05.component.html',
  styleUrls: ['./myc05.component.less'],
})
export class Myc05Component {
  doAdd() {
    console.log('正在执行数据库的添加');
    let action = '添加了新的商品:xxxx';
    new Myc06Service().doLog(action);
  }

  doDelete() {
    console.log('正在执行数据库的删除');
    let action = '删除了新的商品:xxxx';
    new Myc06Service().doLog(action);
  }
}

缺点: 很麻烦,每次都需要自己手动去new一个对象,如果日志做多了一样有点吃不消,这里推荐第二种解决方法

方案二: 省略使用new关键字的操作,直接声明依赖,然后由模块提供相应的内容来达到你的目的,在给出方案二之前,需要先了解一个概念,就是服务

引出服务

概念: 组件是与用户进行交互的一种对象,其中的内容都必须和用户有关,而与用户没有关系的内容和操作都应该剥离出去,放在服务对象里面

关系:

graph TD
服务提供者 -- 依赖注入 --> 组件
组件 -- 声明依赖 --> 服务 
服务 -- 被创建 --> 服务提供者

理解: 首先,在开发过程中,组件可能会产生实现类似日志功能的需求,这种需求会被理解为声明依赖,然后这个功能会存放在服务之中,然后既然有人存在需求,那就需要有人来解决需求,它就是服务提供者,它会收到你的服务需求,将其创建出来,之后将它传递给需要服务的组件,这个过程称为依赖注入

提供方案二(全局服务)

注意: 这种根模块的服务对象只会在应用程序启动时创建,也就是整个应用程序共享同一个服务对象实例,如果需要使用这类服务对象,条件是创建的服务中不存在变化的属性,因为如果多个组件共用这一个实例,使用时会出现数据的问题

// src/app/myc06.service.ts

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

// 1.服务的注册不是使用的servers,而是Injectable,
// 表示所有的服务对象是'可被注入的'
@Injectable({
  // 2.providedIn表示这个服务是由哪一个模块提供的,
  // 默认情况下由主模块AppModule提供,也就是后面的值传root,
  // 这样的服务有点全局的意思,在哪都可以使用
  providedIn: 'root',
})
export class Myc06Service {
  doLog(action: any) {
    let uname = 'admin2';
    let time = new Date().getTime();
    console.log(`管理员:${uname} 时间:${time} 动作:${action}`);
  }
}
<!-- src/app/myc05/myc05.component.html -->

<h2>商品管理</h2>

<button (click)="doAdd()">添加商品</button>
<button (click)="doDelete()">删除商品</button>
// src/app/myc05/myc05.component.ts

import { Component } from '@angular/core';
import { Myc06Service } from '../myc06.service';

@Component({
  selector: 'app-myc05',
  templateUrl: './myc05.component.html',
  styleUrls: ['./myc05.component.less'],
})
export class Myc05Component {
  log = null;
  // 3.声明依赖的话需要在constructor构造函数的参数中进行声明,
  // 这个变量名称没有规定,但是它的类型必须是你创建的服务,
  // 是这个依赖需要的那个服务,比如这里我依赖所需要的服务
  // 是Myc06Service,所以我定义变量的名称的类型也必须是
  // 这个Myc06Service,不然会完不成你想要的任务
  constructor(servers: Myc06Service) {
    // 4.在组件中声明依赖,服务会被服务提供者自动注入
    // 进来,组件直接使用服务对象就可以,这里打印servers
    // 是有值的,使用变量log存储一下是为了方便这个对象
    // 可以在这个类的任意地方都可以使用
    this.log = servers;
  }

  doAdd() {
    console.log('正在执行数据库的添加');
    let action = '添加了新的商品:xxxx';
    // 5.使用服务
    this.log.doLog(action);
  }

  doDelete() {
    console.log('正在执行数据库的删除');
    let action = '删除了新的商品:xxxx';
    this.log.doLog(action);
  }
}

提供方案三(局部服务)

当然,如果需要创建组件自己的服务对象,可以以下面这个例子为例,组件中提供的服务对象是在每个组件实例化时创建的,每个组件都有自己的服务对象实例,这样即使服务中存在数据变量这些内容,它也不会影响组件内部的数据

// src/app/myc06.service.ts

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

// 1.这里的装饰器不需要传递参数对象
@Injectable()
export class Myc06Service {
  doLog(action: any) {
    let uname = 'admin2';
    let time = new Date().getTime();
    console.log(`管理员:${uname} 时间:${time} 动作:${action}`);
  }
}
<!-- src/app/myc05/myc05.component.html -->

<h2>商品管理</h2>

<button (click)="doAdd()">添加商品</button>
<button (click)="doDelete()">删除商品</button>
// src/app/myc05/myc05.component.ts

import { Component } from '@angular/core';
import { Myc06Service } from '../myc06.service';

// 2.在组件的装饰器哪里传递一个参数对象,里面的 providers 
// 就是为当前组件提供服务的地方,可以理解为这个服务只能够在
// 当前组件内部才能够使用,有点局部的味道
@Component({
  selector: 'app-myc05',
  templateUrl: './myc05.component.html',
  styleUrls: ['./myc05.component.less'],
  providers: [ Myc06Service ],
})
export class Myc05Component {
  constructor(servers: Myc06Service) {
    this.log = servers;
  }

  doAdd() {
    console.log('正在执行数据库的添加');
    let action = '添加了新的商品:xxxx';
    this.log.doLog(action);
  }

  doDelete() {
    console.log('正在执行数据库的删除');
    let action = '删除了新的商品:xxxx';
    this.log.doLog(action);
  }
}

方案二和方案三的区别在于创建的服务对象里面是否存在属性,由于方案二可以理解为全局服务对象,方案三理解为局部服务对象,那么服务中存在属性的话,全局的每次都会初始化这个属性或者是更改这个属性,这样数据就会遭到污染,那么方案二也就不再适用了,该用方案三的局部来代替。

创建命令

命令:

// 完整写法
ng g service 服务名称

// 简写形式
ng g s 服务名称

注意事项

  • 服务是通过构造函数所接受的。
  • angular的变更检测无法识别服务内部所存储值得变化,所以在管道中使用的时候,应该使用非纯管道

2.服务提供者

类提供者

说明: 这也是最常用的,它提供服务的方式是使用providers属性,所以在组件和模块中都可以使用,比如下面这样的:第一个服务对象是简写的形式,这里介绍另一种形式也就是第二个服务对象写法的三个属性:provideuseClassmulti

  • provide:用于指定令牌,这个可以理解为给服务起另外一个名字
  • useClass:这个可以理解为当前的这个令牌对应的是哪一个服务的类
  • multi:用于传递一个服务对象数组来解析依赖
@Component({
  providers: [
    服务对象,
    { 
        provide: xxx,
        useClass: xxx,
        multi: xxx
    },
  ],
})

(1)令牌

说明: 所有服务提供者都依赖令牌,它是依赖注入系统标识服务提供者能够解析的依赖,常用的令牌有两种,一种是使用一个类做令牌,一种是使用字符串作为令牌,它们在使用服务对象上的方式有区别,在于使用字符串的令牌需要使用@Inject()来指定。

import { Component, Inject } from '@angular/core';
import { DemoService } from './demo.service';
import { Demo1Service } from './demo1.service';

@Component({
  providers: [{ 
      provide: 'DemoService', 
      useClass: DemoService 
  }, Demo1Service],
})
export class AppComponent {
  constructor(
    @Inject('DemoService')
    private demo: DemoService,
    private demo1: Demo1Service
  ) {}
}

InjectionToken: 当使用字符串作为令牌的时候,可能会遇到应用程序的两个不同部分试图使用同一个令牌来标识不同的服务,这样可能会导致使用错误类型的对象来解析依赖从而导致错误,此时可以使用InjectionToken("字符串")来生成一个唯一的令牌然后使用这个令牌就可以避免这样的错误发生,既然是唯一的也就是不管调用多少次生成的结果都是不一样的,使用的话像下面这样:

export const LOG = new InjectionToken('log');

使用const的原因:

  • 不可变性:使用const声明的常量是不可变的,即它的值在声明后不能被修改。这有助于保护代码中重要的标识符,防止意外的修改导致错误。
  • 作用域:const声明的常量具有块级作用域,在声明它的块中有效。这意味着在其他地方无法更改这个常量的值,从而确保在整个代码中保持一致性。
  • 可读性:通过使用const,我们可以清晰地表明这是一个常量,不会被改变。这可以提高代码的可读性,让其他开发人员更容易理解代码的意图。

(2)multi属性

说明: 如果需要在同一个令牌上面注册一组类似的服务对象在上面,可以将这个属性设置为true,此时使用Inject()获取到的服务就不是单个服务类,而是一个数组了,类似下面这样使用

// 这里定义了一组类似的服务,其中的内容不重要
import { Injectable } from '@angular/core';

@Injectable()
export class DemoService {
  constructor() {}

  fn() {
    console.log('执行了一次');
  }
}

@Injectable()
export class DemoServiceCopy extends DemoService {
  constructor() {
    super();
  }

  override fn() {
    console.log('执行了');
  }
}
import { Component, Inject, InjectionToken } from '@angular/core';
import { DemoService, DemoServiceCopy } from './demo.service';

// 定义一个令牌
export const LOG = new InjectionToken('log');

@Component({
  // 这里给这个令牌注册两个服务上去
  providers: [
    { provide: LOG, useClass: DemoService, multi: true },
    { provide: LOG, useClass: DemoServiceCopy, multi: true },
  ],
})
export class AppComponent {
  // 这里将这个令牌上面的服务获取下来
  service: DemoService[];
  constructor(@Inject(LOG) service: DemoService[]) {
    this.service = service;
  }

  fn() {
    // 打印之后可以看见是一个数组
    console.log(this.service);
  }
}

57KT.png

值提供者

说明: 此时的服务表示的值会很简单,比如是一个对象一个数组什么的,这个值定义在useValue属性上,同时它也拥有providemulti两个属性,用法和类提供者里面的是一样的,这种提供者一般在需要组件自己创建服务对象的时候很有用

// 将上面的服务稍微修改一下
import { Component, Inject, InjectionToken } from '@angular/core';

export const LOG = new InjectionToken('log');

@Component({
  providers: [{ provide: LOG, useValue: [1, 2, 3] }],
})
export class AppComponent {
  service: number[];
  constructor(@Inject(LOG) service: number[]) {
    this.service = service;
  }

  fn() {
    console.log(this.service);
  }
}

NGMVT.png

工厂提供者

说明: 这个跟值提供者类似,只不过工厂提供程序允许您定义一个工厂函数,该函数在需要注入服务时被调用,函数的返回值表示生成服务的实例,显得十分灵活,其中函数定义在useFactory属性上。函数所需要的参数可以使用deps属性传递,这个属性的值是一个令牌数组。同时它也拥有providemulti两个属性,用法跟之前一样,看下面的例子

import { Component, Inject, InjectionToken } from '@angular/core';

export const LOG = new InjectionToken('log');
export const LOG1 = new InjectionToken('log1');

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.less'],
  providers: [
    { provide: LOG, useValue: [1, 2, 3] },
    {
      provide: LOG1,
      deps: [LOG],
      useFactory: (val: number[]) => {
        return val;
      },
    },
  ],
})
export class AppComponent {
  service: number[];
  constructor(@Inject(LOG1) service: number[]) {
    this.service = service;
  }

  fn() {
    console.log(this.service);
  }
}

X.png

使用已有提供者

说明: 那这里就是对已经存在的提供者起另外的名字了,使用useExisting属性接收已经存在的提供者的令牌,然后使用provide属性改名字就可以了,当然它也存在multi属性,用法一样,看下面的例子

import { Component, Inject, InjectionToken } from '@angular/core';

export const LOG = new InjectionToken('log');
export const LOG1 = new InjectionToken('log1');

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.less'],
  providers: [
    { provide: LOG, useValue: [1, 2, 3] },
    // 这里给已经存在的令牌为LOG的提供者改名为LOG1
    { provide: LOG1, useExisting: LOG },
  ],
})
export class AppComponent {
  service: number[];
  constructor(@Inject(LOG1) service: number[]) {
    this.service = service;
  }

  fn() {
    console.log(this.service);
  }
}

3.png

八、组件的生命周期

1.介绍

说明: 介绍的顺序按照执行的顺序来的,使用的话跟vue中是一样的,区别在于触发的时机以及函数的参数不一样。

constructor

说明: 组件的构造函数,在创建组件实例时被调用,通常用于依赖注入。

ngOnChanges

说明: 首先在组件初始化时也会被调用一次,之后每当有输入属性发生变化都会触发一次

函数参数:

  • currentValue: 输入属性现在的值
  • firstChange: 是否在组件初始化的时候调用过,返回值为布尔值
  • previousValue:输入属性以前的值
import { Component, OnChanges } from '@angular/core';

@Component({
  selector: 'app-my-component',
  template: `
    <h1>{{ title }}</h1>
  `
})

export class MyComponent implements OnChanges {
  title: string;

  constructor() {
    this.title = 'My Component';
    console.log('constructor');
  }

  ngOnChanges(changes: SimpleChanges) {
    console.log('ngOnChanges');
  }
}

ngOnInit

说明: 在组件初始化完成后被调用,这个可以理解为vue中的mounted周期

使用场景:初始化组件的属性和数据,确保组件在渲染前已准备好所需的数据。

ngDoCheck

说明: 在每次 Angular 执行变更检测时被调用,在数据绑定发生变化时触发。

使用场景: 用于自定义的变更检测逻辑,主要用于性能优化、检测复杂对象的变化等。

ngAfterContentInit

说明: 在组件内容(例如子组件)初始化完成后被调用。

ngAfterContentChecked

说明: 在每次 Angular 执行内容检测后被调用,用于响应内容变化的操作。

使用场景:该方法在组件内容变更后被调用,可以执行与内容相关的操作,比如更新模板中的投影内容。

ngAfterViewInit

说明: 在组件的视图初始化完成后被调用。

ngAfterViewChecked

说明: 在每次 Angular 执行视图检测后被调用,用于响应视图变化的操作。

使用场景:该方法在视图变更后被调用,可以执行与视图相关的操作,比如处理 DOM 元素、调用第三方库等。

ngOnDestroy

说明: 在组件销毁时被调用。

使用场景:可以进行一些清理操作,如取消订阅,释放资源,避免内存泄漏和性能问题。

九、路由与导航

1.路由的基本使用

注册路由

说明: 应用路由的第一步是注册路由,它是URL和向用户显示的组件之间的映射,在angular16中,默认在app-routing.module.ts这个文件中进行配置,比如下面这个,这里componentpath是路由常用属性,其实还有很多其它属性,如下,最后,路由的顺序十分重要,因为处理路由实例的时候是从前往后处理的。

名称描述
path指定路由路径,路径中如果需要使用/,需要在中间使用,开头和结尾都不行,就像index/index这样使用才行,/indexindex/都是错误用法
component将激活URL与path属性匹配的时候,这个属性指定选中的属性
pathMatch用于确定激活URL与path属性如何匹配,当值为full的时候,需要二者都一模一样才能匹配,当值为prefix的时候,即使URL中存在不属于path的内容的时候,也能匹配成功
redirectTo将浏览器重定向到一个已经激活的,具有不同的URL的路径
children用于指定子路由,子路由可以在嵌套的router-outlet元素中显示附加组件,router-outlet元素包含在活动组件的模板中
outlet用于支持多个outlet元素
resolve用于定义在激活一个路由之前必须完成的工作
canActivate控制何时可以激活一个路由
canActivateChild用于控制合适可以激活一个子路由
canDeactivate控制是否可以停用一个路由
loadChildren用于配置一个仅在需要的情况下加载的模块
canMatch用于控制是否可以加载一个按需加载的模块
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
// 定义了一个组件
import { Myco2Component } from './myco2/myco2.component';

// 此处配置路由规则,每个对象是一条规则
const routes: Routes = [
  {
    path: 'index',
    component: Myco2Component,
  },
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule],
})

// 这里导出来一个路由模块,所以需要这个模块被使用需要根
// 模块的imports属性中注册才可以
export class AppRoutingModule {}

RouterModule的forRoot()

  • 用于在应用程序的主模块中配置主要的路由规则。
  • 仅在应用程序的主模块中调用一次。
  • 会创建并返回一个带有路由配置的RouterModule实例,并将其提供给注入器。

RouterModule的forChild()

  • 用于在其他模块中配置附加的路由规则。
  • 在应用程序的任何模块中都可以调用多次。
  • 会创建并返回一个带有路由配置的RouterModule实例,但不会将其提供给注入器。
  • 根据 forChild 方法调用的顺序,路由规则将按照各个模块的顺序进行处理。

Route与Routes:

  • 前者表示只存在一条路由规则
  • 后者表示一个数组,可以存放多条路由规则

路由出口

说明: 使用router-outlet确定路由切换的页面会显示在哪里,也就是出口了

<!-- app.component.html -->
<!-- 此时在网页中的地址栏中使用/index时页面就会出现这个内容了 -->

<div>路由出口</div>
<router-outlet></router-outlet>

2.导航

声明式导航

说明: 在 HTML 中使用 routerLink 指令创建一个路由链接,它会根据指定的路由路径进行导航。

使用的两种方式:

  • [routerLink]="['路由地址', 参数1,参数2...]"
  • routerLink="路由地址/参数1/参数2..."
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { Myco1Component } from './myco1/myco1.component';
import { RouterModule } from '@angular/router';

let router = [
  {
    path: 'index',
    component: Myco1Component
  }
]

@NgModule({
  declarations: [
    AppComponent,
    Myco1Component,
  ],
  imports: [
    RouterModule.forRoot(router)
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }
<!-- src/app/myco1/myco1.component.html -->

<!-- 这两种写法的跳转结果会存在一些差异 -->
<div routerLink="index">点击进行路由跳转(相对路径)</button>
<button routerLink="/index">点击进行路由跳转(绝对路径)</button>
  • <a routerLink="/home">Home</a>:这种方式指定了完整的绝对路由路径 /home。当用户点击这个链接时,Angular 路由器会导航到根路径下的 home 路由。
  • <a routerLink="home">Home</a>:这种方式指定的是相对路径 home。当用户点击这个链接时,Angular 路由器会根据当前激活路由的上下文进行导航。如果当前激活的路由路径是 /about,那么该链接会导航到 /about/home 路由。

编程式导航

说明: 这里就是使用代码来完成跳转,angular提供Router类来实现需要的效果,下面是这个类中常用的属性和方法:

名称描述
navigated当存在至少一个导航事件的时候,返回值为true,否则为false
url返回活动的url
isActive(url,exact)如果指定的URL是由活动路由定义的,返回值为true,通过指定exact的值,可以确定被指定的URL中的每一个段是不是必须匹配当前的url,如果是,则返回true
events用于监听导航的变化
navigateByUrl(url,exacts)导航访问指定的URL,返回的结果是一个promise,导航成功解析为true,否则为false,表示发生了一个错误,导航被拒绝
navigate(commands,exacts)在导航时使用一个由段组成的数组,exacts对象可以指定URL的变化是否与当前的路由有关,返回的结果是一个promise,导航成功解析为true,否则为false,表示发生了一个错误,导航被拒绝

(1)navigateByUrl

作用: 用于通过指定的 URL 导航到一个新的路由。

参数:

navigateByUrl(url: 'URL地址', extras?: { 
    // (对象)用于设置查询参数
    queryParams: { type: 'electronics' },  
    // (字符串)用于设置 URL 的片段标识符
    fragment: 'details',  
    // (字符串)定义在导航过程中如何处理现有的查询参数。可选值包括
    // `"preserve"`、`"merge"` 或 `"preserveFragment"`。
    queryParamsHandling: 'merge',  
    // (布尔值)指示是否保留现有的片段标识符。
    preserveFragment: true,  
    // (布尔值)指示是否跳过将当前 URL 添加到浏览器的历史记录。
    skipLocationChange: true,  
    // (布尔值)指示是否要替换当前 URL 的历史记录,而不是添加新的 URL 到历史记录。
    replaceUrl: true 
} )

queryParamsHandling 的三个取值:

  • "preserve":该值表示保留现有的查询参数。在导航过程中,新的查询参数将被忽略,仅保留现有的查询参数。例如,如果当前 URL 是 /products?type=electronics,并且你使用 queryParams 设置了新的查询参数 { brand: 'Samsung' },则导航后的 URL 仍然是 /products?type=electronics

  • "merge":该值表示合并新的查询参数与现有的查询参数。在导航过程中,新的查询参数将与现有的查询参数合并。例如,如果当前 URL 是 /products?type=electronics,并且你使用 queryParams 设置了新的查询参数 { brand: 'Samsung' },则导航后的 URL 将会是 /products?type=electronics&brand=Samsung

  • "preserveFragment":该值表示保留现有的片段标识符(URL 中以 # 开头的部分)。在导航过程中,片段标识符将被保留,不会受到新的查询参数的影响。例如,如果当前 URL 是 /products#details,并且你使用 queryParams 设置了新的查询参数 { type: 'electronics' },则导航后的 URL 仍然是 /products#details

使用:

<!-- src/app/myco1/myco1.component.html -->

<button (click)="jump()">点击实现编程式导航</button>
// src/app/myco1/myco1.component.ts

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

@Component({
  selector: 'app-myco1',
  templateUrl: './myco1.component.html',
  styleUrls: ['./myco1.component.less']
})
export class Myco1Component {
  // 只能够在构造函数这里注入服务,普通函数是注入不了的
  constructor(private router: Router) {}

  jump() {
    // 注意:地址有 / 和没有 / 是两码事
    this.router.navigateByUrl('/index')
  }
}

navigateByUrl的参数URL是不能够使用外部的URL的,像https://www.baidu.com/这样,它并不适用,它的参数还是可以理解为定义的路由匹配规则中path的取值

(2)navigate

说明: 他的作用与上面方法的作用一致,区别在于它可以传递路径参数,但是上面哪一种不行

参数:

navigate(['路由地址', 路径参数1, 路径参数2...], {
    // (对象)传递查询参数
    queryParams: queryParams,
    // (字符串)设置 URL 的片段标识符(通常是页面内的锚点)
    fragment: 'section1',
    // (布尔值)决定是否保留现有的查询参数。
    preserveQueryParams: true,
    // (字符串)决定如何处理现有的查询参数
    queryParamsHandling: 'merge',
    // (布尔值)决定是否保留现有的 URL 片段标识符。
    preserveFragment: true,
    // (ActivatedRoute实例)用于指定相对于当前活动路由的导航目标。
    relativeTo: this.activatedRoute,
    // (布尔值)是否替换当前 URL 而不是创建新的浏览历史记录项。
    replaceUrl: false,
    // (对象)用于传递任意的导航状态数据
    state: { data: 'some state data' }
})

使用: 使用的方法跟上面的方法是一致的,只是参数不同而已(以上面的例子为例)

// src/app/myco1/myco1.component.ts

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

@Component({
  selector: 'app-myco1',
  templateUrl: './myco1.component.html',
  styleUrls: ['./myco1.component.less']
})
export class Myco1Component {
  // 只能够在构造函数这里注入服务,普通函数是注入不了的
  constructor(private router: Router) {}

  jump() {
    // 注意:地址有 / 和没有 / 是两码事
    this.router.navigate(['/index'])
  }
}

(3)events属性

说明: 在很多应用程序中,存在一些并不直参与应用程序导航的组件或者指令,但是这些组件或者指令仍然需要知道导航何时发生,此时Router类中的events属性能够有所帮助,它提供了常用的5种事件类型,可以通过过滤来完成相应的操作

事件类型描述
RoutesRecognized路由系统在将URL与一个路径匹配的时候,发送这个事件
NavigationCancel导航过程取消时发送这个事件
NavigationEnd导航过程成功时发送这个事件
NavigationStart导航过程开始时发送这个事件
NavigationError导航过程产生错误时发送这个事件

3.路由设置

信息获取

说明: angular提供一个服务,组件可以接收这个服务来获取路由的详情,这个组件依赖ActivatedRoute,它的返回值是一个ActivatedRouteSnapshot对象,这个对象描述了当前路由的信息,里面有一些常用属性:

名称描述
url返回一个由UrlSegment对象组成的数组,每个UrlSegment对象都描述了能够匹配当前路由的URL中一个单独的值,其中每个对象中的path返回一个包含了段值的字符串,parameters返回参数的一个索引集合
params返回一个Params对象,这个对象保存了每次路由发生变化的URL参数
queryParams返回一个Params对象,这个对象保存了每次路由发生变化的URL查询参数
fragment保存了每次路由变化时的URL片段
import { Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router';

@Component({
  selector: 'app-myco2',
  templateUrl: './myco2.component.html',
  styleUrls: ['./myco2.component.less'],
})
export class Myco2Component {
  constructor(private activeRoute: ActivatedRoute) {
    console.log(this.activeRoute);
  }
}

N%T.png

路径参数

说明:/:参数1:参数2...这样的格式来定义在path路径之后,传值使用/参数1/参数2...的方式传递,这种参数也叫做params参数

定义参数:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { Myco1Component } from './myco1/myco1.component';
import { RouterModule } from '@angular/router';

let router = [
  {
    // 这里给路径为index的路由定义了一个id的路径参数,
    // 如果定义多个的话,注意传值的顺序,避免传错
    path: 'index/:id',
    component: Myco1Component
  }
]

@NgModule({
  declarations: [
    AppComponent,
    Myco1Component,
  ],
  imports: [
    RouterModule.forRoot(router)
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

传递参数:

<!-- 声明式导航 -->
<!-- 这里给上面定义的路径参数id传值为1 -->
<button routerLink="/index/1">
    点击进行路由跳转(绝对路径)
</button>

获取参数:

import { Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router';

@Component({
  selector: 'app-myco2',
  templateUrl: './myco2.component.html',
  styleUrls: ['./myco2.component.less'],
})
export class Myco2Component {
  constructor(private activeRoute: ActivatedRoute) {
    console.log(this.activeRoute.snapshot.params);
  }
}

W5RN.png

通配符与重定向

通配符: 这是一种特殊的路径,使用**表示,它可以匹配任意的url,可以用它来处理错误的访问url,需要注意的是,这种路由规则需要写在最后,因为是从前往后匹配,匹配到就不会往下进行了

重定向: 路由不一定要选择组件,可以将路由当成一个别名,利用这个别名将浏览器重定向到一个不同的URL,重定向使用redirectTo属性指定被重定向的URL,此时pathMatch属性需要从prefix/full中选择一个值

属性作用
prefix配置路由可以匹配那些以指定路径开头的url,并且忽略后面的内容
full匹配的URL需要与定义的path一模一样才可以
// src/app/app.module.ts

import { AppComponent } from './app.component';
import { Myc02Component } from './myc02/myc02.component';
import { Routes, RouterModule } from '@angular/router';


let router: Routes = [
  // 路由重定向:当访问的路径为空的时候,重定向到路径为
  //            index的组件,如果是重定向,那么汽配规则
  //            pathMatch为必填项,其值有两个,一个是  
  //            prefix前缀匹配,也就是开头如果满足匹配
  //            条件就算匹配上了,另一个是完全匹配full,
  //            也就是需要路径完全一致才可以,一般重定向
  //            放在最开头
  {
    path: '',
    redirectTo: 'index',
    pathMatch: 'full',
  },
  {
    path: 'index',
    component: Myc02Component,
  },
  
  // 可以理解为默认展示的页面
  {
    path: '',
    component: Myc02Component,
  },
  
  // 匹配不存在的页面:**在angular中表示匹配所有,也就是
  //                  匹配所有组件,路由一般是满足一个条
  //                  件的匹配就不会继续往下进行,所以这
  //                  个匹配的规则放在最后面,不然所有的
  //                  路径都会匹配到这一个组件
  {
    path: '**',
    component: Myc03Component,
  },
];

@NgModule({
  declarations: [],
  imports: [],
  providers: [],
  bootstrap: [AppComponent],
})
export class AppModule {}

路由嵌套

说明: 路由嵌套也就是子路由,使用这个就可以响应URL的一部分组成内容,此时router-outlet元素嵌入子路由的模板中,从而创建更复杂的内容布局

(1)定义路由

说明: 子路由是使用children属性定义的,可以用一个路由数组为这个属性进行赋值,使用方法也是和之前一样的

// src/app/app.module.ts

import { AppComponent } from './app.component';
import { Myc02Component } from './myc02/myc02.component';
import { Myc03Component } from './myc02/myc03.component';
import { RouterModule } from '@angular/router';

let router = [
  {
    path: '/index',
    component: Myc02Component,
    children: [
        {
          path: 'index',
          component: Myc03Component,
        },
    ]
  },
];

@NgModule({
  declarations: [],
  imports: [
      RouterModule.forRoot(router)
  ],
  providers: [],
  bootstrap: [AppComponent],
})
export class AppModule {}

(2)定义出口

说明: 子路由显示的组件都在一个router-outlet元素中,这个元素需要定义在父元素的模板中

<!-- app.component2.html -->

<!-- 上面是component2组件中存在路由的嵌套, -->
<!-- 所以路由出口放在component2里面 -->
<router-outlet></router-outlet>

(3)路由信息

说明: 这里继续介绍上面的ActivatedRoute这个依赖的几个属性

标题
pathFromRoot它返回一个数组,这个数组代表所有用于匹配当前URL的路由
parent代表选中组件的路由的父路由
firstChild用于匹配当前URL的第一个子路由
children用于匹配当前URL的所有子路由

4.路由守卫

说明: 路由守卫是一种用于保护和控制导航的机制。它允许开发者在路由导航之前和之后执行相应的操作,常用守卫如下:

标题
CanActivate这个属性用于指定能够激活一个路由的守卫
CanActivateChild这个属性用于指定能够激活一个子路由的守卫
CanDeactivate使一个活动的路由失活的守卫
Resolve指定能够推迟路由激活的守卫,直到某个操作执行完毕
CanLoad用于守卫动态加载功能模块的路由

创建命令

// 完整写法
ng generate guard 守卫名称

// 简洁写法
ng g guard 守卫名称

守卫介绍

(1)CanActivate

说明: 用于确定是否允许进入特定路由。它可以通过实现CanActivate接口来创建一个守卫。当导航到某个路由时,Angular会调用CanActivate接口中的canActivate方法。如果该方法返回true,导航将继续;如果返回false,导航将被取消。

import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree, Router } from '@angular/router';
import { Observable } from 'rxjs';

// 可以看出路由守卫的本质是一个服务类
@Injectable({
  providedIn: 'root'
})
export class AuthGuard implements CanActivate {

  constructor(private router: Router) {}

  canActivate(
    // 表示当前路由的快照信息,可以通过它获取路由的参数、
    // 查询参数等。
    route: ActivatedRouteSnapshot,
    // 表示当前路由状态的快照信息,可以通过它获取当前的 URL、
    // 导航方向等。
    state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {

    // 在这里编写守卫的逻辑
    // 如果满足条件,返回 true表示授权通过,导航继续
    
    // 如果不满足条件,返回 false 或者使用 
    // this.router.navigate 方法进行重定向

    return true; 
  }
}
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { HomeComponent } from './home.component';
import { AdminComponent } from './admin.component';
import { AuthGuard } from './auth.guard';

const routes: Routes = [
  { 
    path: '', 
    component: HomeComponent 
  },
  {
    path: 'admin',
    component: AdminComponent,
    // 一个组件可以使用多个守卫来进行授权
    canActivate: [AuthGuard]
  }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

(2)CanActivateChild

说明: 类似于CanActivate,但是它用于确定是否允许进入特定路由的子路由。它通过实现CanActivateChild接口来创建一个守卫。

import { Injectable } from '@angular/core';
import { CanActivateChild, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree, Router } from '@angular/router';
import { Observable } from 'rxjs';

@Injectable()
export class ParentGuard implements CanActivateChild {
  constructor(private router: Router) {}

  canActivateChild(
    // 表示即将激活的子路由的快照信息,
    // 可以通过它获取子路由的参数、查询参数等。
    childRoute: ActivatedRouteSnapshot,
    // 表示当前路由状态的快照信息,可以通过它获取
    // 当前的 URL、导航方向等。
    state: RouterStateSnapshot
  ): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
    // 在这里编写授权逻辑,判断是否允许激活子路由
    // 根据需要可以使用 childRoute 和 state 参数

    const user = // 获取用户信息的逻辑...

    if (user.isAdmin) {
      return true; // 允许激活子路由
    } else {
      return this.router.parseUrl('/forbidden'); // 重定向到禁止访问页面
    }
  }
}
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { ParentGuard } from './parent.guard';

const routes: Routes = [
  {
    path: 'admin',
    // 注意每种路由守卫挂载到组件上面的属性是不一致的
    canActivateChild: [ParentGuard], // 使用 CanActivateChild 守卫
    children: [
      // 子路由配置...
    ]
  },
  // 其他路由配置...
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

(3)CanDeactivate

说明: 用于确定是否允许从当前路由离开。它可以通过实现CanDeactivate接口来创建一个守卫。当要离开当前路由时,Angular会调用CanDeactivate接口中的canDeactivate方法。如果该方法返回true,导航将继续;如果返回false,导航将被取消。

import { Injectable } from '@angular/core';
import { CanDeactivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs';

@Injectable()
export class CanDeactivateGuard implements CanDeactivate<any> {
  canDeactivate(
    // 表示当前要离开的组件实例。
    component: any,
    // 表示当前路由的快照信息。
    currentRoute: ActivatedRouteSnapshot,
    // 表示当前路由状态的快照信息。
    currentState: RouterStateSnapshot,
    // 表示即将导航到的下一个路由状态的快照信息
    // (可选参数,如果它是 undefined,则表示没有下一个路由)。
    nextState?: RouterStateSnapshot
  ): Observable<boolean> | Promise<boolean> | boolean {
    // 在这里编写离开路由的逻辑
    // 根据组件和路由状态判断是否允许离开路由

    if (component.canLeave()) {
      return true; // 允许离开路由
    } else {
      return confirm('确定要离开此页面吗?'); // 弹出确认对话框
    }
  }
}
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { CanDeactivateGuard } from './can-deactivate.guard';
import { MyComponent } from './my.component';

const routes: Routes = [
  {
    path: 'my',
    component: MyComponent,
    canDeactivate: [CanDeactivateGuard], // 使用 CanDeactivate 守卫
  },
  // 其他路由配置...
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

(4)Resolve

说明: 使用 resolve 可以在路由激活之前获取所需的数据,并等待数据可用后再加载路由组件。这样可以确保组件在渲染之前具有所需的数据,以避免在组件中处理数据加载的异步问题。

// 创建一个服务来获取预先需要的数据
import { Injectable } from '@angular/core';
import { Resolve, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs';
import { DataService } from './data.service';

@Injectable()
export class MyResolver implements Resolve<any> {
  constructor(private dataService: DataService) {}

  // route:它包含了当前激活的路由的信息,包括路由
  //        参数、查询参数、路由路径等。
  // state:它表示当前的路由状态,包含了路由树的快照信息,
  //        可以用于获取当前的路由路径、URL 等。
  resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<any> {
    // 在这里通过调用数据服务获取数据
    return this.dataService.getData();
  }
}
// 在路由配置中使用 `resolve`
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HomeComponent } from './home.component';
import { MyResolver } from './my.resolver';

const routes: Routes = [
  {
    path: 'home',
    component: HomeComponent,
    resolve: {
      resolvedData: MyResolver // 使用 MyResolver 获取数据
    }
  },
  // 其他路由配置...
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }
// 在组件中访问预先解析的数据:
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';

@Component({
  ...
})
export class HomeComponent implements OnInit {
  resolvedData: any;

  constructor(private route: ActivatedRoute) {}

  ngOnInit() {
    this.resolvedData = this.route.snapshot.data.resolvedData;
  }
}

(5)CanLoad

说明: 当使用惰性加载(Lazy Loading)模块时,可以使用 CanLoad 守卫来控制是否允许加载该模块。CanLoad 守卫用于在加载惰性模块之前执行某些验证或权限检查

// 创建一个实现了CanLoad接口的守卫类
import { Injectable } from '@angular/core';
import { CanLoad, Route, UrlSegment } from '@angular/router';
import { AuthService } from './auth.service';

@Injectable()
export class MyModuleGuard implements CanLoad {
  constructor(private authService: AuthService) {}

  // route: 表示正在加载的惰性模块的路由配置。
  // segments: 一个数组,包含未处理的 URL 片段对象,
  //           表示正在加载的惰性模块的 URL 片段。
  canLoad(route: Route, segments: UrlSegment[]): Observable<boolean> | Promise<boolean> | boolean {
    // 根据权限检查用户是否可以加载模块
    return this.authService.checkUserPermissions(); 
  }
}
// 在路由配置中使用 CanLoad守卫。
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { MyModuleGuard } from './my-module.guard';

const routes: Routes = [
  {
    path: 'my-module',
    canLoad: [MyModuleGuard], // 使用 MyModuleGuard 来控制是否允许加载模块
    loadChildren: () => import('./my-module/my-module.module').then(m => m.MyModule)
  },
  // 其他路由配置...
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }
// 配置守卫后,可以在AuthService中实现具体的权限检查逻辑。
@Injectable()
export class AuthService {
  // ...

  checkUserPermissions(): Observable<boolean> {
    // 进行具体的权限检查,返回 Observable<boolean> 或 Promise<boolean>
  }
}

十、HTTP请求

说明: angular提供了httpclient类来完成请求操作,这个类存在于HttpClientModule这个模块之中,在使用的时候必须先导入

// 在根模块导入HttpClientModule模块并注册
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { HttpClientModule } from '@angular/common/http';

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule, AppRoutingModule, HttpClientModule],
  providers: [],
  bootstrap: [AppComponent],
})
export class AppModule {}

1.基本使用

说明: 同时,这个类在HttpClientModule模块中是作为服务提供的,所以用起来跟服务的用法是一样,比如下面这样的,对于响应,httpclient会自动将响应转换成一个Observable<类型参数>,这个类型参数就是返回值的类型

import { HttpClient } from '@angular/common/http';
import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.less'],
  providers: [],
})
export class AppComponent {
  constructor(private http: HttpClient) {
    this.http
      .get(
        '/api/sugrec?&prod=pc_his&from=pc_web&json=1&sid=40080_40299_40368_40377_40415_40464_40317_39661_40488_40397_60029_60038_60036&hisdata=%5B%7B%22time%22%3A1677808975%2C%22kw%22%3A%22%E5%8F%91%E7%83%AD%E8%A2%9C%E7%9C%9F%E7%9A%84%E4%BC%9A%E5%8F%91%E7%83%AD%E5%90%97%22%7D%2C%7B%22time%22%3A1677809474%2C%22kw%22%3A%22%E7%99%BE%E9%87%8C%E5%8D%8A%22%7D%2C%7B%22time%22%3A1710219395%2C%22kw%22%3A%22%E4%BB%80%E4%B9%88%E6%98%AFxhr%22%2C%22fq%22%3A4%7D%2C%7B%22time%22%3A1711441444%2C%22kw%22%3A%22qq%E6%B8%B8%E6%88%8F%22%2C%22fq%22%3A2%7D%2C%7B%22time%22%3A1711525288%2C%22kw%22%3A%22angular%22%2C%22fq%22%3A2%7D%5D&_t=1711528615547&req=2&bs=%2F%2F%20%E8%BF%99%E4%B8%AA%E6%98%AF%E5%BD%93%E5%8C%B9%E9%85%8D%E5%88%B0%20%2Fapi%20%E7%9A%84%E6%97%B6%E5%80%99%E4%BC%9A%E5%B0%86%E5%85%B6%E5%8E%BB%E9%99%A4%E6%8E%89&pbs=%2F%2F%20%E8%BF%99%E4%B8%AA%E6%98%AF%E5%BD%93%E5%8C%B9%E9%85%8D%E5%88%B0%20%2Fapi%20%E7%9A%84%E6%97%B6%E5%80%99%E4%BC%9A%E5%B0%86%E5%85%B6%E5%8E%BB%E9%99%A4%E6%8E%89&sc=eb&csor=0'
      ).subscribe();
  }
}

在没有使用subscribe()方法之前,请求是不会发送给服务器的,并且每次调用一次这个方法都会发送一次请求,所以小心同一个请求无意间会发送多次

跨域: 这里向百度发送请求,所以会存在跨域问题,angular中解决跨域问题需要在angular.json文件中添加跨域的配置文件,这个文件一般叫做proxy.conf.json,这个文件就是配置webpack了,同时这个文件一般存在于根目录下面,配置的内容可以简单参考下面的:

// angular.json
{
  "projects": {
    "example": {
      "architect": {
        "serve": {
          "options": { 
              // 此处指定配置文件的位置
              "proxyConfig": "./proxy.conf.json"
          }
        }
      }
    }
  }
}
// proxy.conf.json
{
  // 使用正则进行匹配,如果匹配到的 /api 这个路径,那么
  // 它就会将请求转接到 下面的 target 路径下面,但此时
  // 路径中 还是会存在 /api 这个字符串,它会影响到转接的
  // 效果,所以需要使用 pathRewrite 路径重写将其去除掉
  "/api": {
    "target": "https://www.baidu.com",
    "secure": false,
    "changeOrigin": true,
    // 这个是当匹配到 /api 的时候会将其去除掉
    "pathRewrite": {
      "^/api": ""
    }
  }
}

十一、动画

说明: 动画系统能够以动画的方式显示html元素,从而反映应用程序状态的变化,在程序中动画充当两个角色,一个是强调内容的变化,二是平滑的显示内容,下面看一个定义好的动画,以此来说明:

动画准则:

  • 动画需要针对应用程序的主要任务或者主要的工作流
  • 在开发的过程中不要禁用动画
import {
  animate,
  state,
  style,
  transition,
  trigger,
} from '@angular/animations';

export const HightlightTrigger = trigger('Highlight', [
  state(
    'selected',
    style({
      backgroundColor: 'red',
    })
  ),

  state(
    'notselected',
    style({
      backgroundColor: 'yellow',
    })
  ),

  transition('notselected => selected', animate('200ms')),
  transition('selected => notselected', animate('400ms')),
]);

1.基本使用

启用模块

说明: 动画模块存在于@angular/platform-browser/animations模块中的BrowserAnimationsModule里面

// app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';

// 此处导入动画模块
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';

@NgModule({
  declarations: [AppComponent],
  imports: [
    BrowserModule,
    AppRoutingModule,
    AppRoutingModule,
    BrowserAnimationsModule,
  ],
  providers: [],
  bootstrap: [AppComponent],
})
export class AppModule {}

定义样式

说明: 动画系统的核心是样式组,这是一组被应用于一个HTML元素的css样式属性和样式值,样式组是使用style函数定义的,它接受一个JavaScript对象字面量,从而提供属性名称和属性值之间的映射,就像上面动画的这部分

state(
    'selected',
    style({
        backgroundColor: 'red',
    })
),

state(
    'notselected',
    style({
        backgroundColor: 'yellow',
    })
),

样式的写法:

  • 驼峰式: 就像backgroundColor这样,首单词字母小写,其他单词首字母大写,中间没有连字符
  • css写法: 就是写css的时候怎么写就怎么写,只不过写完需要写在""里面,就像"background-color"这样

定义状态

说明: 上面的每一组样式都给定了一个名字,像selectednotselected,这个就是定义的状态,通过这个可以让angular知道何时为元素应用一组样式

定义状态过渡

说明: 此时需要使用transition函数来指定,这个函数有两个参数,第一个参数告知这个指令被应用于哪些状态,是一个带箭头的字符串,第二个参数表示状态改变的时候应该执行什么动作

关于状态箭头:

  • =>:表示从一个状态到另一个状态的单向过渡
  • <=>:表示从一个状态到另一个状态的双向过渡
// 表示状态从 notselected 到 selected,时间为200ms
transition('notselected => selected', animate('200ms')),

// 表示状态从 selected 到 notselected,时间为200ms
transition('selected => notselected', animate('400ms')),

定义触发器

说明: 动画的触发器封装了元素状态和过渡状态,并且指派了一个名称,使用这个触发器可以在一个组件中应用动画,触发器使用trigger("触发器名称",[状态和过渡数组])函数定义。

应用动画

说明: 首先需要使用animations属性来注册触发器,之后就可以在组件中以[@触发器名称]="表达式"来使用,表达式的返回值需要是定义的状态名称,否则就不能得到自己想要的效果了

<button (click)="fn()">点击切换</button>
<div [@Highlight]="current">这是颜色块</div>
import { Component } from '@angular/core';
import { HightlightTrigger } from './demo.animations';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.less'],
  animations: [HightlightTrigger],
})
export class AppComponent {
  current: string = 'selected';

  fn() {
    this.current = this.current === 'selected' ? 'notselected' : 'selected';
  }
}

2.内置动画状态

说明: 动画状态用于定义一个动画的最终状态,可以对应用于一个元素的样式进行分组,angular提供两个内置的动画状态,以便管理元素的外观,如下

状态信息描述
*是一个回退状态,当元素处于的状态不属于触发器所定义的状态时,它就是会退的状态
void当元素不属于模板的任何一部分时,元素处于void状态,也就是在元素消失的这个过程中会触发所定义的动画,一般结合*ngif来使用

为内置状态创建过渡

说明: 会退状态可以表示任何状态,所以可以结合void状态,以动画的形式表示元素添加和删除的过程,看下面例子:

<button (click)="fn()">点击切换</button>
<div [@Highlight]="current" *ngIf="show">使用触发器的颜色块</div>
import { Component } from '@angular/core';
import { HightlightTrigger } from './demo.animations';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.less'],
  animations: [HightlightTrigger],
})
export class AppComponent {
  current: string = 'notselected';
  show = true;

  // 在点击切换的时候会将颜色块在 显示/隐藏 两种状态间进行切换,
  // 此时在从 显示 => 隐藏 的过程中会触发 void状态下的动画
  fn() {
    this.show = !this.show;
  }
}
import {
  animate,
  state,
  style,
  transition,
  trigger,
} from '@angular/animations';

export const HightlightTrigger = trigger('Highlight', [
  state(
    'selected',
    style({
      backgroundColor: 'red',
    })
  ),

  state(
    'notselected',
    style({
      backgroundColor: 'yellow',
    })
  ),

  state(
    'void',
    style({
      opacity: 0,
    })
  ),

  // 这里表示从 隐藏 => 显示 的动画的过渡时间是2000
  transition('void => *', animate('2000ms')),
]);

animate

说明: 使用animate()函数可以控制动画的过渡,最基础的用法就是传递一个字符串参数,用于指定两个状态之间发生过渡所需要的事件,这个时间的单位可以是s,也可以是ms,但是在一个项目中经量使用同一个单位

(1)设置初始延迟

说明: 当传递两个时间给函数的时候,第二个时间会作为延迟设置,在执行多个动画过渡的时候,延迟可以用来暂停动画,

// 这个动画会延迟 2s 才开始
transition('void => *', animate('2000ms 2000ms')),

(2)指定定时函数

说明: 在字符串参数中可以使用定时函数来控制动画的变化速度,其取值如下:

函数名称作用
linear默认取值是这个,此时动画变化的速度是匀速的
ease-in此时动画变化的速度先比较慢,然后越来越快
ease-out此时动画变化的速度先比较快,然后越来越慢
ease-in-out此时动画变化的速度先比较快,然后减速到中间时刻,之后再次加速直到动画结束
cubic-bezier使用贝塞尔曲线创建中间值
transition('void => *', animate('2000ms 1000ms ease-out')),

(3)过渡时添加样式

说明: 函数的第二个参数可以接收一组包裹在style()中的样式,这些样式会在动画的持续时间里面逐渐应用在宿主元素上面

// 此时从元素 隐藏 => 显示 过程中会将字体大小设置为20px,颜色为红色
transition(
    'void => *',
    animate('200ms', style({ fontSize: '20px', color: 'red' }))
),