Skip to main content

Angular 面试题库

一、Angular 基础

1.1 什么是 Angular?它的核心特性是什么?

面试回答思路:

Angular 是由 Google 开发和维护的一个完整的前端应用框架。我可以从以下几个方面介绍它的核心特性:

1. 完整的框架

  • Angular 是一个完整的解决方案,不仅仅是一个库
  • 内置了路由、HTTP 客户端、表单处理、动画等功能
  • 提供了完整的开发工具链(CLI、测试工具等)
  • 开箱即用,不需要选择和集成各种第三方库

2. TypeScript

  • Angular 使用 TypeScript 编写
  • 提供了强类型检查和更好的 IDE 支持
  • 代码更易维护,错误更容易在编译时发现
  • 支持最新的 JavaScript 特性

3. 组件化架构

  • 应用由组件树构成
  • 每个组件包含模板、样式和逻辑
  • 组件可以嵌套和复用
  • 清晰的组件层次结构

4. 依赖注入(DI)

  • Angular 有强大的依赖注入系统
  • 提高了代码的可测试性和可维护性
  • 便于管理组件之间的依赖关系
  • 支持多级注入器

5. 双向数据绑定

  • 通过 [(ngModel)] 实现双向绑定
  • 视图和模型自动同步
  • 简化了表单处理

6. 指令系统

  • 结构型指令:*ngIf、*ngFor、*ngSwitch
  • 属性型指令:ngClass、ngStyle
  • 可以自定义指令扩展功能

7. RxJS 集成

  • 深度集成了 RxJS
  • 使用 Observable 处理异步操作
  • 强大的数据流管理能力

8. 模块化

  • 使用 NgModule 组织代码
  • 支持懒加载
  • 便于代码分割和优化

补充说明: Angular 适合构建大型企业级应用,它的学习曲线相对陡峭,但提供了完整的解决方案和最佳实践。Angular 的版本更新较快,但向后兼容性做得很好。

typescript
1// Angular 组件示例
2import { Component } from '@angular/core';
3
4@Component({
5 selector: 'app-counter',
6 template: `
7 <div>
8 <p>Count: {{ count }}</p>
9 <button (click)="increment()">Increment</button>
10 </div>
11 `,
12 styles: [`
13 div {
14 padding: 20px;
15 }
16 `]
17})
18export class CounterComponent {
19 count = 0;
20
21 increment() {
22 this.count++;
23 }
24}

1.2 Angular、AngularJS 和 Angular 2+ 的区别

面试回答思路:

这是一个常见的面试题,需要区分 AngularJS(Angular 1.x)和现代 Angular(Angular 2+)。

AngularJS(Angular 1.x):

特点:

  • 使用 JavaScript 编写
  • 基于 MVC 架构
  • 使用 $scope 和脏检查机制
  • 双向数据绑定通过 ng-model
  • 使用 Controller 和 $scope

问题:

  • 性能问题(脏检查)
  • 代码组织混乱
  • 学习曲线陡峭
  • 移动端性能差

Angular 2+(现代 Angular):

改进:

  • 使用 TypeScript 编写
  • 基于组件的架构
  • 使用 Zone.js 和变更检测
  • 性能大幅提升
  • 更好的移动端支持
  • 模块化设计
  • 更好的工具链

主要区别:

特性AngularJSAngular 2+
语言JavaScriptTypeScript
架构MVC组件化
数据绑定脏检查Zone.js
性能较差优秀
移动端不友好友好
学习曲线陡峭中等
CLI强大的 CLI

版本说明:

  • Angular 2 是完全重写,与 AngularJS 不兼容
  • Angular 2+ 指的是 Angular 2 及以后的版本
  • 从 Angular 2 开始,版本号直接叫 Angular,不再叫 Angular 2、3、4
  • 现在最新版本是 Angular 17(2023年)

迁移建议:

  • AngularJS 已经停止维护
  • 新项目应该使用 Angular 2+
  • 老项目可以考虑逐步迁移或重写

1.3 Angular 的架构和核心概念

面试回答思路:

Angular 的架构是理解整个框架的基础,我会从几个核心概念来介绍:

1. 模块(NgModule)

Angular 应用是模块化的,每个应用至少有一个根模块(AppModule)。

作用:

  • 组织代码结构
  • 管理依赖关系
  • 配置编译器
  • 支持懒加载

组成部分:

  • declarations:声明组件、指令、管道
  • imports:导入其他模块
  • providers:提供服务
  • bootstrap:启动组件
  • exports:导出组件供其他模块使用

2. 组件(Component)

组件是 Angular 应用的基本构建块。

组成部分:

  • 模板(Template):定义视图
  • 样式(Styles):定义外观
  • 类(Class):定义逻辑
  • 元数据(Metadata):通过装饰器配置

特点:

  • 每个组件都有一个选择器
  • 组件可以嵌套
  • 组件之间通过 @Input 和 @Output 通信

3. 服务(Service)

服务用于封装可复用的业务逻辑。

特点:

  • 通过依赖注入使用
  • 单例模式(默认)
  • 可以注入到组件或其他服务中
  • 用于数据共享、HTTP 请求等

4. 依赖注入(DI)

Angular 的核心特性之一。

优点:

  • 解耦代码
  • 提高可测试性
  • 便于维护
  • 支持多级注入器

5. 指令(Directive)

扩展 HTML 功能的特殊标记。

类型:

  • 组件指令:带模板的指令
  • 结构型指令:改变 DOM 结构(*ngIf、*ngFor)
  • 属性型指令:改变元素外观或行为(ngClass、ngStyle)

6. 管道(Pipe)

用于转换模板中的数据。

内置管道:

  • DatePipe:日期格式化
  • UpperCasePipe:转大写
  • LowerCasePipe:转小写
  • CurrencyPipe:货币格式化
  • PercentPipe:百分比格式化

7. 路由(Router)

管理应用的导航。

功能:

  • 路由配置
  • 路由守卫
  • 懒加载
  • 路由参数
  • 子路由

架构图:

1应用
2├── 模块(NgModule)
3│ ├── 组件(Component)
4│ │ ├── 模板(Template)
5│ │ ├── 样式(Styles)
6│ │ └── 类(Class)
7│ ├── 服务(Service)
8│ ├── 指令(Directive)
9│ ├── 管道(Pipe)
10│ └── 路由(Router)
11└── 依赖注入(DI)

实际应用: 理解 Angular 的架构对于构建大型应用至关重要。模块化的设计让代码组织更清晰,依赖注入让代码更易测试,这些都是 Angular 的优势。


二、组件通信

2.1 Angular 组件间通信的方式

面试回答思路:

Angular 提供了多种组件间通信的方式,根据组件关系的不同,可以选择不同的方案:

1. @Input 和 @Output(父子组件)

这是最基本也是最常用的通信方式。

@Input:父传子

  • 父组件通过属性绑定传递数据给子组件
  • 子组件使用 @Input 装饰器接收数据
  • 支持数据变化检测

@Output:子传父

  • 子组件使用 @Output 装饰器定义事件
  • 通过 EventEmitter 发射事件
  • 父组件监听事件并处理

特点:

  • 单向数据流,数据流向清晰
  • 类型安全
  • 易于理解和维护

2. ViewChild 和 ContentChild(父访问子)

父组件可以直接访问子组件的实例。

ViewChild:

  • 访问模板中的子组件
  • 可以调用子组件的方法
  • 可以访问子组件的属性

ContentChild:

  • 访问通过内容投影传入的组件
  • 用于组件封装

注意:

  • 只能在 ngAfterViewInit 之后访问
  • 破坏了组件的封装性,谨慎使用

3. 服务(Service)+ 依赖注入

通过共享服务实现组件间通信。

适用场景:

  • 兄弟组件通信
  • 跨层级组件通信
  • 全局状态管理

实现方式:

  • 创建一个服务
  • 在服务中定义共享数据和方法
  • 使用 RxJS Subject 或 BehaviorSubject
  • 组件注入服务并订阅数据

优点:

  • 解耦组件
  • 支持多对多通信
  • 便于状态管理

4. RxJS Subject

使用 RxJS 的 Subject 实现发布-订阅模式。

类型:

  • Subject:普通主题
  • BehaviorSubject:有初始值,会发送最新值
  • ReplaySubject:会重放历史值
  • AsyncSubject:只发送最后一个值

特点:

  • 强大的数据流管理
  • 支持多播
  • 可以组合多个数据流

5. 路由参数

通过路由传递数据。

方式:

  • 路径参数:/user/:id
  • 查询参数:/user?id=123
  • 状态参数:通过 state 传递对象

适用场景:

  • 页面间传递数据
  • 深度链接
  • 书签支持

6. LocalStorage / SessionStorage

通过浏览器存储传递数据。

适用场景:

  • 跨页面数据持久化
  • 用户偏好设置
  • 临时数据存储

注意:

  • 只能存储字符串
  • 有大小限制
  • 注意安全性

选择建议:

  • 父子组件:优先使用 @Input/@Output
  • 兄弟组件:使用共享服务
  • 跨层级:使用服务或状态管理库
  • 页面间:使用路由参数
  • 全局状态:使用 NgRx 或 Akita

在实际项目中,我会根据组件关系和数据流向选择合适的通信方式,保持代码的清晰和可维护性。

typescript
1// 1. @Input 和 @Output 示例
2// 父组件
3@Component({
4 selector: 'app-parent',
5 template: `
6 <app-child
7 [message]="parentMessage"
8 (notify)="onNotify($event)">
9 </app-child>
10 `
11})
12export class ParentComponent {
13 parentMessage = 'Hello from parent';
14
15 onNotify(data: string) {
16 console.log('Received from child:', data);
17 }
18}
19
20// 子组件
21@Component({
22 selector: 'app-child',
23 template: `
24 <p>{{ message }}</p>
25 <button (click)="sendToParent()">Send</button>
26 `
27})
28export class ChildComponent {
29 @Input() message: string;
30 @Output() notify = new EventEmitter<string>();
31
32 sendToParent() {
33 this.notify.emit('Data from child');
34 }
35}
36
37// 2. 服务通信示例
38@Injectable({
39 providedIn: 'root'
40})
41export class DataService {
42 private dataSubject = new BehaviorSubject<string>('initial');
43 data$ = this.dataSubject.asObservable();
44
45 updateData(data: string) {
46 this.dataSubject.next(data);
47 }
48}
49
50// 组件 A
51export class ComponentA {
52 constructor(private dataService: DataService) {}
53
54 sendData() {
55 this.dataService.updateData('Hello from A');
56 }
57}
58
59// 组件 B
60export class ComponentB {
61 data$: Observable<string>;
62
63 constructor(private dataService: DataService) {
64 this.data$ = this.dataService.data$;
65 }
66}

三、生命周期

3.1 Angular 组件的生命周期钩子

面试回答思路:

Angular 组件有完整的生命周期,从创建到销毁的每个阶段都有对应的钩子函数。

生命周期顺序:

1. ngOnChanges()

  • 在 ngOnInit 之前调用
  • 当输入属性(@Input)变化时调用
  • 接收 SimpleChanges 对象,包含变化的详细信息
  • 首次调用发生在 ngOnInit 之前

使用场景:

  • 响应输入属性的变化
  • 对比新旧值
  • 执行相应的逻辑

2. ngOnInit()

  • 组件初始化时调用一次
  • 在第一次 ngOnChanges 之后调用
  • 此时输入属性已经设置完成

使用场景:

  • 初始化组件
  • 获取初始数据
  • 设置订阅

3. ngDoCheck()

  • 在每次变更检测时调用
  • 可以实现自定义的变更检测逻辑
  • 调用频率很高,注意性能

使用场景:

  • 检测 Angular 无法自动检测的变化
  • 实现自定义的脏检查

4. ngAfterContentInit()

  • 内容投影(ng-content)初始化后调用一次
  • 在第一次 ngDoCheck 之后调用

使用场景:

  • 访问投影内容
  • 初始化投影内容相关的逻辑

5. ngAfterContentChecked()

  • 每次检查投影内容后调用
  • 在 ngAfterContentInit 和每次 ngDoCheck 之后调用

使用场景:

  • 响应投影内容的变化

6. ngAfterViewInit()

  • 视图初始化完成后调用一次
  • 此时可以访问 ViewChild 和 ViewChildren

使用场景:

  • 访问子组件
  • 操作 DOM
  • 初始化第三方库

7. ngAfterViewChecked()

  • 每次检查视图后调用
  • 在 ngAfterViewInit 和每次 ngAfterContentChecked 之后调用

使用场景:

  • 响应视图的变化

8. ngOnDestroy()

  • 组件销毁前调用
  • 用于清理工作

使用场景:

  • 取消订阅
  • 清除定时器
  • 解绑事件监听器
  • 释放资源
typescript
1import { Component, OnInit, OnDestroy, OnChanges,
2 AfterViewInit, SimpleChanges } from '@angular/core';
3
4@Component({
5 selector: 'app-lifecycle',
6 template: `<div>Lifecycle Demo</div>`
7})
8export class LifecycleComponent implements OnInit, OnDestroy,
9 OnChanges, AfterViewInit {
10 @Input() data: string;
11
12 // 1. 输入属性变化
13 ngOnChanges(changes: SimpleChanges) {
14 console.log('ngOnChanges', changes);
15 }
16
17 // 2. 组件初始化
18 ngOnInit() {
19 console.log('ngOnInit');
20 // 初始化数据、订阅等
21 }
22
23 // 3. 视图初始化完成
24 ngAfterViewInit() {
25 console.log('ngAfterViewInit');
26 // 访问 ViewChild、操作 DOM
27 }
28
29 // 4. 组件销毁
30 ngOnDestroy() {
31 console.log('ngOnDestroy');
32 // 清理工作:取消订阅、清除定时器
33 }
34}

执行顺序总结:

1constructor
2
3ngOnChanges (如果有 @Input)
4
5ngOnInit
6
7ngDoCheck
8
9ngAfterContentInit
10
11ngAfterContentChecked
12
13ngAfterViewInit
14
15ngAfterViewChecked
16
17(更新时重复 ngDoCheck 到 ngAfterViewChecked)
18
19ngOnDestroy

注意事项:

  1. 不要在 ngAfterViewInit 中修改视图:会导致 ExpressionChangedAfterItHasBeenCheckedError
  2. ngDoCheck 性能问题:调用频率很高,避免复杂操作
  3. 记得取消订阅:在 ngOnDestroy 中取消所有订阅,防止内存泄漏

四、依赖注入

4.1 什么是依赖注入?Angular 如何实现?

面试回答思路:

依赖注入(Dependency Injection,DI)是 Angular 的核心特性之一,也是其与其他框架的重要区别。

什么是依赖注入:

依赖注入是一种设计模式,用于实现控制反转(IoC)。简单来说,就是不在组件内部创建依赖对象,而是由外部注入。

为什么需要依赖注入:

1. 解耦代码

  • 组件不需要知道依赖的具体实现
  • 便于替换依赖
  • 提高代码的灵活性

2. 提高可测试性

  • 可以轻松注入 mock 对象
  • 便于单元测试
  • 隔离测试环境

3. 便于维护

  • 依赖关系清晰
  • 修改依赖不影响使用方
  • 符合开闭原则

4. 单例模式

  • 服务默认是单例的
  • 避免重复创建实例
  • 便于状态共享

Angular 的依赖注入系统:

1. 注入器(Injector)

Angular 有多级注入器:

  • 根注入器(Root Injector)
  • 模块注入器(Module Injector)
  • 组件注入器(Component Injector)

2. 提供者(Provider)

定义如何创建依赖对象。

提供者类型:

typescript
1// 1. 类提供者(最常用)
2providers: [UserService]
3// 等价于
4providers: [{ provide: UserService, useClass: UserService }]
5
6// 2. 值提供者
7providers: [{ provide: API_URL, useValue: 'https://api.example.com' }]
8
9// 3. 工厂提供者
10providers: [{
11 provide: Logger,
12 useFactory: (config: Config) => {
13 return config.debug ? new DebugLogger() : new ProductionLogger();
14 },
15 deps: [Config]
16}]
17
18// 4. 别名提供者
19providers: [
20 NewService,
21 { provide: OldService, useExisting: NewService }
22]

3. 注入令牌(Injection Token)

用于注入非类类型的依赖。

typescript
1import { InjectionToken } from '@angular/core';
2
3export const API_URL = new InjectionToken<string>('api.url');
4
5// 提供值
6providers: [
7 { provide: API_URL, useValue: 'https://api.example.com' }
8]
9
10// 注入使用
11constructor(@Inject(API_URL) private apiUrl: string) {}

4. 注入范围

typescript
1// 1. 根级别(推荐)
2@Injectable({
3 providedIn: 'root'
4})
5export class UserService {}
6
7// 2. 模块级别
8@NgModule({
9 providers: [UserService]
10})
11
12// 3. 组件级别
13@Component({
14 providers: [UserService]
15})

5. 可选依赖和多个依赖

typescript
1import { Optional, SkipSelf, Self, Host } from '@angular/core';
2
3constructor(
4 // 可选依赖
5 @Optional() private logger: Logger,
6
7 // 跳过当前注入器
8 @SkipSelf() private parentService: ParentService,
9
10 // 只在当前注入器查找
11 @Self() private localService: LocalService,
12
13 // 在宿主组件查找
14 @Host() private hostService: HostService
15) {}

实际应用示例:

typescript
1// 服务定义
2@Injectable({
3 providedIn: 'root'
4})
5export class DataService {
6 private data: string[] = [];
7
8 getData() {
9 return this.data;
10 }
11
12 addData(item: string) {
13 this.data.push(item);
14 }
15}
16
17// 组件使用
18@Component({
19 selector: 'app-data-list',
20 template: `
21 <ul>
22 <li *ngFor="let item of data">{{ item }}</li>
23 </ul>
24 `
25})
26export class DataListComponent implements OnInit {
27 data: string[];
28
29 constructor(private dataService: DataService) {}
30
31 ngOnInit() {
32 this.data = this.dataService.getData();
33 }
34}

依赖注入的优势:

  1. 代码更清晰,依赖关系一目了然
  2. 便于单元测试,可以轻松 mock 依赖
  3. 提高代码复用性
  4. 便于维护和扩展

五、RxJS 和响应式编程

5.1 什么是 RxJS?为什么 Angular 使用 RxJS?

面试回答思路:

RxJS 是 Angular 的核心依赖之一,理解 RxJS 对于掌握 Angular 至关重要。

什么是 RxJS:

RxJS(Reactive Extensions for JavaScript)是一个使用 Observable 序列来编写异步和基于事件的程序的库。

核心概念:

1. Observable(可观察对象)

  • 表示一个可调用的未来值或事件的集合
  • 是数据流的生产者
  • 可以发出多个值
  • 支持同步和异步

2. Observer(观察者)

  • 消费 Observable 发出的值
  • 包含三个回调函数:next、error、complete

3. Subscription(订阅)

  • 表示 Observable 的执行
  • 用于取消执行

4. Operators(操作符)

  • 用于处理数据流的纯函数
  • 可以组合使用
  • 分为创建、转换、过滤、组合等类型

5. Subject

  • 既是 Observable 又是 Observer
  • 可以多播(multicast)

为什么 Angular 使用 RxJS:

1. 统一的异步处理

  • HTTP 请求
  • 用户输入事件
  • 定时器
  • WebSocket

2. 强大的数据流管理

  • 可以轻松组合多个数据流
  • 丰富的操作符
  • 声明式编程

3. 更好的错误处理

  • 统一的错误处理机制
  • 可以重试、降级

4. 取消和清理

  • 通过 unsubscribe 取消订阅
  • 防止内存泄漏

基本使用示例:

typescript
1import { Observable, of, from, interval } from 'rxjs';
2import { map, filter, take } from 'rxjs/operators';
3
4// 1. 创建 Observable
5const observable = new Observable(subscriber => {
6 subscriber.next(1);
7 subscriber.next(2);
8 subscriber.next(3);
9 subscriber.complete();
10});
11
12// 2. 订阅 Observable
13observable.subscribe({
14 next: value => console.log(value),
15 error: err => console.error(err),
16 complete: () => console.log('Complete')
17});
18
19// 3. 使用操作符
20const numbers$ = of(1, 2, 3, 4, 5);
21
22numbers$.pipe(
23 filter(n => n % 2 === 0), // 过滤偶数
24 map(n => n * 2) // 乘以 2
25).subscribe(value => console.log(value)); // 输出: 4, 8
26
27// 4. 定时器
28const timer$ = interval(1000).pipe(
29 take(5) // 只取前 5 个值
30);
31
32timer$.subscribe(value => console.log(value)); // 0, 1, 2, 3, 4

在 Angular 中的应用:

1. HTTP 请求

typescript
1import { HttpClient } from '@angular/common/http';
2import { Observable } from 'rxjs';
3import { map, catchError } from 'rxjs/operators';
4
5@Injectable({
6 providedIn: 'root'
7})
8export class UserService {
9 constructor(private http: HttpClient) {}
10
11 getUsers(): Observable<User[]> {
12 return this.http.get<User[]>('/api/users').pipe(
13 map(users => users.filter(u => u.active)),
14 catchError(error => {
15 console.error('Error:', error);
16 return of([]);
17 })
18 );
19 }
20}
21
22// 组件中使用
23export class UserListComponent implements OnInit {
24 users$: Observable<User[]>;
25
26 constructor(private userService: UserService) {}
27
28 ngOnInit() {
29 this.users$ = this.userService.getUsers();
30 }
31}
32
33// 模板中使用 async 管道
34<div *ngFor="let user of users$ | async">
35 {{ user.name }}
36</div>

2. 表单处理

typescript
1import { FormControl } from '@angular/forms';
2import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators';
3
4export class SearchComponent implements OnInit {
5 searchControl = new FormControl();
6 results$: Observable<any[]>;
7
8 constructor(private searchService: SearchService) {}
9
10 ngOnInit() {
11 this.results$ = this.searchControl.valueChanges.pipe(
12 debounceTime(300), // 防抖
13 distinctUntilChanged(), // 去重
14 switchMap(term =>
15 this.searchService.search(term)
16 )
17 );
18 }
19}

3. 组件通信

typescript
1@Injectable({
2 providedIn: 'root'
3})
4export class MessageService {
5 private messageSubject = new Subject<string>();
6 message$ = this.messageSubject.asObservable();
7
8 sendMessage(message: string) {
9 this.messageSubject.next(message);
10 }
11}
12
13// 发送消息
14export class SenderComponent {
15 constructor(private messageService: MessageService) {}
16
17 send() {
18 this.messageService.sendMessage('Hello');
19 }
20}
21
22// 接收消息
23export class ReceiverComponent implements OnInit, OnDestroy {
24 private subscription: Subscription;
25
26 constructor(private messageService: MessageService) {}
27
28 ngOnInit() {
29 this.subscription = this.messageService.message$.subscribe(
30 message => console.log(message)
31 );
32 }
33
34 ngOnDestroy() {
35 this.subscription.unsubscribe();
36 }
37}

常用操作符:

创建操作符:

  • of:创建发出指定值的 Observable
  • from:从数组、Promise 等创建 Observable
  • interval:定时发出递增的数字
  • timer:延迟后发出值

转换操作符:

  • map:转换每个值
  • mergeMap/flatMap:映射并合并
  • switchMap:映射并切换到最新的
  • concatMap:映射并按顺序连接

过滤操作符:

  • filter:过滤值
  • take:取前 n 个值
  • takeUntil:直到另一个 Observable 发出值
  • debounceTime:防抖
  • distinctUntilChanged:去重

组合操作符:

  • combineLatest:组合最新值
  • merge:合并多个 Observable
  • concat:按顺序连接
  • forkJoin:等待所有完成

错误处理:

  • catchError:捕获错误
  • retry:重试
  • retryWhen:条件重试

最佳实践:

  1. 总是取消订阅
typescript
1export class MyComponent implements OnInit, OnDestroy {
2 private subscription: Subscription;
3
4 ngOnInit() {
5 this.subscription = this.dataService.getData().subscribe();
6 }
7
8 ngOnDestroy() {
9 this.subscription.unsubscribe();
10 }
11}
12
13// 或使用 takeUntil
14private destroy$ = new Subject<void>();
15
16ngOnInit() {
17 this.dataService.getData()
18 .pipe(takeUntil(this.destroy$))
19 .subscribe();
20}
21
22ngOnDestroy() {
23 this.destroy$.next();
24 this.destroy$.complete();
25}
  1. 使用 async 管道
html
1<!-- 自动订阅和取消订阅 -->
2<div *ngIf="data$ | async as data">
3 {{ data.name }}
4</div>
  1. 避免嵌套订阅
typescript
1// ❌ 不好
2this.service1.getData().subscribe(data1 => {
3 this.service2.getData(data1).subscribe(data2 => {
4 // ...
5 });
6});
7
8// ✅ 好
9this.service1.getData().pipe(
10 switchMap(data1 => this.service2.getData(data1))
11).subscribe(data2 => {
12 // ...
13});

六、表单处理

6.1 Angular 的表单类型和使用

面试回答思路:

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

1. 模板驱动表单(Template-Driven Forms)

特点:

  • 逻辑主要在模板中
  • 使用 ngModel 双向绑定
  • 适合简单表单
  • 类似 AngularJS 的方式

基本使用:

typescript
1import { FormsModule } from '@angular/forms';
2
3@Component({
4 selector: 'app-template-form',
5 template: `
6 <form #form="ngForm" (ngSubmit)="onSubmit(form)">
7 <input
8 name="username"
9 [(ngModel)]="user.username"
10 required
11 minlength="3"
12 #username="ngModel"
13 />
14
15 <div *ngIf="username.invalid && username.touched">
16 <div *ngIf="username.errors?.['required']">
17 Username is required
18 </div>
19 <div *ngIf="username.errors?.['minlength']">
20 Username must be at least 3 characters
21 </div>
22 </div>
23
24 <input
25 name="email"
26 [(ngModel)]="user.email"
27 required
28 email
29 />
30
31 <button type="submit" [disabled]="form.invalid">
32 Submit
33 </button>
34 </form>
35 `
36})
37export class TemplateFormComponent {
38 user = {
39 username: '',
40 email: ''
41 };
42
43 onSubmit(form: NgForm) {
44 if (form.valid) {
45 console.log(this.user);
46 }
47 }
48}

2. 响应式表单(Reactive Forms)

特点:

  • 逻辑主要在组件类中
  • 更灵活、更强大
  • 适合复杂表单
  • 更易于测试
  • 推荐使用

基本使用:

typescript
1import { ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
2
3@Component({
4 selector: 'app-reactive-form',
5 template: `
6 <form [formGroup]="userForm" (ngSubmit)="onSubmit()">
7 <input formControlName="username" />
8
9 <div *ngIf="username.invalid && username.touched">
10 <div *ngIf="username.errors?.['required']">
11 Username is required
12 </div>
13 <div *ngIf="username.errors?.['minlength']">
14 Username must be at least 3 characters
15 </div>
16 </div>
17
18 <input formControlName="email" />
19
20 <div formGroupName="address">
21 <input formControlName="street" />
22 <input formControlName="city" />
23 </div>
24
25 <button type="submit" [disabled]="userForm.invalid">
26 Submit
27 </button>
28 </form>
29 `
30})
31export class ReactiveFormComponent implements OnInit {
32 userForm: FormGroup;
33
34 constructor(private fb: FormBuilder) {}
35
36 ngOnInit() {
37 this.userForm = this.fb.group({
38 username: ['', [Validators.required, Validators.minLength(3)]],
39 email: ['', [Validators.required, Validators.email]],
40 address: this.fb.group({
41 street: [''],
42 city: ['']
43 })
44 });
45
46 // 监听值变化
47 this.userForm.valueChanges.subscribe(value => {
48 console.log('Form value:', value);
49 });
50
51 // 监听特定字段
52 this.username.valueChanges.subscribe(value => {
53 console.log('Username:', value);
54 });
55 }
56
57 get username() {
58 return this.userForm.get('username');
59 }
60
61 onSubmit() {
62 if (this.userForm.valid) {
63 console.log(this.userForm.value);
64 }
65 }
66}

3. 自定义验证器

typescript
1// 同步验证器
2function forbiddenNameValidator(nameRe: RegExp): ValidatorFn {
3 return (control: AbstractControl): ValidationErrors | null => {
4 const forbidden = nameRe.test(control.value);
5 return forbidden ? { forbiddenName: { value: control.value } } : null;
6 };
7}
8
9// 异步验证器
10function uniqueUsernameValidator(userService: UserService): AsyncValidatorFn {
11 return (control: AbstractControl): Observable<ValidationErrors | null> => {
12 return userService.checkUsername(control.value).pipe(
13 map(exists => exists ? { usernameTaken: true } : null),
14 catchError(() => of(null))
15 );
16 };
17}
18
19// 使用
20this.userForm = this.fb.group({
21 username: ['',
22 [Validators.required, forbiddenNameValidator(/admin/i)],
23 [uniqueUsernameValidator(this.userService)]
24 ]
25});

4. 动态表单

typescript
1export class DynamicFormComponent {
2 form: FormGroup;
3
4 constructor(private fb: FormBuilder) {
5 this.form = this.fb.group({
6 items: this.fb.array([])
7 });
8 }
9
10 get items() {
11 return this.form.get('items') as FormArray;
12 }
13
14 addItem() {
15 this.items.push(this.fb.group({
16 name: ['', Validators.required],
17 quantity: [1, [Validators.required, Validators.min(1)]]
18 }));
19 }
20
21 removeItem(index: number) {
22 this.items.removeAt(index);
23 }
24}

选择建议:

  • 简单表单:模板驱动表单
  • 复杂表单:响应式表单
  • 需要动态添加字段:响应式表单
  • 需要复杂验证:响应式表单

七、路由和导航

7.1 Angular Router 的使用和路由守卫

面试回答思路:

Angular Router 是 Angular 的官方路由库,功能强大且完善。

基本路由配置:

typescript
1import { Routes, RouterModule } from '@angular/router';
2
3const routes: Routes = [
4 { path: '', redirectTo: '/home', pathMatch: 'full' },
5 { path: 'home', component: HomeComponent },
6 { path: 'about', component: AboutComponent },
7 { path: 'users/:id', component: UserDetailComponent },
8 { path: '**', component: NotFoundComponent }
9];
10
11@NgModule({
12 imports: [RouterModule.forRoot(routes)],
13 exports: [RouterModule]
14})
15export class AppRoutingModule {}

路由导航:

typescript
1// 1. 模板中导航
2<a routerLink="/home">Home</a>
3<a [routerLink]="['/users', userId]">User</a>
4
5// 2. 编程式导航
6export class MyComponent {
7 constructor(private router: Router) {}
8
9 goToHome() {
10 this.router.navigate(['/home']);
11 }
12
13 goToUser(id: number) {
14 this.router.navigate(['/users', id]);
15 }
16
17 goToUserWithQuery() {
18 this.router.navigate(['/users'], {
19 queryParams: { page: 1, size: 10 }
20 });
21 }
22}

获取路由参数:

typescript
1import { ActivatedRoute } from '@angular/router';
2
3export class UserDetailComponent implements OnInit {
4 userId: string;
5
6 constructor(private route: ActivatedRoute) {}
7
8 ngOnInit() {
9 // 1. 快照方式(适用于参数不会变化的情况)
10 this.userId = this.route.snapshot.paramMap.get('id');
11
12 // 2. Observable 方式(适用于参数可能变化的情况)
13 this.route.paramMap.subscribe(params => {
14 this.userId = params.get('id');
15 });
16
17 // 3. 查询参数
18 this.route.queryParamMap.subscribe(params => {
19 const page = params.get('page');
20 const size = params.get('size');
21 });
22 }
23}

路由守卫(Route Guards):

1. CanActivate(进入守卫)

typescript
1import { Injectable } from '@angular/core';
2import { CanActivate, Router } from '@angular/router';
3
4@Injectable({
5 providedIn: 'root'
6})
7export class AuthGuard implements CanActivate {
8 constructor(
9 private authService: AuthService,
10 private router: Router
11 ) {}
12
13 canActivate(): boolean {
14 if (this.authService.isLoggedIn()) {
15 return true;
16 }
17
18 this.router.navigate(['/login']);
19 return false;
20 }
21}
22
23// 使用
24const routes: Routes = [
25 {
26 path: 'admin',
27 component: AdminComponent,
28 canActivate: [AuthGuard]
29 }
30];

2. CanDeactivate(离开守卫)

typescript
1export interface CanComponentDeactivate {
2 canDeactivate: () => boolean | Observable<boolean>;
3}
4
5@Injectable({
6 providedIn: 'root'
7})
8export class CanDeactivateGuard implements CanDeactivate<CanComponentDeactivate> {
9 canDeactivate(component: CanComponentDeactivate): boolean | Observable<boolean> {
10 return component.canDeactivate ? component.canDeactivate() : true;
11 }
12}
13
14// 组件实现
15export class EditComponent implements CanComponentDeactivate {
16 hasUnsavedChanges = false;
17
18 canDeactivate(): boolean {
19 if (this.hasUnsavedChanges) {
20 return confirm('You have unsaved changes. Do you really want to leave?');
21 }
22 return true;
23 }
24}

3. Resolve(预加载数据)

typescript
1@Injectable({
2 providedIn: 'root'
3})
4export class UserResolver implements Resolve<User> {
5 constructor(private userService: UserService) {}
6
7 resolve(route: ActivatedRouteSnapshot): Observable<User> {
8 const id = route.paramMap.get('id');
9 return this.userService.getUser(id);
10 }
11}
12
13// 路由配置
14{
15 path: 'users/:id',
16 component: UserDetailComponent,
17 resolve: { user: UserResolver }
18}
19
20// 组件中使用
21ngOnInit() {
22 this.route.data.subscribe(data => {
23 this.user = data['user'];
24 });
25}

懒加载:

typescript
1const routes: Routes = [
2 {
3 path: 'admin',
4 loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule)
5 }
6];

八、性能优化

8.1 Angular 性能优化的方法

面试回答思路:

Angular 性能优化涉及多个方面,我会从几个关键点来介绍:

1. 变更检测优化

使用 OnPush 策略:

typescript
1@Component({
2 selector: 'app-user',
3 changeDetection: ChangeDetectionStrategy.OnPush,
4 template: `<div>{{ user.name }}</div>`
5})
6export class UserComponent {
7 @Input() user: User;
8}

手动触发变更检测:

typescript
1constructor(private cdr: ChangeDetectorRef) {}
2
3updateData() {
4 // 更新数据
5 this.data = newData;
6 // 手动触发检测
7 this.cdr.markForCheck();
8}

2. TrackBy 函数

typescript
1@Component({
2 template: `
3 <div *ngFor="let item of items; trackBy: trackByFn">
4 {{ item.name }}
5 </div>
6 `
7})
8export class ListComponent {
9 items: Item[];
10
11 trackByFn(index: number, item: Item) {
12 return item.id; // 使用唯一标识
13 }
14}

3. 懒加载模块

typescript
1const routes: Routes = [
2 {
3 path: 'feature',
4 loadChildren: () => import('./feature/feature.module')
5 .then(m => m.FeatureModule)
6 }
7];

4. 使用 Pure Pipes

typescript
1@Pipe({
2 name: 'filter',
3 pure: true // 默认就是 true
4})
5export class FilterPipe implements PipeTransform {
6 transform(items: any[], searchText: string): any[] {
7 if (!items || !searchText) {
8 return items;
9 }
10 return items.filter(item =>
11 item.name.toLowerCase().includes(searchText.toLowerCase())
12 );
13 }
14}

5. 虚拟滚动

typescript
1import { ScrollingModule } from '@angular/cdk/scrolling';
2
3@Component({
4 template: `
5 <cdk-virtual-scroll-viewport itemSize="50" class="viewport">
6 <div *cdkVirtualFor="let item of items">
7 {{ item.name }}
8 </div>
9 </cdk-virtual-scroll-viewport>
10 `
11})
12export class VirtualScrollComponent {
13 items = Array.from({length: 10000}).map((_, i) => ({
14 id: i,
15 name: `Item ${i}`
16 }));
17}

6. AOT 编译

bash
1# 生产构建默认使用 AOT
2ng build --prod

7. 预加载策略

typescript
1import { PreloadAllModules } from '@angular/router';
2
3@NgModule({
4 imports: [
5 RouterModule.forRoot(routes, {
6 preloadingStrategy: PreloadAllModules
7 })
8 ]
9})
10export class AppRoutingModule {}

8. 取消订阅

typescript
1export class MyComponent implements OnDestroy {
2 private destroy$ = new Subject<void>();
3
4 ngOnInit() {
5 this.dataService.getData()
6 .pipe(takeUntil(this.destroy$))
7 .subscribe();
8 }
9
10 ngOnDestroy() {
11 this.destroy$.next();
12 this.destroy$.complete();
13 }
14}

九、总结

这份 Angular 面试题库涵盖了:

  1. ✅ Angular 基础概念和核心特性
  2. ✅ 架构和核心概念
  3. ✅ 组件通信
  4. ✅ 生命周期
  5. ✅ 依赖注入
  6. ✅ RxJS 和响应式编程
  7. ✅ 表单处理
  8. ✅ 路由和导航
  9. ✅ 性能优化

建议结合实际项目经验,深入理解每个知识点的应用场景和最佳实践。

forum

评论区 / Comments