Angular 18 上手开发
0x01 概述
(1)简介
- 官方网站:https://www.angular.cn/
- Angular 由 Google 的专业团队维护,Angular 提供了广泛的工具、API 和库,简化和优化开发工作流程
- Angular 提供了一个坚实的平台,可用于构建快速、可靠、能够随着团队规模和代码库规模扩展的应用程序
- 特点:
- 整合性高,降低技术决策成本
- 简化 DOM 操作,注重业务逻辑
- 采用后端的依赖注入系统
- 与 React、Vue 对比,Angular:
- 在大型企业级应用中提供更成熟的框架和严格的架构模式,适合复杂项目的长期维护
- 初始加载时间较长,不够灵活和轻量
- 学习曲线较陡峭
(2)创建项目
Angular 18 于 2024 年 5 月正式发布,以下内容均采用 18.2.10 / 18.2.9 版本,NodeJS 采用 23.0.0 版本,npm 采用 10.9.0
- 使用命令
npm install -g @angular/cli
安装脚手架 - 使用命令
ng new angular-app
创建名为 angular-app 的项目- 不向 Google 发送数据
- 不使用 CSS 预编译
- 不使用 SSR
- 使用命令
cd angular-app
进入项目目录 - 使用命令
ng serve --open
启动项目并自动开启浏览器
使用
ng
命令创建组件等:
脚手架 说明 命令 Component 组件 ng generate component my-new-component
Directive 指令 ng generate directive my-new-directive
Pipe 管道 ng generate pipe my-new-pipe
Service 服务 ng generate service my-new-service
Class 类 ng generate class my-new-class
Interface 接口 ng generate interface my-new-interface
Enum 枚举 ng generate enum my-new-enum
Module 模块 ng generate module my-new-module
(3)项目结构
-
.angular:编译缓存目录
-
node_modules:Node 包目录
-
public:公共资源目录
-
src:代码资源目录
-
app:App 模块代码目录
- app.component.css:组件样式文件
- app.component.html:组件模板文件
- app.component.spec.ts:组件测试文件
- app.component.ts:组件入口文件
- app.config.ts:模块配置文件
- app.routes.ts:模块路由文件
-
index.html:模板文件
<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <title>AngularApp</title> <base href="/"> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="icon" type="image/x-icon" href="favicon.ico"> </head> <body> <app-root></app-root> </body> </html>
-
main.ts:入口文件
import { bootstrapApplication } from '@angular/platform-browser'; import { appConfig } from './app/app.config'; import { AppComponent } from './app/app.component'; bootstrapApplication(AppComponent, appConfig) .catch((err) => console.error(err));
-
style.css:全局样式文件
-
-
.editorconfig:编辑器配置文件,详情参考 https://editorconfig.org
-
.gitignore:git 忽略文件
-
angular.json:Angular 配置文件
-
package.json:Node 包配置文件
-
在
"script"
项中添加以下内容:{ "scripts": { "dev": "ng serve --open", // ... }, }
以后通过命令
npm run dev
来启动项目(代替命令ng serve --open
)
-
-
tsconfig.json:TypeScript 配置文件
0x02 组件
(1)简介
以 app.component.ts 为例:
import { Component } from '@angular/core'; import { RouterOutlet } from '@angular/router'; @Component({ selector: 'app-root', standalone: true, imports: [RouterOutlet], templateUrl: './app.component.html', styleUrl: './app.component.css' }) export class AppComponent { title = 'angular-app'; }
-
@Component
是一个装饰器,用于加载一些配置信息,其中-
selector
:选择器,用于选择模板中的 DOM 标签,并将该组件渲染至该标签中<!-- filename: index.html --> <body> <app-root></app-root> </body>
-
standalone
:独立模式,true
表示该组件不依赖于任何模块,可以直接在应用中使用,而不需要通过模块进行声明 -
imports
:导入组件,此处导入了RouterOutlet
组件-
RouterOutlet
是 Angular 路由模块中的一个重要组件,用于在应用中定义路由视图的占位符类似 React Router 中的
<Outlet />
-
-
templateUrl
:模板文件路径 -
styleUrl
:样式文件路径
-
-
导出经过装饰器声明的类,类中可以传值
(2)创建组件
-
使用命令
ng generate component hello
(或ng g c hello
)创建一个在 app 目录下,名为 hello 的新组件 -
app 目录结构
flowchart TB app-->hello & app.component.css & app.component.html & ... hello-->hello.component.css & hello.component.html & hello.component.spec.ts & hello.component.ts -
修改 hello.component.html
<div style="width: 200px; height: 200px; background-color: red"> 这里是 hello 组件 </div>
-
修改 hello.component.ts
import { Component } from '@angular/core'; @Component({ selector: 'app-hello', standalone: true, imports: [], templateUrl: './hello.component.html', styleUrl: './hello.component.css' }) export class HelloComponent { }
-
修改 app.component.ts
import { HelloComponent } from "./hello/hello.component";
-
修改 app.component.html
<app-hello></app-hello>
-
启动项目,访问 http://localhost:4200/,此时可以发现页面中存在 hello 组件
(3)生命周期
-
生命周期钩子及其执行顺序如下:
钩子 作用 时机 ngOnChanges()
每当 Angular 设置或重新设置数据绑定的输入属性时响应 在 ngOnInit()
之前以及所绑定的一个或多个输入属性方式变化时调用ngOnInit()
在 Angular 第一次显示数据绑定和设置指令或组件的输入属性后,初始化指令或组件 首次 ngOnChanges()
之后调用,且仅调用一次ngDoCheck()
每当发生 Angular 无法或不愿自检的变化时,进行检测 每次执行变更检测时的 ngOnChanges()
之后调用
首次执行变更检测的ngOnInit()
之后调用ngAfterContentInit()
当 Angular 将外部内容投影至组件或指令中的内容之后调用 首次 ngDoCheck()
之后调用,且仅调用一次ngAfterContentChecked()
每当 Angular 检测完被投影至组件或指令中的内容之后调用 每次 ngDoCheck()
之后调用
首次ngAfterContentInit()
之后调用ngAfterViewInit()
当 Angular 初始化完组件视图、其子视图、或包含该指令的视图之后调用 首次 ngAfterContentChecked()
之后调用,且仅调用一次ngAfterViewChecked()
每当 Angular 检测完组件视图、其子视图、或包含该指令的视图的变更之后调用 每次 ngAfterContentChecked()
之后调用
首次ngAfterViewInit()
之后调用ngOnDestroy()
每当 Angular 销毁指令或组件之前调用 在 Angular 销毁指令或组件之前立即调用 graph LR ngOnChanges--首次-->ngOnInit-->ngDoCheck--首次-->ngAfterContentInit-->ngAfterContentChecked--首次-->ngAfterViewInit-->ngAfterViewChecked-.->ngOnDestroy ngOnChanges-->ngDoCheck-->ngAfterContentChecked-->ngAfterViewChecked -
举例:app.component.ts
export class AppComponent { ngOnChanges() { console.log('ngOnChanges'); } ngOnInit() { console.log('ngOnInit'); } ngDoCheck() { console.log('ngDoCheck'); } ngAfterContentInit() { console.log('ngAfterContentInit'); } ngAfterContentChecked() { console.log('ngAfterContentChecked'); } ngAfterViewInit() { console.log('ngAfterViewInit'); } ngAfterViewChecked() { console.log('ngAfterViewChecked'); } ngOnDestroy() { console.log('ngOnDestroy'); } }
(4)交互
组件间传参
-
@Input
:用于父组件给子组件绑定属性,设置输入类数据-
hello.component.ts
import { Component, Input } from '@angular/core'; @Component({/* ... */}) export class HelloComponent { @Input() username!: string; ngOnInit() { console.log(this.username); } }
-
app.component.html
<app-hello [username]="'SRIGT'"></app-hello>
-
-
@Output
:用于子组件弹射触发事件,该事件来自父组件给子组件的传递-
app.component.ts
export class AppComponent { list = [1, 2, 3]; add(newNumber: number) { this.list.push(newNumber); } }
-
hello.component.ts
import { Component, EventEmitter, Output } from '@angular/core'; @Component({ selector: 'app-hello', standalone: true, imports: [], templateUrl: './hello.component.html', styleUrl: './hello.component.css', }) export class HelloComponent { @Output() add = new EventEmitter(); clickHandler() { this.add.emit(4); } }
-
app.component.html
<app-hello (add)="add($event)"></app-hello> <p>{{ list.toString() }}</p>
-
hello.component.html
<button (click)="clickHandler()">+4</button>
-
-
@ViewChild
:获取子组件实例与数据-
app.component.ts
import { Component, ViewChild } from '@angular/core'; // ... @Component({/* ... */}) export class AppComponent { @ViewChild('childComp') child: any; clickHandler() { console.log(this.child); } }
-
app.component.html
<app-hello #childComp></app-hello> <button (click)="clickHandler()">Click</button>
-
0x03 模板
(1)插值
-
“插值”指将表达式嵌入到 HTML 文档中,通过
{{ }}
实现<!-- filename: app.component.html --> <p>1 + 1 = {{ 1 + 1 }}</p>
-
在 app.component.ts 导出的类中包含的变量值,也可以通过插值语法嵌入到 HTML 文档中
-
app.component.ts
// import ... @Component({/* ... */}) export class AppComponent { expression = `1 + 1 = ${1 + 1}`; }
-
app.component.html
<p>{{ expression }}</p>
-
(2)绑定
a. 属性绑定
-
一般地,属性绑定使用
[ ]
实现,如:<h1 [id]="'h1-title'">Title</h1>
渲染后变成了
<h1 _ngcontent-ng-xxxxxxxxxxx id="h1-title">Title</h1>
-
类绑定
-
单一类绑定:
<h1 [class]="'h1-title'">Title</h1> <!-- 或 --> <h1 [class.h1-title]="true">Title</h1>
-
多个类绑定:
<h1 [class]="'h1-title text-4xl font-bold'">Title</h1> <!-- 或 --> <h1 [class]="{ 'h1-title': true, 'text-4xl': false }">Title</h1> <!-- 或 --> <h1 [class]="[ 'h1-title', 'text-4xl' ]">Title</h1>
-
-
样式绑定
-
单一样式绑定:
<h1 [style]="'font-size:1em'">Title</h1> <!-- 或 --> <h1 [style.font-size]="'1em'">Title</h1> <!-- 或 --> <h1 [style.font-size.em]="'1'">Title</h1>
-
多重样式绑定:
<h1 [style]="'font-size:1em;font-weight:900;'">Title</h1> <!-- 或 --> <h1 [style]="{ 'font-size': '1em', 'font-weight': 900 }">Title</h1>
-
b. 事件绑定
-
一般地,事件绑定使用
( )
实现,如:-
app.component.ts
export class AppComponent { clickHandler() { alert("Button clicked") } }
-
app.component.html
<button (click)="clickHandler()">Click</button>
-
-
对于事件对象,使用
$event
传递-
app.component.ts
export class AppComponent { clickHandler(event: Event) { console.log(event) } }
-
app.component.html
<button (click)="clickHandler($event)">Click</button>
-
c. 双向绑定
-
双向绑定是应用内组件共享数据的方式
-
一般地,双向绑定使用
[( )]
实现-
app.component.ts
export class AppComponent { number: number = 10; add() { this.number += 1; } sub() { this.number -= 1; } }
-
app.component.html
<button (click)="sub()">-1</button> <button (click)="add()">+1</button> <span>Current number is {{ number }}</span>
-
-
表单元素的双向绑定需要
@NgModule
装饰器,使用前需要从@angular/forms
中导入FormsModule
-
app.component.ts
// ... import { FormsModule } from '@angular/forms'; @Component({ // ... imports: [/*...*/, FormsModule], }) export class AppComponent { text = ''; }
-
app.component.html
<input [(ngModel)]="text" /> <p>Input text: {{ text }}</p>
-
d. 模板变量
-
模板变量使用
#
声明-
app.component.ts
export class AppComponent { clickHandler(value: string) { alert(value); } }
-
app.component.html
<input #text /> <button (click)="clickHandler(text.value)">Submit</button>
-
-
Angular 根据声明模板变量的位置进行赋值
- 在标准 HTML 元素上,则引用该元素
- 在组件上,则引用该组件实例
- 在
<ng-template>
元素上,则引用一个TemplateRef
实例来代表模板
(3)渲染
a. 条件
I. if
使用 *ngIF
实现,使用前需要从 @angular/common
导入 NgIf
注意:仅用于元素是否被渲染,而非控制元素的显隐
-
app.component.ts
// ... import { NgIf } from '@angular/common'; @Component({ // ... imports: [/* ... */, NgIf], }) export class AppComponent { isRender = false; }
-
app.component.html
<p> My name is <span *ngIf="isRender">John</span> <span *ngIf="!isRender">SRIGT</span> . </p> <!-- My name is SRIGT . -->
II. switch
使用 *ngSwitch
实现,使用前需要从 @angular/common
导入 NgSwitch
-
app.component.ts
// ... import { NgSwitch, NgSwitchCase, NgSwitchDefault } from '@angular/common'; @Component({ // ... imports: [/* ... */, NgSwitch, NgSwitchCase, NgSwitchDefault], }) export class AppComponent { nameType = "nickname" }
-
app.component.html
<p [ngSwitch]="nameType"> My name is <span *ngSwitchCase="'realname'">John</span> <span *ngSwitchCase="'nickname'">SRIGT</span> <span *ngSwitchDefault>Someone</span> . </p> <!-- My name is SRIGT . -->
b. 循环
循环渲染使用 *ngFor
实现,使用前需要从 @angular/common
导入 NgFor
-
app.component.ts
// ... import { NgFor } from '@angular/common'; @Component({ // ... imports: [/* ... */, NgFor], }) export class AppComponent { names = ['Alex', 'Bob', 'Charlie']; }
-
app.component.html
<div *ngFor="let name of names; let i = index; let odd = odd"> {{ odd }} {{ i }} {{ name }} </div> <!-- false 0 Alex true 1 Bob false 2 Charlie -->
c. 控制流
Angular 16 新特性
-
@if
、@else if
、@else
-
app.component.ts
export class AppComponent { isRender = false; }
-
app.component.html
<p> My name is @if (isRender) { {{ "John" }} } @else { {{ "SRIGT" }} } . </p> <!-- My name is SRIGT . -->
-
-
@switch
、@case
、@default
-
app.component.ts
export class AppComponent { nameType = "nickname" }
-
app.component.html
<p> My name is @switch (nameType) { @case ('realname') { {{ "John" }} } @case ('nickname') { {{ "SRIGT" }} } @default { {{ "Someone" }} } } . </p> <!-- My name is SRIGT . -->
-
-
@for
-
app.component.ts
export class AppComponent { items = [ { id: 1, name: 'Alex' }, { id: 2, name: 'Bob' }, { id: 3, name: 'Charlie' }, ]; }
-
app.component.html
<ul> @for (item of items; track item.id) { <li>{{ item.name }}</li> } @empty { <li>There are no items.</li> } </ul>
-
(4)表单
a. 单个表单控件
-
注册响应式表单模块,需要从
@angular/forms
导入ReactiveFormsModule
// ... import { ReactiveFormsModule } from '@angular/forms'; @Component({ // ... imports: [/* ... */, ReactiveFormsModule], }) export class AppComponent {}
-
生成一个新的
FormControl
实例并保存在组件中,需要从@angular/forms
导入FormControl
// ... import { FormControl, ReactiveFormsModule } from '@angular/forms'; @Component({/* ... */}) export class AppComponent { username = new FormControl(''); reset() { this.username.setValue(''); } }
-
在模板中注册该实例
<label> username: <input type="text" [formControl]="username" /> </label> <p>{{ username.value }}</p> <button (click)="reset()">reset</button>
b. 多个表单控件
-
创建
FormGroup
实例,需要从@angular/forms
导入FormGroup
// ... import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; @Component({/* ... */}) export class AppComponent { loginForm = new FormGroup({ username: new FormControl(''), password: new FormControl(''), }); onSubmit() { alert( `username: ${this.loginForm.value.username}\npassword: ${this.loginForm.value.password}` ); } }
-
在模板中注册该实例
<form [formGroup]="loginForm"> <label> username: <input type="text" formControlName="username" /> </label> <br /> <label> password: <input type="password" formControlName="password" /> </label> <br /> <button (click)="onSubmit()">Login</button> </form>
c. 表单验证
-
Angular 支持 HTML5 提供的表单验证关键字(如
required
等),通过双向绑定可以获取组件信息,从得到验证结果-
app.component.ts
// ... import { FormControl, FormGroup, ReactiveFormsModule, ValidationErrors, Validators, } from '@angular/forms'; @Component({ // ... imports: [/* ... */, ReactiveFormsModule], }) export class AppComponent { // 表单数据 formData = { username: '', password: '' }; // 自定义验证方法 passwordValidator(control: FormControl): ValidationErrors | null { return control.value.length < 5 ? { password: true } : null; } // 表单控件组 loginForm = new FormGroup({ username: new FormControl(this.formData.username, [ Validators.maxLength(10), ]), password: new FormControl(this.formData.password, [ Validators.required, this.passwordValidator, ]), }); // 提交方法 onSubmit() { alert( `username: ${this.loginForm.value.username}\npassword: ${this.loginForm.value.password}` ); } }
-
app.component.html
<form [formGroup]="loginForm"> <label> username: <input type="text" formControlName="username" required /> </label> <br /> <span>{{ loginForm.get("username")?.valid }}</span> <br /> <label> password: <input type="password" formControlName="password" /> </label> <br /> <span>{{ loginForm.get("password")?.valid }}</span> <br /> <button (click)="onSubmit()">Login</button> </form>
其中,
.valid
是布尔类型
-
-
ngModel
可用于跟踪修改状态与有效性验证,通过三个 CSS 类来更新控件:状态 为真 为假 已被访问 ng-touched
ug-untouched
已变化 ng-dirty
ng-pristine
有效 ng-valid
ng-invalid
input { outline: none; } input.ng-invalid { border: 1px solid red; }
(5)管道
- 管道用于传输,即数据处理
- 管道采用链式连接,自左向右,依次执行
- 语法格式:
{{ 输入数据 | 管道1:参数 | 管道2... }}
注意:管道操作符的优先级高于 JavaScript 三元运算符
?:
,同时使用时需要使用( )
包裹三元运算符部分
a. 内置管道
-
内置管道包括
管道 功能 date
格式化日期 json
经过 JSON.stringify()
uppercase
字母大写转换 lowercase
字母小写转换 decimal
数值特定格式 currectcy
数值货币格式 percent
数值百分比格式 slice
切片成新子集 -
举例:
-
app.component.ts
// ... import { CommonModule } from '@angular/common'; @Component({ // ... imports: [/* ... */, CommonModule], }) export class AppComponent { today: Date = new Date(); }
-
app.component.html
<p>{{ today | date: 'yyyy-MM-dd HH:mm:ss'}}</p>
-
b. 自定义管道
使用命令 ng g p newPipe
生成名为 newPipe 的自定义管道,通过 @Pipe
装饰器标识管道
-
prefix.pipe.ts
import { Pipe, PipeTransform } from '@angular/core'; @Pipe({ name: 'prefix', standalone: true, }) export class PrefixPipe implements PipeTransform { transform(value: string, ...args: unknown[]): unknown { return `angular-${value}`; } }
-
app.component.ts
// ... import { CommonModule } from '@angular/common'; import { PrefixPipe } from './prefix.pipe'; @Component({ // ... imports: [/* ... */, CommonModule, PrefixPipe], }) export class AppComponent {}
-
app.component.html
<p>{{ "SRIGT" | prefix }}</p>
0x04 依赖注入
(1)服务
-
从组件中抽离出来的代码成为服务,其本质上是函数
- 主要将数据访问的职责从组件中独立成服务,使组件专注于数据展示
-
使用命令
ng g s list
生成一个名为 list 的服务,通过@Injectable
装饰器标识服务import { Injectable } from '@angular/core'; @Injectable({ providedIn: 'root', }) export class ListService { constructor() {} }
-
服务需要作为依赖,注入到系统、组件或模块才能使用,通过注册提供商和根注入器实现
(2)依赖注入
-
在上述新建的服务中,
providedIn: 'root'
指定 Angular 应该在根注入器中提供该服务 -
providedIn
取值包括:'root'
:注入到 App Module,所有子组件都可以使用null
:不设定服务在作用域- 组件名:只作用于该组件,常用于懒加载
-
完善上述服务:
-
list.service.ts
import { Injectable } from '@angular/core'; @Injectable({ providedIn: 'root', }) export class ListService { constructor() {} names = ['Alex', 'Bob', 'Charlie']; getNames() { return this.names; } }
-
app.component.ts
// ... import { ListService } from './list.service'; @Component({/* ... */}) export class AppComponent { constructor(private listService: ListService) {} names: Array<string> | undefined; ngOnInit() { this.names = this.listService.getNames(); } }
-
app.component.html
<ul> @for (name of names; track $index) { <li>{{ name }}</li> } </ul>
-
0x05 路由
(1)配置
-
路由器(Router)是一套规则列表,可用于查询 URL 对应的视图规则
-
路由(Route)是列表中的一条规则
path
:URL 目标component
:对应视图(组件)
-
一般地,路由规则配置在 .routes.ts 文件中,如 app.routes.ts:
import { Routes } from '@angular/router'; export const routes: Routes = [];
-
举例,设置链接,从
/
跳转至/hello
:-
app.routes.ts
import { Routes } from '@angular/router'; import { HelloComponent } from './hello/hello.component'; export const routes: Routes = [ { path: 'hello', component: HelloComponent, }, ];
-
app.component.ts
import { Component } from '@angular/core'; import { RouterLink, RouterOutlet } from '@angular/router'; @Component({ selector: 'app-root', standalone: true, imports: [RouterOutlet, RouterLink], templateUrl: './app.component.html', styleUrl: './app.component.css', }) export class AppComponent {}
-
app.component.html
<a [routerLink]="['/hello']">Hello</a> <router-outlet></router-outlet>
-
hello.component.html
<p>Hello, Angular</p>
-
-
使用通配路由实现 404 页面
export const routes: Routes = [ // ... { path: '**', component: NotFoundComponent, }, ];
(2)嵌套
-
路由规则的
children
属性可用于实现路由嵌套// filename: app.routes.ts import { Routes } from '@angular/router'; import { HelloComponent } from './hello/hello.component'; export const routes: Routes = [ { path: 'hello', component: HelloComponent, children: [ { path: 'angular', component: HelloComponent, }, ], }, ];
-
在非父级路由
/hello
需要跳转到/hello/angular
时,需要些完整路径<!-- filename: app.component.html --> <a [routerLink]="['/hello/angular']">Hello</a> <router-outlet></router-outlet>
(3)传参
-
query:通过
<a>
的queryParams
参数传递参数,通过ActivatedRoute.snapshot.queryParams
获取参数-
app.component.html
<a [routerLink]="['/hello']" [queryParams]="{ name: 'Angular' }">Hello</a> <router-outlet></router-outlet>
-
hello.component.ts
import { Component } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; @Component({/* ... */}) export class HelloComponent { constructor(private activatedRoute: ActivatedRoute) {} name = "" ngOnInit() { this.name = this.activatedRoute.snapshot.queryParams["name"] } }
-
hello.component.html
<p>Hello, {{ name }}</p>
-
-
params:通过在路由配置中加入参数名来传递参数,通过 `` 获取参数
-
app.routes.ts
import { Routes } from '@angular/router'; import { HelloComponent } from './hello/hello.component'; export const routes: Routes = [ { path: 'hello/:name', component: HelloComponent, }, ];
-
hello.component.ts
import { Component } from '@angular/core'; import { ActivatedRoute, Params } from '@angular/router'; @Component({ selector: 'app-hello', standalone: true, imports: [], templateUrl: './hello.component.html', styleUrl: './hello.component.css', }) export class HelloComponent { constructor(private activatedRoute: ActivatedRoute) {} name = ''; ngOnInit() { this.activatedRoute.params.subscribe( (params: Params) => (this.name = params['name']) ); } }
-
hello.component.html
<p>Hello, {{ name }}</p>
-
更多详细内容参考官方文档
-End-