前言
Angular 是一个流行的开源前端框架,用于构建现代化的、单页面 Web 应用程序和动态 Web 页面。最开始由一个小的团队将其开发,后被Google收购,之后Google负责后期的开发和维护,并被广泛使用在许多大型的企业级应用程序中,我在这里的话分享一些经常使用的,所以肯定没有官方文档那样全面,如有我没分享到的,可以去官方文档查看,在正片开始前,先了解一点其他的内容
尤雨溪与Vue:
最开始的
Angular
是叫做AngularJs
是用js来开发,后来被谷歌收购,由于其很复杂,很难理解,谷歌决定将其框架源码进行简化,成为2.x
的版本,此时就不叫AngularJs
了,而叫Angular
,在简化期间,其开发团队的一位成员尤雨溪
还是觉得2.x
还是很复杂,想进一步简化,但是没有得到公司同意,于是自己将其简化,得到的产物就是最初始的Vue
并将其开源免费,因此它也失去了在谷歌的工作,后来阿里巴巴向其发出邀请,希望他继续开发Vue
,但他拒绝了,因为他想Vue
是一个开源免费的,后其维护的工作一直由社区进行,直到现在....
注意:本文根据代码所实现的效果可能跟使用的
angular版本
不一致和文件夹的层次
不同而得到不同的效果
一、设计原则
说明: 在正式开始之前我们先来了解几个写代码时应该注意的常用原则,分别是YAGNI
、DRY
、OCP
、Low Coupling, High Cohesion
、Dimeter Law
这五个,当你在开发的过程中都没有违背这些法则的话,你会发现后面自己的代码维护起来要容易的多
1.YAGNI
说明: YAGNI原则是敏捷软件开发中的一项原则,它强调只关注当前需求,而不去实现未来可能需要但尚未具体需求的功能。该原则的核心思想是避免过度设计和开发不必要的功能,以减少开发时间和复杂性。
理念: 在软件开发过程中,应该专注于解决当前存在的业务需求,而不去考虑将来可能出现的需求。因为未来的需求很难预测和确定,可能在实际使用中变化、增加或者完全被抛弃。因此,将精力放在当前确切的需求上,可以减少不必要的开发工作,并避免浪费时间和资源。
注意: YAGNI 原则并不是说不考虑未来的需求,而是在当前需求满足后再考虑未来的需求。当确实需要未来的功能时,可以通过重构、扩展和迭代的方式逐步引入,而不是过早地进行预先设计和开发。这样可以更好地应对变化、降低风险,并且按需添加功能,使软件更具灵活性和可维护性。
2.DRY
说明: 该原则认为,相同的代码片段不应该在系统中出现多次,而应该封装成可重用的模块或函数。
好处:
- 可维护性:当需要修改某个功能或修复Bug时,只需要在封装好的代码位置做一次修改,而不是在多个重复的地方都进行修改,从而减少了出错的机会和工作量。
- 可读性:通过封装重复代码,使代码更加简洁、清晰,避免了重复的冗余代码,提高了代码的可读性。
- 可扩展性:通过将通用的功能封装成可重用的模块,可以更方便地扩展应用程序,以满足未来的需求。
注意: DRY 原则并不是要求完全避免重复,因为某些情况下重复可能是合理的。例如,相同的逻辑在不同的上下文中可能有不同的实现方式。在这种情况下,DRY 原则的重点是避免无谓的重复,尽量找到合适的抽象和封装方式。
3.OCP
原则: 对于修改封闭,对于扩展开放
好处:
- 可维护性:通过遵循 OCP 原则,不修改现有代码,只扩展功能,减少了对现有代码的依赖和可能的破坏,增强了代码的稳定性和可维护性。
- 可扩展性:通过扩展接口和实现类,系统的功能可以方便地进行扩展和变化,而不会影响到其他部分的代码和功能。
- 可复用性:通过抽象接口的使用,可以提高代码的复用性,因为不同的实现可以在不同的上下文中重复使用。
注意: OCP 原则并不是说完全避免修改已有代码,而是尽量减少对已有代码的修改,并通过接口和抽象来隔离变化。这样能够使软件系统更加灵活、可扩展,并更好地应对变化需求。
4.Low Coupling, High Cohesion
目的: 提高系统的可维护性、可重用性和扩展性
说明: Low Coupling
指的是模块之间的相互依赖性应尽可能低。模块之间的耦合度越低,它们之间的关系越独立,就越容易修改和替换其中的一个模块,而不会对其他模块产生影响。低耦合度可以减少系统中的依赖关系,增强模块的灵活性和可重用性。High Cohesion
指的是模块内部的元素之间紧密相关,共同完成一个任务或功能。高内聚的模块具有清晰的职责,遵循单一责任原则,每个模块专注于自己的功能,并且不涉及与其他模块无关的任务。高内聚度可以提高模块的可读性、可维护性和重用性,因为模块内部的元素具有逻辑上的紧密连接。
注意: 低耦合和高内聚是相辅相成的。低耦合度和高内聚度使得软件系统的各个模块更加独立和集中,使得系统的扩展和修改更加容易,并且减少了变更对其他模块的影响,通过实践低耦合和高内聚原则,可以提高系统的稳定性、可维护性和可扩展性。这些原则在面向对象设计和模块化开发中都具有重要的指导意义,可以帮助开发人员构建高质量的软件系统。
5.Dimeter Law
核心: 一个模块(类、对象等)应当尽可能少地依赖其他模块。具体而言,一个对象应当仅与其直接的朋友交互,而不与朋友的朋友交互。
对象的方法限制:
对象本身的方法。
通过参数传递给方法的对象的方法。
作为该对象实例变量的直接属性的方法。
好处:降低耦合度:模块之间的耦合度低,修改一个模块不会对其他模块造成过多的影响。
提高可维护性:模块更加独立,易于理解和修改,减少代码的相互依赖。
增强可测试性:模块之间解耦,易于进行单元测试和集成测试。
二、项目的初始化
说明: Angular
的项目初始化的工具是Angular CLI,使用前需要先安装,不过在安装之前需要先查看自己node
的版本,需要LTS
稳定的版本,因为使用LTS
版本是为了确保稳定性、兼容性和安全性,以提供一个可靠的开发环境和更长期的支持,具体的下载方法可以参考官网也可以参考我下面的内容。
1.安装angular cli
js
// 先检查自己node的版本,看与官网的LTS版本是否相同,推荐使用nvm
// 这个在我前面所分享的node一文中存在
node -v
// node安装完毕之后你就可以使用npm包管理工具了,推荐使用yarn,这个在
// node一文中也提到过,这里查看是否存在包管理工具npm
npm -v
js
// 全局安装angular cli,方便自己下一次初始化项目的时候就不用再去安装了
npm install -g @angular/cli
2.初始化项目
说明: angular
初始化项目的命令是ng new
后面加上项目的名字来初始化一个项目,在初始化的过程中,Angular CLI 会安装必要的 Angular npm 包和其它依赖包,花费的时间可能会久一点,毕竟angular
的实现也借助了其它的框架
js
// 这里初始化一个demo的项目
ng new demo
vue cli
初始化项目和angular cli
初始化项目类似,都是以问答的方式产生的,在angular
中,你可能会遇到以下几个问题
js
// 问题一:就是你想你这个项目中与文件有关的内容能够与它们分享嘛
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
js
// 问题二:是否使用angular中的路由
Would you like to add Angular routing?
js
// 问题三:css的预编译工具使用哪一个
Which stylesheet format would you like to use?
3.项目的运行
说明: 在包都安装完毕之后可以使用cd 项目名称
这个命令进入到该项目文件夹,之后执行code .
就会在编辑器中打开这个项目
js
// -open会自动的打开浏览器,一般默认的端口号是 4200
ng serve --open
4.项目结构的介绍
说明: 这里介绍一下在项目初始化中常出现的文件,当然angular
中的配置文件很多,具体可以点击此处去官方文档查看
js
// 某个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 项目的配置文件,包含了构建、部署和其他项目配置选项。
关于样式,
angular
与vue
不同在于如果你angular
中的样式是像我下面那样写的话,每个组件的样式都是自己独立的,不会与其它组件的样式混合在一起,而vue
则会出现样式混合的问题,但可以通过在style
上面加上scoped
来解决这个问题,如果需要任何组件都可以使用的样式,那么就可以在styles.scss
中去设置
5.项目启动的过程
说明: 看Angular
项目是如何启动运行的,需要从angular.json
配置文件来入手,在我看来,它和vue
的vue.config.js
配置文件的作用是一致的,然后在angular.json
文件中可以找到一个index
配置项和main
的配置项,它们分别是是HTML
的入口以及JavaScript
的入口,不过angular2.x
以后,都是用TypeScript
来编写的,以至于后缀均是.ts
json
// angular.json
"options": {
"index": "src/index.html",
"main": "src/main.ts",
},
html
<!-- index配置项所指向的文件(默认) -->
<!doctype html>
<!-- en表示展示的内容英文,中文用zh替换 -->
<html lang="en">
<body>
<!-- 这里会发现一个自定义的组件,Angular推荐大家: -->
<!-- 自定义组件的时候加上自己的前缀,以此区分是组件还是标签 -->
<!-- 其次看到 app- 前缀,均有angular提供 -->
<app-root></app-root>
</body>
</html>
ts
// main.ts
import { AppModule } from './app/app.module';
// bootstrapModule表示引导启动的意思,会启动一个AppModule
// 的模块
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.error(err));
ts
// app.module.ts
import { AppComponent } from './app.component';
@NgModule({
// 从上面提示的文件看下来看到这里的bootstrap引导启动
// 一个AppComponent的组件
bootstrap: [AppComponent]
})
ts
// 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
的项目是由一个个模块组成的,但是这个模块并不是ES
模块,初始状况下,一个Angular
项目只存在一个主模块NgMoudle
,这个模块有且仅有一个,它用于启动应用程序,模块就是一组相关功能的集合,专注于某个应用领域,它可以将组件和一组相关的应用关联起来,是应用组织代码的一种方式
这里需要注意的是:组件必须存放在模块里面,书写在Src文件夹之中,并且程序员所写的代码必须存放在app文件夹下面
1.NgMoudle
四、组件
说明: vue
中的组件跟angular
中的组件是一致的,都会表示一段可以重复使用的页面片段,在vue
中,一个组件分为三部分:模板
、脚本
和样式
,在angular
中也是如此,只不过书写的方式会有所区别,但是不管怎样写,一定注意上面的注意事项
1.自定义组件(基础版)
说明: vue
跟angular
是类似的,一个组件需要主体部分,而vue
是单文件组件,只需要一个.vue
格式的文件就足够,但是在angular
中规定,一个组件就是一个class
,而angular
是基于ts开发的,那么一个组件就是一个.ts
文件,并且组件是需要供别人使用,从而组件的这一个.ts
的文件就是一个ES
的模块,若需要供它人使用需要将其导出,其次,单纯一个class
,别人会不知道它是用来干嘛的,这时候需要借助装饰器
来起到类似注视的作用。
创建组件class
js
// 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 {}
如果将一个组件的三部分
ts
、template
、style
拆分出去的话,由于你写的文件都存放在app目录下面,那么项目一旦大起来,文件多起来,如果文件命名不是很合理,你可能找一个组件会找半天,甚至找不到都有可能,所以在写文件名的时候推荐带上它是什么部分,举个例子,上面我写了一个myc01.ts
的文件,别人看的话会不知道它是组件还是模块,但是我知道它是组件,所以我可以将其写成myc01.component.ts
,这样我在后面看的时候就知道这个文件是组件还是模块了,其它的以此类推
明白了上面,那既然是一个大型项目,组件肯定会有很多,如果一个组件拆分成三个文件放在app文件夹下,那么100个组件就有300个文件了,到时候你肯定人都麻了,所以每一个组件的三部分可以放在一个文件夹里面,文件夹名推荐为组件的名字,这些操作都是为了你后期能够更好的去维护自己的代码
在模块中注册组件
说明: 由于是刚初始化的一个项目文件,当然模块就只有主模块,也就是app.module.ts
这个文件
js
// 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: [
AppComponent,
Myc01 // 进行注册,注册的名字就是引入的类名
],
imports: [
BrowserModule,
AppRoutingModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
组件的使用
说明: 在vue
中,自己定义的组件是在项目创建时创建的一个APP
组件中使用的,在angular
中也不例外,在项目初始化的时候会生成一个app-root
的组件,所以就在这里使用。
js
// src/app/app.component.html
// 标签名就是在selector中的内容
<myc01></myc01>
注意:在
angular
中,禁 止在入口文件src/index.html文件中去使用自定义组件,别问为什么,因为你在vue
中也不允许这么干
使用命令创建组件
说明: 看到这里是不是觉得angular
创建组件很麻烦,需要建文件夹,建文件然后注册,其实是挺麻烦的,但是官方想到了这一点,它给了我们一个命令ng generate component 组件名
来快速创建组件,其可以简写成ng g component 组件名
,ng
命令来自哪里呢,在你安装好angular cli
的时候,ng.cmd
命令就已经存在了,如果你没有这个命令也没有关系,在你初始化好的项目中,会有一个node_modules
文件夹,这个文件夹中有一个.bin
的可执行命令的文件夹,里面存在一个ng
的命令,其实在安装好的node文件夹里面,你会看到一个npm.cmd的命令,它是用于执行第三方模块的工具的,你还会找到一个npx.cmd的命令,这个适用于执行node_modules中.bin文件夹下面的命令的
,所以你可以使用npx ng g component 组件名
这个命令来创建组件
js
// 安装好angular cli:
ng generate component 组件名
ng g component 组件名
js
// 没有安装或安装失败angular cli:
npx ng generate component 组件名
npx ng g component 组件名
在安装成功之后,它会创建一个以组件名为名字的文件夹,在文件夹中会分别创建
.ts
、.html
、.css
的文件,并会更新主模块中的内容,将创建的组件注册进去
警告
说明: 上面说到过每一个组件就是一个函数,那么如果在组件自己的模板中使用组件自己,其等价于在函数中调用函数本身,这样搞会形成递归调用,最后浏览器就挂掉了
五、指令
说明: 由于angular
中的指令非常多,这里就分享几个最常用的,也就是在vue
中常用的那几个
angular指令的分类:
1.结构型指令(
*开头
):会影响DOM树的结构2.属性型指令(
用[]
):只会影响元素的外观或者行为3.组件指令:组件是一种特殊的指令
1.HTML数据绑定
说明: vue
中的双括号语法{{ }}
就是借鉴于angular
中的ng表达式
,也叫做大胡子语法
,都是用{{ }}
来表示,使用方法都差不多,下面列举一些常用常用的写法,后面如果我学到有特殊的写法我会列举出来
js
// 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;
}
html
<!-- 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>
不适用{{}}的情况:
1.new关键字,因为new关键字在
angular
中是禁止使用的2.JSON的方法,因为JSON在插值语法中是undefined
2.属性绑定
说明: 在angular
中,属性绑定有两种写法,一种是直接在属性值上面直接使用插值语法{{ }}
,一种是使用[]
将属性值包裹起来,这个就相当于vue
中的v-bind
html
<!-- src/app/myc03/myc03.component.html -->
<!-- 绑定的属性是变量的写法: -->
<!-- 在鼠标移动上去之后都会显示出:这是我写的一段文字 -->
<p title ="{{ name }}">你觉得这个是不是我写的</p>
<p [title] ="name">这个呢</p>
html
<!-- src/app/myc03/myc03.component.html -->
<!-- 绑定的属性存在常量的写法:需要用到字符串的拼接,注意单双引号交替 -->
<p [title] ="'你是谁' + Name">这个呢</p>
html
<!-- src/app/myc03/myc03.component.html -->
<!-- 然后我试了一下能不能在这个地方能不能使用字符串模板,答案是不行 -->
<p [title] ="`你是谁${Name}`">这个呢</p>
注意一下下面这种情况:
html
<!-- src/app/myc03/myc03.component.html -->
<!-- 在使用图片的时候,由于图片都会存放在一个文件夹下,那么引入图片的 -->
<!-- 路径开头都是一致的,比如说:'../assets/' + '图片名称'这种格式的 -->
<!-- 那么在使用的时候就可以只将图片名称定义为变量,因为后台可能也不知 -->
<!-- 道你图片会放在什么位置,那能够确定的就只有图片的名称,所以这样写 -->
<!-- 方便了你,也方便了别人,就像下面这样: -->
<img [src] ="'../assets/' + 'imgUrl'">图片路径的拼接</img>
ts
// 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'
}
3.事件绑定
说明: angular
中事件的绑定并不会像vue
那样使用@事件名
,而是使用(事件名)
来代替,其次,事件名不需要像vue
那样加上on
,直接像(click)
就好,这个就相当于vue
中的v-on
html
<!-- src/app/myc03/myc03.component.html -->
<button (click)="onSave()">Save</button>
ts
// 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的第一个事件')
}
}
这里有一点需要注意的是,
angular
中绑定的事件函数后面是需要加上括号的,不然会检测不到这个事件导致其失效
4.循环
说明: angular
中循环的指令是*ngFor
,后面跟一个循环的式子,与vue
的v-for
相比,我觉得还是稍微复杂点
html
<!-- src/app/myc03/myc03.component.html -->
<ul>
<li *ngFor="let item of data">{{ item }}</li>
<li *ngFor="let item of data; index as i">{{ i }}</li>
<li *ngFor="let item of data; let i = index">{{ i }}</li>
</ul>
注意:
1.上面式子中只要你需要声明变量,你就必须使用
let
关键字2.
*ngFor
这个指令循环只能使用of
,不可以使用in
3.如果需要取索引它是不允许你直接使用
index
,需要你用变量重写4.重写的方式的话推荐上面的两种写法
ts
// 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 {
data = ['数据一', '数据二', '数据三'];
}
5.条件
说明: 这里介绍的指令是*ngIf
,它的后面可以是一个处理函数,可以是一个表达式,可以是一个值等等,但是前提是能够得到布尔值
,最后就是vue
中v-if
跟这个指令差不多,也就是显示隐藏的实质是是否将元素从dom中删除
html
<!-- src/app/myc03/myc03.component.html -->
<!-- 这样在页面上你就看不见任何元素了,在dom树中你也看不见 -->
<ul *ngIf="true">这里展示与v-if相类似的功能</ul>
注意这两种写法:
*ngIf
会销毁和创建元素,[ngIf]
只是添加或移除元素的属性
说明: 既然vue
中存在v-if
和v-else
,那么angular
也会存在自己的v-else
!
html
<!-- src/app/myc03/myc03.component.html -->
<ul *ngIf="false; else elseBlock">这里展示与v-if相类似的功能</ul>
<ng-template #elseBlock>
<p>这里展示与v-else相类似的功能</p>
</ng-template>
注意:
1.else使用的格式是
*ngIf="布尔值表达式; else else展示的内容"
2.else展示的内容是通过
#
来进行关联的3.else展示的内容和if中间是不允许有其他代码的
4.else展示的部分必须使用
ng-template
,这个标签是一个容器,这个容器不会在dom结构中生成,但是它的内容可以
6.样式
说明: 在angular
中控制样式有两种方式,一种是控制少量样式使用[ngStyle]="样式对象"
的方法,这种方法虽然可行但是不太稳妥,一般推荐另外一种方法就是[ngClass]="class配置对象"
html
<!-- src/app/myc03/myc03.component.html -->
<!-- 样式绑定的是使用ngStyle -->
<p [ngStyle]="styleControl">这里使用style来控制样式</p>
<!-- 方法一定要加(),否则方法不会执行 -->
<button (click)="toggle()">更改样式</button>
ts
// 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
来控制样式
html
<!-- src/app/myc03/myc03.component.html -->
<!-- 样式绑定的是使用ngClass -->
<p [ngClass]="classControl">这里使用class来控制样式</p>
<!-- 方法一定要加(),否则方法不会执行 -->
<button (click)="toggle()">更改样式</button>
less
// src/app/myc03/myc03.component.less
.tColor {
background-color: red;
}
ts
// 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;
}
}
7.双向绑定
前提: angular
中的指令实质上是一个个的class
,它们按照功能会分别封装在不同的模块里面,想要使用必须导入才行,导入的话在主模块app.moudle.ts
的@NgMoudle
里面的imports
中导入,就像上面介绍的6个指令,它们都是存放在CommonModule
这个模块之中,如果没有导入模块你就不能够使用,其次,双向绑定使用的是([ngModel]),这个指令存放在FormsModule模块中,这个模块存放在@angular/forms里面
js
// 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 {}
html
// src/app/myc03/myc03.component.html
<input type="text" [(ngModel)]="pname">
<p>pname的值是: {{ pname }}</p>
ts
// 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)]
来表示双向数据绑定了
html
// src/app/myc03/myc03.component.html
<input type="text" [value]="pname" (input)="changeValue($event)">
<p>pname的值是: {{ pname }}</p>
ts
// 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:可以理解为作用一样但长相有区别
1.
$event
是Angular模板中用于访问事件对象的特殊变量2.
event
是JavaScript中的普通事件对象,可以在组件类中直接使用。
8.自定义指令
命令:
ts
// 完整写法
ng g directive 指令名称
// 简写形式
ng g d 指令名称
默认生成的内容及使用:
html
<!-- src/app/myc03/myc03.component.html -->
<!-- 指令是作为元素的属性来使用的 -->
<input type="text" appFirstDirective>
ts
// src/app/first-directive.directive.ts
import { Directive } from '@angular/core';
// 命名的是指令,所以这里不用component
@Directive({
// 前面的指令在使用的时候可以看出,指令是作为元素的属性来使用的,
// 所以在命名的时候需要使用到[],其次指令默认属性只有selector,
// 跟组件相比,它没有样式style和模板template,根据对象间的父子
// 判定,指令是父,组件是子,因此组件是一种特殊的指令
selector: '[appFirstDirective]'
})
export class FirstDirectiveDirective {
// 每一次调用指令都会执行一次这个constructor这个构造函数,函数的参数
// 可以获取到是那个元素使用了这个指令,获得的结果是一个配置对象
constructor(el: ElementRef) {
// 这里就可以拿到上面input的信息了
console.log(el)
}
}
ts
// src/app/app.model.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FirstDirectiveDirective } from './first-directive.directive';
@NgModule({
// 既然组件是默认的指令,那么指令的注册方式肯定也是一致的
declarations: [FirstDirectiveDirective],
imports: [
BrowserModule,
],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {}
六、管道
说明: 在最开始是叫过滤器的,只不过到angular2.x
的时候就改名叫做管道(Pipe
)了,vue
中使用的过滤器filter
跟这个是一模一样的,至于怎么理解管道呢,可以理解为它是一种将多个处理函数组织起来的方式,可以有序地执行多个任务,而普通的处理函数更加独立,只负责完成单一的功能。
1.自定义管道
说明: 一个管道可以理解它是一个class,所以在使用的时候需要创建一个.ts
的文件,对于文件的命名,由于定义的组件文件的名字是组件名称.component.ts
这种格式,以此类推,定义的管道文件的名字可以写成管道名称.pipe.ts
这种格式
定义
ts
// 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)
}
}
注意:
根据 Angular 的管道规范,
transform
方法应该在所有可能的代码路径上都返回一个有效的值,而不仅仅是在某些分支或条件下返回值,简单来讲就是transform
方法的返回值应该是满足所有条件,而不仅仅只是部分条件
ts
// 这样写transform函数就会有问题,当flag传递的是false的时候管道的返回值
// 是undefined,这可能会导致潜在的 bug 或意外的行为
transform(val: any, flag = true) {
if(flag) {
console.log(val, flag)
}
}
注册
js
// 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 { }
使用
说明: 在使用的时候需要使用到管道符|
,一般看到的格式为{{ 变量 | 管道 }}
,这样写之后这个变量就会自动的去执行定义的管道中的transform
函数,将函数执行后所得到的值会覆盖变量原有的值
html
<!-- src/app/app.component.html -->
<app-myco1></app-myco1>
html
<!-- src/app/myc01/myc01.component.html -->
<input type="text" [(ngModel)]="text">
<!-- 这里传递的false其实对应的是transform函数的第二个参数flag, -->
<!-- 而不是对应第一个参数val -->
<span>{{ text | fitterDemo : false }}</span>
ts
// 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
}
这里值得注意的在于管道传递的参数,管道传递参数的方式是使用
:传递的参数
,存在多个就继续:传递的参数
的方式进行传值就好了,其次,由于transform
函数的第一个参数是默认就有的,不需要传,所以这里传递的参数的数量是从transform
函数的第一个参数开始算起的
创建命令
ts
// 完整命令:
ng g pipe 管道名称
// 简写命令:
ng g p 管道名称
在初始化好的管道文件中需要注意的地方:
ts
// 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;
}
}
2.预定义管道
基本使用
说明: 这个类似于Lodash
这个JavaScript库一样,它将一些你经常会进行的操作封装成管道,让你直接使用就可以,其次,这些管道是存放在CommonModule
模块里面,也就是你不需要引入就可以使用了,至于为什么我在双向绑定
那里提到过,最后这里用UpperCasePipe
这个管道举个例子,在文档中你看到的预定义管道后面都会加上一个pipe
的描述,在使用的时候这个是需要去掉的,然后呢,如果需要查看更多跟预定义管道的内容呢,戳此处去文档查看
html
<!-- src/app/myc01/myc01.component.html -->
<!-- 通过这个管道的操作后小写的english就会变成大写的ENGLISH了 -->
{{ value_expression | uppercase }}
ts
// 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 {
value_expression = 'english'
}
七、服务与依赖注入
服务
是不需要在app.module.ts
中进行注册的
1.自定义服务
场景导入
说明: 以购物网站为例,与删除商品数据有关的操作是很敏感的,一般的人是没有这个权限的,如果删除了数据的话,这个操作会被记录下来,在开发的过程中,记录这样的信息称为日志
,这里简单模拟一下这个行为,比如我添加或者删除数据的时候,记录其操作的相关信息
简单实现
html
<!-- src/app/myc05/myc05.component.html -->
<h2>商品管理</h2>
<button (click)="doAdd()">添加商品</button>
<button (click)="doDelete()">删除商品</button>
ts
// 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
关键字进行注册,最后调用方法实现功能。
提供方案一
说明: 将日志的公共部分提取出来,并将其封装成一个方法用于调用,这样可以解决前面代码不宜维护的缺点
ts
// src/app/myc06.service.ts
export class Myc06Service {
// 提供一个做日志的方法,参数为做日志的行为
doLog(action: any) {
let uname = 'admin2';
let time = new Date().getTime();
console.log(`管理员:${uname} 时间:${time} 动作:${action}`);
}
}
html
<!-- src/app/myc05/myc05.component.html -->
<h2>商品管理</h2>
<button (click)="doAdd()">添加商品</button>
<button (click)="doDelete()">删除商品</button>
ts
// 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
关键字的操作,直接声明依赖,然后由模块提供相应的内容来达到你的目的,在给出方案二之前,需要先了解一个概念,就是服务
引出服务
概念: angular
认为:组件
是与用户进行交互的一种对象,其中的内容都必须和用户有关,而与用户没有关系的内容和操作都应该剥离出去,放在服务对象
里面
关系:
理解: 首先,在开发过程中,组件可能会产生实现类似
日志
功能的需求,这种需求会被理解为声明依赖
,然后这个功能会存放在服务
之中,然后既然有人存在需求,那就需要有人来解决需求,它就是服务提供者
,它会收到你的服务
需求,将其创建出来,之后将它传递给需要服务
的组件,这个过程称为依赖注入
提供方案二
下面这种写法是
angular6以后
在根模块注册服务的方法
注意: 这种根模块的服务对象
只会在应用程序启动时创建,也就是整个应用程序共享同一个服务对象实例,如果需要使用这类服务对象,条件是创建的服务中不存在变化的属性,因为如果多个组件共用这一个实例,使用时会出现数据的问题
ts
// 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}`);
}
}
html
<!-- src/app/myc05/myc05.component.html -->
<h2>商品管理</h2>
<button (click)="doAdd()">添加商品</button>
<button (click)="doDelete()">删除商品</button>
ts
// 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);
}
}
下面这是
angular6前
注册服务的写法,这种和上面那种注册的方式的效果是一致的,
ts
// src/app/app.module.ts
import { Myc06Service } from './myc06.service';
@NgModule({
declarations: [],
imports: [],
providers: [ Myc06Service ],
bootstrap: [],
})
export class AppModule {}
提供方案三
当然,如果需要创建
组件自己的服务对象
,可以以下面这个例子为例,组件中提供的服务对象是在每个组件实例化时创建的,每个组件都有自己的服务对象实例
,这样即使服务中存在数据变量这些内容,它也不会影响组件内部的数据
ts
// 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}`);
}
}
html
<!-- src/app/myc05/myc05.component.html -->
<h2>商品管理</h2>
<button (click)="doAdd()">添加商品</button>
<button (click)="doDelete()">删除商品</button>
ts
// 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);
}
}
方案二和方案三的区别在于
创建的服务对象里面是否存在属性
,由于方案二可以理解为全局服务对象,方案三理解为局部服务对象,那么服务中存在属性的话,全局的每次都会初始化这个属性或者是更改这个属性,这样数据就会遭到污染,
那么方案二也就不再适用了,该用方案三的局部来代替。
创建命令
命令:
ts
// 完整写法
ng g service 服务名称
// 简写形式
ng g s 服务名称
2.HttpClient
说明: 这是一个angular
内置的服务,从名字就可以看出它是与请求有关的,在使用的时候需要注意它需要在app.module.ts
里面将模块HttpClientModule
从@angular/common/http
导入并注册该模块,一般情况下 ,由于这个服务是angular
中内置的,因此并不需要为其独自创建一个.service.ts
的文件,直接在组件中使用就好
基本使用
导入: 先导入需要的模块并将其进行注册
ts
// src/app/app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { Myc06Component } from './myc06/myc06.component';
// 1.首先需要从指定的模块中导入 HttpClientModule 模块
import { HttpClientModule } from '@angular/common/http';
@NgModule({
declarations: [
AppComponent,
Myc06Component,
],
imports: [
BrowserModule,
AppRoutingModule,
// 2.将这个模块导入注册进来
HttpClientModule,
],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {}
使用: 在指定的组件之中去使用这个服务
html
<!-- src/app/myc06/myc06.component.html -->
<button (click)="Load()">进行加载</button>
ts
// src/app/myc06/myc06.component.ts
// 4.将angular内置的服务 HttpClient 导入进来,注意导入的路径
import { HttpClient } from '@angular/common/http';
import { Component } from '@angular/core';
@Component({
selector: 'app-myc06',
templateUrl: './myc06.component.html',
styleUrls: ['./myc06.component.less'],
})
export class Myc06Component {
constructor(private api: HttpClient) {}
Load() {
// 这个请求我是从百度搜索哪里随便找的一个,
// 所以在发请求的时候会存在跨域的问题
let url =
'/api/sugrec?pre=1&p=3&ie=utf-8&json=1&prod=pc&from=pc_web&sugsid=39311,39398,39396,39407,39097,39413,39436,39358,39307,39451,39464,39233,39249,26350,39426&wd=dew&his=%5B%7B%22time%22%3A1692619187%2C%22kw%22%3A%22%E6%8A%96%E9%9F%B3%22%7D%2C%7B%22time%22%3A1692675866%2C%22kw%22%3A%22%E6%8E%98%E9%87%91%22%7D%2C%7B%22time%22%3A1692675877%2C%22kw%22%3A%22%E7%A8%80%E5%9C%9F%E6%8E%98%E9%87%91%22%7D%2C%7B%22time%22%3A1692694636%2C%22kw%22%3A%22single%20element%20css%20spinners%22%7D%2C%7B%22time%22%3A1692757556%2C%22kw%22%3A%22bilibil%22%7D%2C%7B%22time%22%3A1692765426%2C%22kw%22%3A%22%E5%85%B8%E7%B1%8D%E9%87%8C%E7%9A%84%E4%B8%AD%E5%9B%BD%22%7D%2C%7B%22time%22%3A1692849120%2C%22kw%22%3A%22%E5%93%94%E5%93%A9%E5%93%94%E5%93%A9%22%2C%22fq%22%3A4%7D%2C%7B%22time%22%3A1692926175%2C%22kw%22%3A%22%E8%8A%B1%E7%93%A3%22%7D%2C%7B%22time%22%3A1692947784%2C%22kw%22%3A%22%E5%88%87%E5%B0%94%E8%AF%BA%E8%B4%9D%E5%88%A9%20%E7%90%B4%E4%BC%A6%22%2C%22fq%22%3A2%7D%2C%7B%22time%22%3A1695978309%2C%22kw%22%3A%22%E7%99%BE%E5%BA%A6%22%7D%5D&req=2&bs=%E4%B9%A0%E8%BF%91%E5%B9%B3%E5%BC%BA%E8%B0%83%E5%9B%A2%E7%BB%93%E5%B0%B1%E6%98%AF%E5%8A%9B%E9%87%8F&csor=3&pwd=de&cb=jQuery1102007980192120627838_1696042595801&_=1696042595804';
// this.api表示那个内置的请求服务对象,后面跟请求的方式
// 你所见的所有请求方式都可以使用,这里以get请求为例,
// 它有两个参数,第一个是请求的地址,第二个是一个配置对象,
// 用于设置请求头什么的,如果请求方式后面不跟subscribe
// 这个方法的话,那么当前的请求是不会生效的,如果跟这个
// 方法,方法有两个函数参数,一个是成功时执行,一个是失败时
// 执行,跟promise的那两个函数参数使用的方法是一致的,
// 下面这种写法是最新的写法
this.api.get(url).subscribe({
next: (res) => {
console.log(res);
},
error: (err) => {
console.log(err);
},
});
}
}
private:
访问修饰符可以提供封装性,确保类的内部细节对外部是不可见的。这有助于实现信息隐藏和数据封装的原则,以便更好地控制类的行为和状态。
代理: 这里简单介绍一下代理是怎么配置的吧,代理的话实际上还是与webpack
相关,我在webpack
里面代理的具体配置我已经写好了
angular
的webpack
配置文件是proxy.conf.json
json
// proxy.conf.json(这个文件在根文件夹下)
{
// 使用正则进行匹配,如果匹配到的 /api 这个路径,那么
// 它就会将请求转接到 下面的 target 路径下面,但此时
// 路径中 还是会存在 /api 这个字符串,它会影响到转接的
// 效果,所以需要使用 pathRewrite 路径重写将其去除掉
"/api": {
"target": "https://www.baidu.com",
"secure": false,
"changeOrigin": true,
// 这个是当匹配到 /api 的时候会将其去除掉
"pathRewrite": {
"^/api": ""
}
}
}
八、组件的生命周期
1.介绍
说明: 介绍的顺序按照执行的顺序来的,使用的话跟vue
中是一样的,区别在于触发的时机以及函数的参数不一样。
constructor
说明: 组件的构造函数,在创建组件实例时被调用,通常用于依赖注入。
ngOnChanges
说明: 在输入属性发生变化时被调用,在组件初始化时也会被调用一次。
ts
import { Component, OnChanges } from '@angular/core';
@Component({
selector: 'app-my-component',
template: `
<h1>{{ title }}</h1>
`
})
// implements OnChanges:这个不是必须的,如果存在的话,没有
// OnChanges是会报错的
export class MyComponent implements OnChanges {
title: string;
constructor() {
this.title = 'My Component';
console.log('constructor');
}
// 这是唯一一个有参数的周期函数,它会在组件的输入
// 属性发生变化时调用,changes是一个对象,包含了
// 输入属性的变化信息。
ngOnChanges(changes: SimpleChanges) {
console.log('ngOnChanges');
}
}
使用场景:
监听输入属性的变化,一旦输入属性发生变化,我们可以根据需要执行相应的操作,比如重新计算或更新其他相关的属性。
ngOnInit
说明: 在组件初始化完成后被调用,这个可以理解为vue
中的mounted周期
使用场景:
初始化组件的属性和数据,确保组件在渲染前已准备好所需的数据。
ngDoCheck
说明: 在每次 Angular 执行变更检测时被调用,在数据绑定发生变化时触发。
使用场景:
用于自定义的变更检测逻辑,主要用于性能优化、检测复杂对象的变化等。
ngAfterContentInit
说明: 在组件内容(例如子组件)初始化完成后被调用。
ngAfterContentChecked
说明: 在每次 Angular 执行内容检测后被调用,用于响应内容变化的操作。
使用场景:
该方法在组件内容变更后被调用,可以执行与内容相关的操作,比如更新模板中的投影内容。
ngAfterViewInit
说明: 在组件的视图初始化完成后被调用。
ngAfterViewChecked
说明: 在每次 Angular 执行视图检测后被调用,用于响应视图变化的操作。
使用场景:
该方法在视图变更后被调用,可以执行与视图相关的操作,比如处理 DOM 元素、调用第三方库等。
ngOnDestroy
说明: 在组件销毁时被调用。
使用场景:
可以进行一些清理操作,如取消订阅,释放资源,避免内存泄漏和性能问题。
九、组件的数据传递
说明: 理解上跟vue
是类似的,区别在于使用,在angular中
,父组件可以通过属性绑定将数据传递给子组件,子组件通过@Input()
装饰器接收数据并进行处理。而子组件可以通过@Output()
装饰器和事件发射器EventEmitter
将数据发送回父组件。
1.父组件给子组件传递数据
父组件:
html
<!-- src/app/myc01/myc01.component.html -->
<app-myc02 [demoText]="text"></app-myc02>
ts
// 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
}
子组件:
html
<!-- src/app/myc02/myc02.component.html -->
<p>{{ demoText }}</p>
ts
// 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
}
2.父组件直接获取子组件的引用
父组件:
说明: 这种方式主要是使用#
这个标识符来标记子组件,然后在父组件中定义变量,将这个变量通过@ViewChild
装饰器与对应的子组件连接起来,此时,这个变量的值会变成指定子组件的引用,当然,装饰器与变量还是一对一的关系
html
<!-- src/app/myc01/myc01.component.html -->
<!-- 将子组件myc02标记为#c01 -->
<app-myc02 #c01></app-myc02>
<button (click)='print()'>打印出子组件</button>
ts
// 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)
}
}
子组件:
html
<!-- src/app/myc02/myc02.component.html -->
<p>这是子组件</p>
ts
// 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 {}
3.子组件给父组件传递数据
子组件:
html
<!-- src/app/myc02/myc02.component.html -->
<!-- 子组件向上传递数据的话还是通过出发事件来完成任务的 -->
<button (click)="demo()">点击向父组件传递数据</button>
ts
// 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)
}
}
父组件:
html
<!-- src/app/myc01/myc01.component.html -->
<span>{{ text }}</span>
<!-- 在父组件上面需要定义一个自定义事件,事件的名字 -->
<!-- 需要和子组件中定义的那个触发器的名字相同, -->
<!-- 这样就接收到了子组件的数据,数据会存放在$event里面 -->
<app-myc02 (demoEvent)="doSomething($event)"></app-myc02>
关于
$event
:如果事件是原生事件,
$event
表示事件对象event如果事件是自定义事件,
$event
表示获取的数据
ts
// 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
}
}
十、路由与导航
1.路由的使用
创建路由规则
说明: 在路由规则比较少,复杂程度不是很大的话,可以将路由规则放在app.module.ts
文件里面,但是不可以放在模块里面,其次,路由规则是一个数组,数组的每个元素是一个对象,对象由两部分组成,一个是path,这个是路由的路径,也就是在地址栏中 / 后面的内容,它是一个字符串,另一个是component,它是path路径对应的组件,它的值是上面导入进来的组件名
ts
// src/app/app.module.ts
import { AppComponent } from './app.component';
import { Myc02Component } from './myc02/myc02.component';
// 一般情况这个路由规则数组的名称叫 router,也是习惯吧
let router = [
{
path: 'index',
// path: '/index', --> 错误写法
// path: 'index/', --> 错误写法
// path: 'index/index', --> 正确写法
component: Myc02Component,
},
];
@NgModule({
declarations: [],
imports: [],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {}
html
<!-- src/app/myc02/myc02.component.html -->
<p>这是index页面</p>
注意:
path的值不能够使用
/
开头,也不能够使用/
结尾,如果非要使用,只能够在中间
注册路由
说明: 注册路由需要使用到RouterModule
这个模块,注意导入的路径是@angular/router
ts
// src/app/app.module.ts
import { AppComponent } from './app.component';
import { Myc02Component } from './myc02/myc02.component';
import { RouterModule } from '@angular/router';
let router = [
{
path: 'index',
component: Myc02Component,
},
];
@NgModule({
declarations: [],
imports: [
// 这个模块有两个静态方法forRoot和forChild用来
// 配置路由规则,一个是规则在主模块中注册,一个
// 是在指定的模块中注册,可以理解为全局路由和局部
// 路由吧,这里就是在主模块中注册
RouterModule.forRoot(router)
],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {}
forRoot
方法:
用于在应用程序的主模块中配置主要的路由规则。
仅在应用程序的主模块中调用一次。
会创建并返回一个带有路由配置的
RouterModule
实例,并将其提供给注入器。
forChild
方法:用于在其他模块中配置附加的路由规则。
在应用程序的任何模块中都可以调用多次。
会创建并返回一个带有路由配置的
RouterModule
实例,但不会将其提供给注入器。根据
forChild
方法调用的顺序,路由规则将按照各个模块的顺序进行处理。
路由出口
说明: vue
中这个出口是用router-view
来完成的,这里也类似,只不过是用router-outlet
来替代
html
<!-- src/app/app.component.html -->
<!-- 然后切换的页面会在这里显示 -->
<router-outlet></router-outlet>
特殊的路由path
ts
// src/app/app.module.ts
import { AppComponent } from './app.component';
import { Myc02Component } from './myc02/myc02.component';
import { Routes, RouterModule } from '@angular/router';
// Route类型:表示只存在一条路由规则
// Routes类型:表示一个数组,可以存放多条路由规则
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 {}
路由部分单独分离出来
说明: 如果程序很复杂,很庞大,那路由规则也会很多,可能也很复杂,这样就可以将每个部分的路由规则抽离出来,这样也便于去维护,比如像下面这样
ts
// 在路由模块中定义路由:
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HomeComponent } from './home.component';
import { AboutComponent } from './about.component';
const routes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'about', component: AboutComponent }
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
ts
// 在主模块中导入路由模块并将其添加到 imports 数组中:
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { HomeComponent } from './home.component';
import { AboutComponent } from './about.component';
@NgModule({
imports: [
BrowserModule,
AppRoutingModule
],
declarations: [
AppComponent,
HomeComponent,
AboutComponent
],
bootstrap: [AppComponent]
})
export class AppModule { }
路由的嵌套
说明: 实现路由的嵌套只需要在创建的路由规则中添加一个children的属性
就可以,它的值是一个数组,数组的每一个元素就是嵌套路由中的每一条路由规则,之后,嵌套的路由出口是放在那个存在路由嵌套的组件中
ts
// 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,
// 这里的本质是在组件Myc02Component里面还会存在路由,
// 也就是路由的嵌套了
children: [
{
// 没有 / 表示 组件Myc03Component的路由path是
// '/index/index',它会自动的进行拼接
path: 'index',
component: Myc03Component,
},
]
},
];
@NgModule({
declarations: [],
imports: [
RouterModule.forRoot(router)
],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {}
html
<!-- src/app/app.component2.html -->
<!-- 上面是component2组件中存在路由的嵌套, -->
<!-- 所以路由出口放在component2里面 -->
<router-outlet></router-outlet>
2.导航
声明式导航
说明: 在 HTML 中使用 routerLink
指令创建一个路由链接。这个指令会为元素自动添加点击事件,并根据指定的路由路径进行导航,值得注意的是,这个指令可以用在任意的标签上面
ts
// src/app/app.module.ts
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 { }
html
<!-- src/app/app.component.html -->
<p>路由部分</p>
<router-outlet></router-outlet>
html
<!-- src/app/myco1/myco1.component.html -->
<!-- 这两种写法的跳转结果会存在一些差异 -->
<button 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
服务来实现编程式导航。
(1)navigateByUrl
作用: 用于通过指定的 URL 导航到一个新的路由。
参数:
ts
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
。
使用:
html
<!-- src/app/myco1/myco1.component.html -->
<button (click)="jump()">点击实现编程式导航</button>
ts
// 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
说明: 他的作用与上面方法的作用一致,区别在于它可以传递路径参数,但是上面哪一种不行
参数:
ts
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' }
})
使用: 使用的方法跟上面的方法是一致的,只是参数不同而已(以上面的例子为例)
ts
// 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.路由参数
路径参数
说明: vue
的params参数
使用跟这个是类似的,也就是参数需要定义在路由配置对象里面,像/:参数名
这样的格式来定义在path
路径之后,传值使用/参数值
的方式传递
(1)定义参数
ts
// src/app/app.module.ts
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 { }
(2)传递参数
html
<!-- src/app/myco1/myco1.component.html -->
<!-- 声明式导航 -->
<!-- 这里给上面定义的路径参数id传值为1 -->
<button routerLink="/index/1">点击进行路由跳转(绝对路径)</button>
ts
// 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() {
// 暂时了解到可以传递路径参数的方法只有navigate,
// 传递的位置在第一数组参数里面,数组的第一个元素为
// 路由地址,从第二个参数开始就是传递的路径参数的值了
// 注意传递的顺序,避免穿错
this.router.navigate(['/index',1])
}
}
(3)参数的接受
ts
// src/app/myco1/myco1.component.ts
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
@Component({
selector: 'app-myco1'
templateUrl: './myco1.component.html',
styleUrls: ['./myco1.component.less']
})
export class Myco1Component implements OnInit {
// 当跳转到此页面的时候,我需要知道传递了什么参数,
// 对于路由来说,肯定需要路由服务来支持的,但是对于
// 查看某个页面所需要的路由参数,我只需要当前页面路
// 由信息就足够了,也就是说不需要去使用路由器对象了,
// 当前路由信息通过ActivatedRoute服务对象获取
constructor(private route: ActivatedRoute) {}
// 一般参数获取实在页面加载完毕之后进行的,
// 也就是类似vue中的mounted生命周期,
// angular用ngOnInit来代替
ngOnInit() {
// this.route.params可以拿到传递的参数,
// 但是其类型是可订阅的,所以需要去订阅它
// 去获取并且通过回调函数来对参数进行自定义操作
this.route.params.subscribe((data) => {
console.log(data)
})
}
}
(4)注意事项
如果在路由配置对象中某个路径他的路径参数被定义了,那么这个路由参数是一个必传的值,也就是如果不传,路由跳转就会报错
4.路由守卫
说明: 路由守卫是一种用于保护和控制导航的机制。它允许开发者在路由导航之前和之后执行相应的操作,常用的守卫有三个,分别是CanActivate
、CanActivateChild
、CanDeactivate
,特殊的有两个,分别是Resolve
和CanLoad
创建命令
ts
// 完整写法
ng generate guard 守卫名称
// 简洁写法
ng g guard 守卫名称
守卫介绍
(1)CanActivate
说明: 用于确定是否允许进入特定路由。它可以通过实现CanActivate
接口来创建一个守卫。当导航到某个路由时,Angular会调用CanActivate
接口中的canActivate
方法。如果该方法返回true
,导航将继续;如果返回false
,导航将被取消。
ts
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 方法进行重定向
// 或者返回 false 或者返回 this.router.navigate 方法
return true;
}
}
ts
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
接口来创建一个守卫。
ts
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'); // 重定向到禁止访问页面
}
}
}
ts
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
,导航将被取消。
ts
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('确定要离开此页面吗?'); // 弹出确认对话框
}
}
}
ts
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
可以在路由激活之前获取所需的数据,并等待数据可用后再加载路由组件。这样可以确保组件在渲染之前具有所需的数据,以避免在组件中处理数据加载的异步问题。
ts
// 创建一个服务来获取预先需要的数据
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();
}
}
ts
// 在路由配置中使用 `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 { }
ts
// 在组件中访问预先解析的数据:
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
守卫用于在加载惰性模块之前执行某些验证或权限检查
ts
// 创建一个实现了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();
}
}
ts
// 在路由配置中使用 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 { }
ts
// 配置守卫后,可以在AuthService中实现具体的权限检查逻辑。
@Injectable()
export class AuthService {
// ...
checkUserPermissions(): Observable<boolean> {
// 进行具体的权限检查,返回 Observable<boolean> 或 Promise<boolean>
}
}