Skip to main content

Angular基础教程

Angular是由Google维护的一个开源前端框架,它提供了完整的解决方案用于构建复杂的企业级单页面应用程序(SPA)。Angular采用TypeScript语言开发,提供了组件化架构、依赖注入、响应式编程和全面的工具生态系统。

核心价值

Angular框架优势

  • 🏗️ 完整框架:提供一站式解决方案,不需要选择额外库
  • 📦 组件化架构:基于组件的模块化开发方式
  • 🧰 依赖注入:强大的依赖注入系统简化测试和管理
  • 📋 TypeScript支持:内置TypeScript支持,提供类型安全
  • 🛠️ 工具生态:Angular CLI提供完善的开发工具链
  • 🔄 双向绑定:灵活的数据绑定机制

1. Angular架构概览

Angular应用是由模块、组件和服务等构建块组成的:

1.1 核心构建块

  • 模块(NgModule): 相关组件、指令、管道和服务的容器
  • 组件(Component): 控制视图的类,负责数据和UI交互
  • 模板(Template): 定义组件视图的HTML
  • 元数据(Metadata): 通过装饰器定义类的行为
  • 服务(Service): 共享数据和功能的可复用类
  • 依赖注入(DI): 管理组件和服务依赖关系的设计模式

2. 开始使用Angular

2.1 安装Angular CLI

Angular CLI是一个命令行工具,用于初始化、开发、构建和维护Angular应用:

bash
1# 安装Angular CLI
2npm install -g @angular/cli
3
4# 查看版本
5ng version

2.2 创建新项目

使用Angular CLI创建一个新项目:

bash
1# 创建新项目
2ng new my-angular-app
3
4# 选项说明:
5# --strict: 启用严格模式
6# --routing: 添加路由模块
7# --style=scss: 使用SCSS预处理器
8
9cd my-angular-app
10ng serve

2.3 Angular项目结构

1my-angular-app/
2├── src/ # 源代码目录
3│ ├── app/ # 应用程序组件
4│ │ ├── app.component.ts # 根组件
5│ │ ├── app.component.html
6│ │ ├── app.component.scss
7│ │ ├── app.component.spec.ts
8│ │ ├── app.module.ts # 根模块
9│ │ └── app-routing.module.ts
10│ ├── assets/ # 静态资源
11│ ├── environments/ # 环境配置
12│ ├── favicon.ico
13│ ├── index.html # 主HTML文件
14│ ├── main.ts # 应用入口点
15│ ├── polyfills.ts # 浏览器兼容性脚本
16│ └── styles.scss # 全局样式
17├── angular.json # Angular工作区配置
18├── package.json # NPM依赖
19├── tsconfig.json # TypeScript配置
20└── README.md

3. 组件基础

组件是Angular应用的核心构建块,每个组件包含:

  • 一个HTML模板定义视图
  • 一个TypeScript类控制逻辑和数据
  • CSS样式定义外观

3.1 创建组件

使用Angular CLI创建组件:

bash
1ng generate component hero
2# 简写形式
3ng g c hero

组件文件结构:

1src/app/hero/
2├── hero.component.ts # 组件类和元数据
3├── hero.component.html # HTML模板
4├── hero.component.scss # 样式(SCSS)
5└── hero.component.spec.ts # 单元测试

基本组件结构:

hero.component.ts
typescript
1import { Component, OnInit } from '@angular/core';
2
3@Component({
4 selector: 'app-hero',
5 templateUrl: './hero.component.html',
6 styleUrls: ['./hero.component.scss']
7})
8export class HeroComponent implements OnInit {
9 // 组件属性
10 name: string = 'Windstorm';
11
12 constructor() { }
13
14 ngOnInit(): void {
15 // 组件初始化逻辑
16 }
17
18 // 组件方法
19 greet(): string {
20 return `Hello, ${this.name}!`;
21 }
22}

3.2 组件模板语法

Angular模板支持多种绑定语法:

hero.component.html
html
1<div class="hero-card">
2 <!-- 插值表达式 -->
3 <h2>{{ name }}</h2>
4
5 <!-- 属性绑定 -->
6 <img [src]="heroImageUrl" [alt]="name">
7
8 <!-- 事件绑定 -->
9 <button (click)="onSelect()">选择</button>
10
11 <!-- 双向绑定 (需要FormsModule) -->
12 <input [(ngModel)]="name">
13
14 <!-- 模板引用变量 -->
15 <input #heroInput>
16 <button (click)="updateName(heroInput.value)">更新名称</button>
17
18 <!-- 条件渲染 -->
19 <div *ngIf="isSelected">已选择!</div>
20
21 <!-- 列表渲染 -->
22 <ul>
23 <li *ngFor="let power of powers; let i = index">
24 {{ i + 1 }}. {{ power }}
25 </li>
26 </ul>
27
28 <!-- 类绑定 -->
29 <div [class.selected]="isSelected">状态展示</div>
30
31 <!-- 样式绑定 -->
32 <div [style.color]="isActive ? 'green' : 'red'">状态颜色</div>
33</div>

3.3 生命周期钩子

Angular组件有一系列生命周期钩子:

钩子时机用途
ngOnChanges输入属性变化时响应输入属性变化
ngOnInit组件初始化时执行初始化逻辑
ngDoCheck变更检测运行时自定义变更检测
ngAfterContentInit内容投影初始化后处理投影内容
ngAfterContentChecked内容投影检查后检查投影内容变化
ngAfterViewInit视图初始化后处理子视图逻辑
ngAfterViewChecked视图检查后检查视图变化
ngOnDestroy组件销毁前清理资源

示例实现:

typescript
1import { Component, OnInit, OnDestroy, OnChanges, SimpleChanges, Input } from '@angular/core';
2
3@Component({
4 selector: 'app-lifecycle',
5 template: `<div>{{ data }}</div>`
6})
7export class LifecycleComponent implements OnInit, OnChanges, OnDestroy {
8 @Input() data: string;
9
10 constructor() {
11 console.log('Constructor called');
12 }
13
14 ngOnChanges(changes: SimpleChanges) {
15 console.log('ngOnChanges called', changes);
16 }
17
18 ngOnInit() {
19 console.log('ngOnInit called');
20 }
21
22 ngOnDestroy() {
23 console.log('ngOnDestroy called');
24 // 清理代码(取消订阅、清除定时器等)
25 }
26}

4. 指令基础

Angular有三种类型的指令:

  1. 组件指令: 带有模板的指令
  2. 结构型指令: 修改DOM结构(*ngIf, *ngFor, *ngSwitch)
  3. 属性型指令: 修改元素的外观或行为(ngClass, ngStyle)

4.1 内置指令

内置指令示例
html
1<!-- NgIf -->
2<div *ngIf="isLoggedIn; else loginBlock">
3 欢迎回来,{{ username }}!
4</div>
5<ng-template #loginBlock>
6 请登录以继续
7</ng-template>
8
9<!-- NgFor -->
10<ul>
11 <li *ngFor="let item of items; let i = index; trackBy: trackByFn">
12 {{ i + 1 }} - {{ item.name }}
13 </li>
14</ul>
15
16<!-- NgSwitch -->
17<div [ngSwitch]="userRole">
18 <div *ngSwitchCase="'admin'">管理员面板</div>
19 <div *ngSwitchCase="'editor'">编辑面板</div>
20 <div *ngSwitchDefault>用户面板</div>
21</div>
22
23<!-- NgClass -->
24<div [ngClass]="{
25 'active': isActive,
26 'disabled': isDisabled,
27 'highlight': isHighlighted
28}">样式控制</div>
29
30<!-- NgStyle -->
31<div [ngStyle]="{
32 'color': textColor,
33 'font-size.px': fontSize,
34 'background-color': isImportant ? 'red' : 'transparent'
35}">样式控制</div>

4.2 自定义指令

创建自定义指令:

bash
1ng generate directive highlight
2# 简写
3ng g d highlight

实现自定义指令:

highlight.directive.ts
typescript
1import { Directive, ElementRef, HostListener, Input } from '@angular/core';
2
3@Directive({
4 selector: '[appHighlight]'
5})
6export class HighlightDirective {
7 @Input('appHighlight') highlightColor: string = 'yellow';
8 @Input() defaultColor: string = '';
9
10 constructor(private el: ElementRef) {}
11
12 @HostListener('mouseenter') onMouseEnter() {
13 this.highlight(this.highlightColor || 'yellow');
14 }
15
16 @HostListener('mouseleave') onMouseLeave() {
17 this.highlight(this.defaultColor);
18 }
19
20 private highlight(color: string) {
21 this.el.nativeElement.style.backgroundColor = color;
22 }
23}

使用自定义指令:

html
1<p appHighlight="lightblue" defaultColor="white">
2 鼠标悬停时会高亮显示
3</p>

5. 模块系统

Angular应用由模块组成,主要模块类型包括:

  1. 根模块(AppModule): 应用的引导模块
  2. 功能模块: 关注特定功能的模块
  3. 共享模块: 提供共享组件/指令/管道的模块
  4. 核心模块: 提供单例服务的模块
  5. 路由模块: 配置路由的模块

5.1 创建功能模块

bash
1ng generate module admin
2# 简写
3ng g m admin

模块定义示例:

admin.module.ts
typescript
1import { NgModule } from '@angular/core';
2import { CommonModule } from '@angular/common';
3import { FormsModule } from '@angular/forms';
4
5import { AdminRoutingModule } from './admin-routing.module';
6import { AdminDashboardComponent } from './admin-dashboard/admin-dashboard.component';
7import { UserManagementComponent } from './user-management/user-management.component';
8
9@NgModule({
10 imports: [
11 CommonModule,
12 FormsModule,
13 AdminRoutingModule
14 ],
15 declarations: [
16 AdminDashboardComponent,
17 UserManagementComponent
18 ],
19 exports: [
20 AdminDashboardComponent // 导出给其他模块使用
21 ]
22})
23export class AdminModule { }

5.2 共享模块

创建共享模块:

shared.module.ts
typescript
1import { NgModule } from '@angular/core';
2import { CommonModule } from '@angular/common';
3import { FormsModule } from '@angular/forms';
4
5import { HighlightDirective } from './directives/highlight.directive';
6import { ButtonComponent } from './components/button/button.component';
7import { CardComponent } from './components/card/card.component';
8import { TruncatePipe } from './pipes/truncate.pipe';
9
10@NgModule({
11 imports: [
12 CommonModule
13 ],
14 declarations: [
15 HighlightDirective,
16 ButtonComponent,
17 CardComponent,
18 TruncatePipe
19 ],
20 exports: [
21 CommonModule,
22 FormsModule,
23 HighlightDirective,
24 ButtonComponent,
25 CardComponent,
26 TruncatePipe
27 ]
28})
29export class SharedModule { }

6. 服务与依赖注入

服务是一个广义的类别,用于封装可重用的逻辑,如数据访问、日志记录和业务规则。

6.1 创建服务

bash
1ng generate service hero
2# 简写
3ng g s hero

服务实现:

hero.service.ts
typescript
1import { Injectable } from '@angular/core';
2import { HttpClient } from '@angular/common/http';
3import { Observable, of } from 'rxjs';
4import { catchError, map, tap } from 'rxjs/operators';
5
6import { Hero } from './hero.model';
7import { LoggingService } from './logging.service';
8
9@Injectable({
10 providedIn: 'root' // 注册为应用级单例
11})
12export class HeroService {
13 private apiUrl = 'api/heroes';
14
15 constructor(
16 private http: HttpClient,
17 private loggingService: LoggingService
18 ) { }
19
20 getHeroes(): Observable<Hero[]> {
21 return this.http.get<Hero[]>(this.apiUrl).pipe(
22 tap(_ => this.log('fetched heroes')),
23 catchError(this.handleError<Hero[]>('getHeroes', []))
24 );
25 }
26
27 getHero(id: number): Observable<Hero> {
28 const url = `${this.apiUrl}/${id}`;
29 return this.http.get<Hero>(url).pipe(
30 tap(_ => this.log(`fetched hero id=${id}`)),
31 catchError(this.handleError<Hero>(`getHero id=${id}`))
32 );
33 }
34
35 private log(message: string) {
36 this.loggingService.log(`HeroService: ${message}`);
37 }
38
39 private handleError<T>(operation = 'operation', result?: T) {
40 return (error: any): Observable<T> => {
41 console.error(error);
42 this.log(`${operation} failed: ${error.message}`);
43 return of(result as T);
44 };
45 }
46}

6.2 依赖注入

依赖注入(DI)是一种设计模式,用于实现控制反转。Angular的DI系统通过以下方式工作:

  1. 提供者(Provider): 告诉Angular如何创建服务实例
  2. 注入器(Injector): 维护服务实例并在需要时创建
  3. 依赖(Dependency): 服务或对象,注入到类中

提供服务的方式:

typescript
1// 1. 在@Injectable装饰器中提供
2@Injectable({
3 providedIn: 'root' // 应用级单例
4})
5export class LoggingService { }
6
7// 2. 在模块中提供
8@NgModule({
9 providers: [HeroService]
10})
11export class AppModule { }
12
13// 3. 在组件中提供(组件及其子组件可用)
14@Component({
15 providers: [UserService]
16})
17export class UserComponent { }

7. 表单处理

Angular提供两种表单处理方法:模板驱动表单和响应式表单。

7.1 模板驱动表单

基于模板指令,简单直观,适合简单场景:

contact-form.component.ts
typescript
1import { Component } from '@angular/core';
2import { NgForm } from '@angular/forms';
3
4interface ContactForm {
5 name: string;
6 email: string;
7 message: string;
8}
9
10@Component({
11 selector: 'app-contact-form',
12 templateUrl: './contact-form.component.html'
13})
14export class ContactFormComponent {
15 model: ContactForm = {
16 name: '',
17 email: '',
18 message: ''
19 };
20
21 submitted = false;
22
23 onSubmit(form: NgForm) {
24 if (form.valid) {
25 console.log('Form submitted', this.model);
26 this.submitted = true;
27 // 处理表单提交...
28 }
29 }
30}
contact-form.component.html
html
1<div class="container">
2 <div *ngIf="submitted" class="alert alert-success">
3 表单已提交成功!
4 </div>
5
6 <form #contactForm="ngForm" (ngSubmit)="onSubmit(contactForm)" novalidate>
7 <div class="form-group">
8 <label for="name">姓名</label>
9 <input
10 type="text"
11 class="form-control"
12 id="name"
13 name="name"
14 [(ngModel)]="model.name"
15 required
16 #name="ngModel">
17 <div [hidden]="name.valid || name.pristine" class="alert alert-danger">
18 姓名是必填项
19 </div>
20 </div>
21
22 <div class="form-group">
23 <label for="email">电子邮箱</label>
24 <input
25 type="email"
26 class="form-control"
27 id="email"
28 name="email"
29 [(ngModel)]="model.email"
30 required
31 email
32 #email="ngModel">
33 <div [hidden]="email.valid || email.pristine" class="alert alert-danger">
34 请输入有效的电子邮箱
35 </div>
36 </div>
37
38 <div class="form-group">
39 <label for="message">留言</label>
40 <textarea
41 class="form-control"
42 id="message"
43 name="message"
44 rows="5"
45 [(ngModel)]="model.message"
46 required
47 minlength="10"
48 #message="ngModel"></textarea>
49 <div [hidden]="message.valid || message.pristine" class="alert alert-danger">
50 留言至少需要10个字符
51 </div>
52 </div>
53
54 <button type="submit" class="btn btn-primary" [disabled]="!contactForm.form.valid">
55 提交
56 </button>
57 </form>
58</div>

7.2 响应式表单

基于显式模型定义,更加灵活强大,适合复杂场景:

registration.component.ts
typescript
1import { Component, OnInit } from '@angular/core';
2import { FormBuilder, FormGroup, FormControl, Validators, AbstractControl, ValidationErrors } from '@angular/forms';
3
4@Component({
5 selector: 'app-registration',
6 templateUrl: './registration.component.html'
7})
8export class RegistrationComponent implements OnInit {
9 registrationForm: FormGroup;
10 submitted = false;
11
12 constructor(private fb: FormBuilder) { }
13
14 ngOnInit() {
15 this.registrationForm = this.fb.group({
16 name: ['', [Validators.required, Validators.minLength(3)]],
17 email: ['', [Validators.required, Validators.email]],
18 password: ['', [Validators.required, Validators.minLength(8)]],
19 confirmPassword: ['', Validators.required],
20 terms: [false, Validators.requiredTrue]
21 }, {
22 validators: this.passwordMatchValidator
23 });
24 }
25
26 // 自定义验证器
27 passwordMatchValidator(control: AbstractControl): ValidationErrors | null {
28 const password = control.get('password');
29 const confirmPassword = control.get('confirmPassword');
30
31 if (password?.value !== confirmPassword?.value) {
32 return { 'passwordMismatch': true };
33 }
34
35 return null;
36 }
37
38 get f() {
39 return this.registrationForm.controls;
40 }
41
42 onSubmit() {
43 this.submitted = true;
44
45 if (this.registrationForm.invalid) {
46 return;
47 }
48
49 console.log('Registration form submitted', this.registrationForm.value);
50 // 处理表单提交...
51 }
52}
registration.component.html
html
1<div class="container">
2 <h2>用户注册</h2>
3
4 <form [formGroup]="registrationForm" (ngSubmit)="onSubmit()">
5 <div class="form-group">
6 <label for="name">姓名</label>
7 <input
8 type="text"
9 id="name"
10 formControlName="name"
11 class="form-control"
12 [ngClass]="{ 'is-invalid': submitted && f.name.errors }">
13 <div *ngIf="submitted && f.name.errors" class="invalid-feedback">
14 <div *ngIf="f.name.errors.required">姓名是必填项</div>
15 <div *ngIf="f.name.errors.minlength">姓名至少需要3个字符</div>
16 </div>
17 </div>
18
19 <div class="form-group">
20 <label for="email">电子邮箱</label>
21 <input
22 type="email"
23 id="email"
24 formControlName="email"
25 class="form-control"
26 [ngClass]="{ 'is-invalid': submitted && f.email.errors }">
27 <div *ngIf="submitted && f.email.errors" class="invalid-feedback">
28 <div *ngIf="f.email.errors.required">电子邮箱是必填项</div>
29 <div *ngIf="f.email.errors.email">请输入有效的电子邮箱地址</div>
30 </div>
31 </div>
32
33 <div class="form-group">
34 <label for="password">密码</label>
35 <input
36 type="password"
37 id="password"
38 formControlName="password"
39 class="form-control"
40 [ngClass]="{ 'is-invalid': submitted && f.password.errors }">
41 <div *ngIf="submitted && f.password.errors" class="invalid-feedback">
42 <div *ngIf="f.password.errors.required">密码是必填项</div>
43 <div *ngIf="f.password.errors.minlength">密码至少需要8个字符</div>
44 </div>
45 </div>
46
47 <div class="form-group">
48 <label for="confirmPassword">确认密码</label>
49 <input
50 type="password"
51 id="confirmPassword"
52 formControlName="confirmPassword"
53 class="form-control"
54 [ngClass]="{ 'is-invalid': submitted && (f.confirmPassword.errors || registrationForm.errors?.passwordMismatch) }">
55 <div *ngIf="submitted && (f.confirmPassword.errors || registrationForm.errors?.passwordMismatch)" class="invalid-feedback">
56 <div *ngIf="f.confirmPassword.errors?.required">确认密码是必填项</div>
57 <div *ngIf="registrationForm.errors?.passwordMismatch">两次输入的密码不匹配</div>
58 </div>
59 </div>
60
61 <div class="form-group form-check">
62 <input
63 type="checkbox"
64 id="terms"
65 formControlName="terms"
66 class="form-check-input"
67 [ngClass]="{ 'is-invalid': submitted && f.terms.errors }">
68 <label for="terms" class="form-check-label">我同意所有条款和条件</label>
69 <div *ngIf="submitted && f.terms.errors" class="invalid-feedback">
70 <div *ngIf="f.terms.errors.required">必须同意条款和条件才能继续</div>
71 </div>
72 </div>
73
74 <button type="submit" class="btn btn-primary">注册</button>
75 </form>
76</div>

8. HTTP通信

Angular提供HttpClient用于与服务器通信。

data.service.ts
typescript
1import { Injectable } from '@angular/core';
2import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
3import { Observable, throwError } from 'rxjs';
4import { catchError, retry, map } from 'rxjs/operators';
5
6import { Product } from './product.model';
7
8@Injectable({
9 providedIn: 'root'
10})
11export class DataService {
12 private apiUrl = 'https://api.example.com/products';
13
14 constructor(private http: HttpClient) { }
15
16 getProducts(category?: string): Observable<Product[]> {
17 let params = new HttpParams();
18 if (category) {
19 params = params.set('category', category);
20 }
21
22 return this.http.get<Product[]>(this.apiUrl, { params }).pipe(
23 retry(2), // 失败时重试
24 catchError(this.handleError)
25 );
26 }
27
28 getProduct(id: number): Observable<Product> {
29 return this.http.get<Product>(`${this.apiUrl}/${id}`).pipe(
30 catchError(this.handleError)
31 );
32 }
33
34 createProduct(product: Product): Observable<Product> {
35 const httpOptions = {
36 headers: new HttpHeaders({
37 'Content-Type': 'application/json'
38 })
39 };
40
41 return this.http.post<Product>(this.apiUrl, product, httpOptions).pipe(
42 catchError(this.handleError)
43 );
44 }
45
46 updateProduct(product: Product): Observable<Product> {
47 return this.http.put<Product>(`${this.apiUrl}/${product.id}`, product).pipe(
48 catchError(this.handleError)
49 );
50 }
51
52 deleteProduct(id: number): Observable<void> {
53 return this.http.delete<void>(`${this.apiUrl}/${id}`).pipe(
54 catchError(this.handleError)
55 );
56 }
57
58 private handleError(error: any) {
59 let errorMessage = '';
60 if (error.error instanceof ErrorEvent) {
61 // 客户端错误
62 errorMessage = `Error: ${error.error.message}`;
63 } else {
64 // 服务器端错误
65 errorMessage = `Error Code: ${error.status}\nMessage: ${error.message}`;
66 }
67 console.error(errorMessage);
68 return throwError(() => new Error(errorMessage));
69 }
70}

在组件中使用HTTP服务:

product-list.component.ts
typescript
1import { Component, OnInit } from '@angular/core';
2import { DataService } from '../services/data.service';
3import { Product } from '../models/product.model';
4
5@Component({
6 selector: 'app-product-list',
7 templateUrl: './product-list.component.html'
8})
9export class ProductListComponent implements OnInit {
10 products: Product[] = [];
11 loading = false;
12 error = '';
13
14 constructor(private dataService: DataService) { }
15
16 ngOnInit(): void {
17 this.getProducts();
18 }
19
20 getProducts(): void {
21 this.loading = true;
22 this.dataService.getProducts()
23 .subscribe({
24 next: (data) => {
25 this.products = data;
26 this.loading = false;
27 },
28 error: (error) => {
29 this.error = error;
30 this.loading = false;
31 }
32 });
33 }
34
35 deleteProduct(id: number): void {
36 if (confirm('确定要删除这个产品吗?')) {
37 this.dataService.deleteProduct(id)
38 .subscribe({
39 next: () => {
40 this.products = this.products.filter(p => p.id !== id);
41 },
42 error: (error) => {
43 this.error = error;
44 }
45 });
46 }
47 }
48}

9. Angular最佳实践

9.1 目录结构

推荐按功能模块组织目录:

1app/
2├── core/ # 核心功能(单例服务、拦截器等)
3│ ├── services/
4│ ├── interceptors/
5│ └── core.module.ts
6├── shared/ # 共享组件、指令、管道
7│ ├── components/
8│ ├── directives/
9│ ├── pipes/
10│ └── shared.module.ts
11├── features/ # 按功能划分的模块
12│ ├── home/
13│ ├── auth/
14│ ├── products/
15│ └── admin/
16├── models/ # 数据模型/接口
17├── utils/ # 工具函数
18├── app-routing.module.ts
19├── app.component.ts
20└── app.module.ts

9.2 性能优化

  1. 使用OnPush变更检测:减少不必要的变更检测
  2. 延迟加载模块:提高初始加载性能
  3. 追踪NgFor:为大列表使用trackBy函数
  4. 纯管道:使用纯管道进行数据转换
  5. 使用AOT编译:提前编译提高性能
typescript
1// OnPush变更检测
2@Component({
3 changeDetection: ChangeDetectionStrategy.OnPush
4})
5
6// NgFor优化
7<div *ngFor="let item of items; trackBy: trackById"></div>
8
9trackById(index: number, item: any): number {
10 return item.id;
11}

9.3 代码风格指南

  • 遵循官方Angular风格指南
  • 使用类型安全(TypeScript接口和类型)
  • 将业务逻辑从组件移至服务
  • 避免在模板中使用复杂表达式
  • 组件保持小巧、聚焦且可重用
  • 使用RxJS操作符简化异步代码

参与讨论