基础概念篇
1. 什么是Angular?它与AngularJS有什么区别?
答案: Angular是由Google开发的基于TypeScript的开源Web应用框架,用于构建单页应用程序(SPA)。
Angular vs AngularJS对比:
特性 | AngularJS | Angular |
---|---|---|
语言 | JavaScript | TypeScript |
架构 | MVC | 组件化架构 |
移动支持 | 无 | 原生支持 |
SEO | 差 | 支持服务端渲染 |
性能 | 较低 | 高性能 |
学习曲线 | 中等 | 较陡峭 |
发布时间 | 2010 | 2016 |
2. Angular的核心概念有哪些?
答案:
核心构建块:
- 模块(Modules):应用的组织单元,使用@NgModule装饰器
- 组件(Components):UI的基本构建块,控制视图
- 服务(Services):可复用的业务逻辑,通过依赖注入使用
- 指令(Directives):扩展HTML元素的行为
- 管道(Pipes):数据转换和格式化
- 路由(Router):页面导航管理
核心特性:
- 依赖注入(DI)
- 双向数据绑定
- 变更检测
- Zone.js
3. Angular的架构模式是什么?
答案: Angular采用组件化架构模式,主要特点:
分层架构:
┌─────────────────┐
│ Presentation │ ← 组件层(Components)
├─────────────────┤
│ Business │ ← 服务层(Services)
├─────────────────┤
│ Data │ ← 数据层(HTTP Client, State Management)
└─────────────────┘
设计模式:
- MVP(Model-View-Presenter)
- 依赖注入模式
- 观察者模式
- 单例模式
4. 什么是TypeScript?为什么Angular使用TypeScript?
答案: TypeScript是Microsoft开发的JavaScript超集,添加了静态类型定义。
Angular使用TypeScript的原因:
- 静态类型检查:编译时发现错误
- 更好的IDE支持:自动完成、重构等
- 面向对象编程:类、接口、泛型等
- ES6+特性支持:装饰器、模块等
- 更好的代码可维护性
// TypeScript示例
interface User {
id: number;
name: string;
email: string;
}
class UserService {
getUser(id: number): User {
// 实现逻辑
}
}
组件篇
5. Angular组件的生命周期钩子有哪些?
答案:
生命周期顺序:
- ngOnChanges:输入属性变化时调用
- ngOnInit:初始化时调用(一次)
- ngDoCheck:每次变更检测时调用
- ngAfterContentInit:内容投影初始化后调用(一次)
- ngAfterContentChecked:每次检查投影内容后调用
- ngAfterViewInit:视图初始化后调用(一次)
- ngAfterViewChecked:每次检查视图后调用
- ngOnDestroy:销毁前调用
export class MyComponent implements OnInit, OnDestroy {
ngOnInit(): void {
console.log('Component initialized');
}
ngOnDestroy(): void {
console.log('Component destroyed');
}
}
6. 组件通信的方式有哪些?
答案:
1. 父子组件通信:
// 父传子:@Input
@Component({
template: `<child-component [data]="parentData"></child-component>`
})
export class ParentComponent {
parentData = 'Hello Child';
}
@Component({
selector: 'child-component'
})
export class ChildComponent {
@Input() data: string;
}
// 子传父:@Output
@Component({
selector: 'child-component',
template: `<button (click)="sendData()">Send</button>`
})
export class ChildComponent {
@Output() dataEvent = new EventEmitter<string>();
sendData() {
this.dataEvent.emit('Hello Parent');
}
}
2. 服务通信:
@Injectable()
export class DataService {
private dataSubject = new BehaviorSubject<string>('');
data$ = this.dataSubject.asObservable();
updateData(data: string) {
this.dataSubject.next(data);
}
}
3. ViewChild/ViewChildren:
@Component({
template: `<child-component #child></child-component>`
})
export class ParentComponent {
@ViewChild('child') childComponent: ChildComponent;
callChildMethod() {
this.childComponent.someMethod();
}
}
7. 什么是内容投影(Content Projection)?
答案: 内容投影是Angular中将外部内容插入到组件模板指定位置的机制,类似于Vue的插槽。
单插槽投影:
// 子组件
@Component({
selector: 'card',
template: `
<div class="card">
<ng-content></ng-content>
</div>
`
})
export class CardComponent {}
// 使用
<card>
<h1>卡片标题</h1>
<p>卡片内容</p>
</card>
多插槽投影:
// 子组件
@Component({
selector: 'card',
template: `
<div class="card">
<header>
<ng-content select="[slot=header]"></ng-content>
</header>
<main>
<ng-content select="[slot=content]"></ng-content>
</main>
</div>
`
})
export class CardComponent {}
// 使用
<card>
<h1 slot="header">标题</h1>
<p slot="content">内容</p>
</card>
8. Angular中的变更检测机制是如何工作的?
答案: Angular使用Zone.js来监听异步操作,并触发变更检测。
变更检测过程:
- 触发源:用户事件、HTTP请求、定时器等
- Zone.js拦截:拦截异步操作
- 触发检测:从根组件开始检测
- 单向检测:从上到下检测组件树
- 更新DOM:更新变化的部分
优化策略:
// OnPush策略
@Component({
changeDetection: ChangeDetectionStrategy.OnPush
})
export class OptimizedComponent {
@Input() data: any;
constructor(private cdr: ChangeDetectorRef) {}
// 手动触发检测
updateData() {
this.cdr.markForCheck();
}
}
脱离Zone:
export class MyComponent {
constructor(private ngZone: NgZone) {}
heavyTask() {
this.ngZone.runOutsideAngular(() => {
// 不触发变更检测的操作
setInterval(() => {
// 重任务
}, 100);
});
}
}
指令篇
9. Angular中的指令类型有哪些?
答案:
指令分类:
1. 组件指令(Component Directives)
- 最复杂的指令类型
- 有自己的模板
- 继承自Directive
2. 属性指令(Attribute Directives)
- 改变元素外观或行为
- 不改变DOM结构
@Directive({
selector: '[appHighlight]'
})
export class HighlightDirective {
@Input() appHighlight: string;
@HostListener('mouseenter') onMouseEnter() {
this.highlight(this.appHighlight || 'yellow');
}
@HostListener('mouseleave') onMouseLeave() {
this.highlight('');
}
private highlight(color: string) {
this.el.nativeElement.style.backgroundColor = color;
}
constructor(private el: ElementRef) {}
}
3. 结构指令(Structural Directives)
- 改变DOM结构
- 使用*语法
@Directive({
selector: '[appUnless]'
})
export class UnlessDirective {
private hasView = false;
@Input() set appUnless(condition: boolean) {
if (!condition && !this.hasView) {
this.viewContainer.createEmbeddedView(this.templateRef);
this.hasView = true;
} else if (condition && this.hasView) {
this.viewContainer.clear();
this.hasView = false;
}
}
constructor(
private templateRef: TemplateRef<any>,
private viewContainer: ViewContainerRef
) {}
}
10. 常用的内置指令有哪些?
答案:
结构指令:
<!-- *ngIf -->
<div *ngIf="isVisible; else elseTemplate">显示内容</div>
<ng-template #elseTemplate>隐藏时显示</ng-template>
<!-- *ngFor -->
<li *ngFor="let item of items; index as i; trackBy: trackByFn">
{{i}}: {{item.name}}
</li>
<!-- *ngSwitch -->
<div [ngSwitch]="value">
<p *ngSwitchCase="'A'">A</p>
<p *ngSwitchCase="'B'">B</p>
<p *ngSwitchDefault>Default</p>
</div>
属性指令:
<!-- ngClass -->
<div [ngClass]="{'active': isActive, 'disabled': isDisabled}">
<!-- ngStyle -->
<div [ngStyle]="{'color': textColor, 'font-size': fontSize + 'px'}">
<!-- ngModel -->
<input [(ngModel)]="inputValue">
服务与依赖注入篇
11. 什么是依赖注入?Angular是如何实现的?
答案: 依赖注入(DI)是一种设计模式,将依赖对象的创建和管理交给外部容器,而不是在类内部创建。
Angular DI特点:
- 分层注入器:模块级、组件级注入器
- 提供商(Providers):配置如何创建依赖
- 注入令牌(Tokens):标识依赖项
// 服务定义
@Injectable({
providedIn: 'root' // 单例,应用级注入
})
export class DataService {
constructor(private http: HttpClient) {}
}
// 组件中注入
@Component({})
export class MyComponent {
constructor(private dataService: DataService) {}
}
// 自定义提供商
@NgModule({
providers: [
{ provide: API_URL, useValue: 'https://api.example.com' },
{ provide: DataService, useClass: MockDataService },
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }
]
})
export class AppModule {}
12. Provider的不同类型有哪些?
答案:
Provider类型:
1. useClass:
{ provide: DataService, useClass: DataService }
// 简写
DataService
2. useValue:
{ provide: API_CONFIG, useValue: { url: 'https://api.com', timeout: 5000 } }
3. useFactory:
{
provide: DataService,
useFactory: (http: HttpClient) => new DataService(http),
deps: [HttpClient]
}
4. useExisting:
{ provide: NewService, useExisting: OldService }
5. multi: true:
{
provide: HTTP_INTERCEPTORS,
useClass: LoggingInterceptor,
multi: true
}
13. 什么是注入令牌(Injection Token)?
答案: 注入令牌是用于标识依赖项的唯一标识符,特别适用于注入原始值或配置对象。
// 创建令牌
export const API_CONFIG = new InjectionToken<ApiConfig>('api.config');
interface ApiConfig {
url: string;
timeout: number;
}
// 提供值
@NgModule({
providers: [
{
provide: API_CONFIG,
useValue: {
url: 'https://api.example.com',
timeout: 5000
}
}
]
})
export class AppModule {}
// 注入使用
@Injectable()
export class ApiService {
constructor(@Inject(API_CONFIG) private config: ApiConfig) {}
getData() {
console.log(this.config.url);
}
}
模块篇
14. Angular模块系统是如何工作的?
答案: Angular模块(NgModule)是组织应用的方式,将相关的组件、指令、服务等打包在一起。
NgModule元数据:
@NgModule({
declarations: [ // 声明:组件、指令、管道
AppComponent,
HeaderComponent
],
imports: [ // 导入:其他模块
CommonModule,
FormsModule,
HttpClientModule
],
providers: [ // 提供商:服务
DataService,
{ provide: API_URL, useValue: 'https://api.com' }
],
exports: [ // 导出:可被其他模块使用
HeaderComponent
],
bootstrap: [ // 启动组件(仅根模块)
AppComponent
]
})
export class AppModule {}
模块类型:
- 根模块(Root Module):AppModule,启动应用
- 特性模块(Feature Module):业务功能模块
- 共享模块(Shared Module):通用组件和服务
- 核心模块(Core Module):单例服务
15. 懒加载模块是如何实现的?
答案: 懒加载允许按需加载模块,减少初始包大小,提高应用启动速度。
路由配置:
const routes: Routes = [
{
path: 'admin',
loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule)
},
{
path: 'user',
loadChildren: () => import('./user/user.module').then(m => m.UserModule)
}
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule {}
特性模块:
@NgModule({
declarations: [
AdminComponent,
UserListComponent
],
imports: [
CommonModule,
AdminRoutingModule
]
})
export class AdminModule {}
预加载策略:
RouterModule.forRoot(routes, {
preloadingStrategy: PreloadAllModules // 预加载所有懒加载模块
})
路由篇
16. Angular路由的核心概念有哪些?
答案:
核心概念:
- Routes:路由配置数组
- Router:导航服务
- ActivatedRoute:当前激活路由信息
- RouterOutlet:路由组件显示位置
- RouterLink:声明式导航
基本配置:
const routes: Routes = [
{ path: '', redirectTo: '/dashboard', pathMatch: 'full' },
{ path: 'dashboard', component: DashboardComponent },
{ path: 'users/:id', component: UserDetailComponent },
{ path: 'admin', loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule) },
{ path: '**', component: NotFoundComponent }
];
路由参数:
// 路径参数
export class UserDetailComponent {
userId: string;
constructor(private route: ActivatedRoute) {
this.userId = this.route.snapshot.paramMap.get('id');
// 响应式获取
this.route.paramMap.subscribe(params => {
this.userId = params.get('id');
});
}
}
// 查询参数
this.router.navigate(['/users'], { queryParams: { page: 1, size: 10 } });
17. 路由守卫有哪些类型?
答案:
守卫类型:
1. CanActivate - 控制是否可以激活路由:
@Injectable()
export class AuthGuard implements CanActivate {
constructor(private auth: AuthService, private router: Router) {}
canActivate(): boolean {
if (this.auth.isLoggedIn()) {
return true;
}
this.router.navigate(['/login']);
return false;
}
}
2. CanActivateChild - 控制是否可以激活子路由:
@Injectable()
export class AdminGuard implements CanActivateChild {
canActivateChild(): boolean {
return this.checkAdminPermission();
}
}
3. CanDeactivate - 控制是否可以离开路由:
@Injectable()
export class CanDeactivateGuard implements CanDeactivate<FormComponent> {
canDeactivate(component: FormComponent): boolean {
if (component.hasUnsavedChanges()) {
return confirm('有未保存的更改,确定要离开吗?');
}
return true;
}
}
4. Resolve - 预加载数据:
@Injectable()
export class UserResolve implements Resolve<User> {
constructor(private userService: UserService) {}
resolve(route: ActivatedRouteSnapshot): Observable<User> {
return this.userService.getUser(route.paramMap.get('id'));
}
}
路由配置:
{
path: 'user/:id',
component: UserComponent,
canActivate: [AuthGuard],
canDeactivate: [CanDeactivateGuard],
resolve: { user: UserResolve }
}
表单篇
18. Angular中的表单类型有哪些?
答案:
两种表单类型:
1. 模板驱动表单(Template-driven Forms):
// 组件
export class TemplateFormComponent {
user = { name: '', email: '' };
onSubmit(form: NgForm) {
if (form.valid) {
console.log(this.user);
}
}
}
// 模板
<form #userForm="ngForm" (ngSubmit)="onSubmit(userForm)">
<input name="name" [(ngModel)]="user.name" required #name="ngModel">
<div *ngIf="name.invalid && name.touched">姓名必填</div>
<input name="email" [(ngModel)]="user.email" required email #email="ngModel">
<div *ngIf="email.invalid && email.touched">邮箱格式错误</div>
<button [disabled]="userForm.invalid">提交</button>
</form>
2. 响应式表单(Reactive Forms):
export class ReactiveFormComponent {
userForm: FormGroup;
constructor(private fb: FormBuilder) {
this.userForm = this.fb.group({
name: ['', [Validators.required, Validators.minLength(2)]],
email: ['', [Validators.required, Validators.email]],
address: this.fb.group({
street: [''],
city: ['']
})
});
}
onSubmit() {
if (this.userForm.valid) {
console.log(this.userForm.value);
}
}
get name() { return this.userForm.get('name'); }
get email() { return this.userForm.get('email'); }
}
// 模板
<form [formGroup]="userForm" (ngSubmit)="onSubmit()">
<input formControlName="name">
<div *ngIf="name?.invalid && name?.touched">
<div *ngIf="name?.errors?.['required']">姓名必填</div>
<div *ngIf="name?.errors?.['minlength']">姓名至少2个字符</div>
</div>
<input formControlName="email">
<div *ngIf="email?.invalid && email?.touched">邮箱格式错误</div>
<div formGroupName="address">
<input formControlName="street" placeholder="街道">
<input formControlName="city" placeholder="城市">
</div>
<button [disabled]="userForm.invalid">提交</button>
</form>
19. 如何创建自定义验证器?
答案:
同步验证器:
// 自定义验证器函数
export function forbiddenNameValidator(forbiddenName: string): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
const forbidden = forbiddenName && control.value?.toLowerCase().includes(forbiddenName.toLowerCase());
return forbidden ? { forbiddenName: { value: control.value } } : null;
};
}
// 使用
this.userForm = this.fb.group({
name: ['', [Validators.required, forbiddenNameValidator('admin')]]
});
异步验证器:
export function uniqueEmailValidator(userService: UserService): AsyncValidatorFn {
return (control: AbstractControl): Observable<ValidationErrors | null> => {
if (!control.value) {
return of(null);
}
return userService.checkEmailExists(control.value).pipe(
map(exists => exists ? { emailExists: true } : null),
catchError(() => of(null))
);
};
}
// 使用
this.userForm = this.fb.group({
email: ['', [Validators.required, Validators.email], [uniqueEmailValidator(this.userService)]]
});
跨字段验证器:
export const passwordMatchValidator: ValidatorFn = (control: AbstractControl): ValidationErrors | null => {
const password = control.get('password');
const confirmPassword = control.get('confirmPassword');
return password && confirmPassword && password.value !== confirmPassword.value
? { passwordMismatch: true }
: null;
};
// 使用
this.userForm = this.fb.group({
password: ['', Validators.required],
confirmPassword: ['', Validators.required]
}, { validators: passwordMatchValidator });
HTTP篇
20. Angular中如何进行HTTP通信?
答案:
HttpClient基本使用:
@Injectable({
providedIn: 'root'
})
export class ApiService {
private baseUrl = 'https://api.example.com';
constructor(private http: HttpClient) {}
// GET请求
getUsers(): Observable<User[]> {
return this.http.get<User[]>(`${this.baseUrl}/users`);
}
// POST请求
createUser(user: User): Observable<User> {
return this.http.post<User>(`${this.baseUrl}/users`, user, {
headers: new HttpHeaders({
'Content-Type': 'application/json'
})
});
}
// PUT请求
updateUser(id: number, user: User): Observable<User> {
return this.http.put<User>(`${this.baseUrl}/users/${id}`, user);
}
// DELETE请求
deleteUser(id: number): Observable<void> {
return this.http.delete<void>(`${this.baseUrl}/users/${id}`);
}
// 带参数的GET请求
getUsersWithParams(page: number, size: number): Observable<User[]> {
const params = new HttpParams()
.set('page', page.toString())
.set('size', size.toString());
return this.http.get<User[]>(`${this.baseUrl}/users`, { params });
}
}
错误处理:
export class ApiService {
getUsers(): Observable<User[]> {
return this.http.get<User[]>(`${this.baseUrl}/users`).pipe(
retry(3), // 重试3次
catchError(this.handleError)
);
}
private handleError(error: HttpErrorResponse): Observable<never> {
let errorMessage = '';
if (error.error instanceof ErrorEvent) {
// 客户端错误
errorMessage = `Error: ${error.error.message}`;
} else {
// 服务端错误
errorMessage = `Error Code: ${error.status}\nMessage: ${error.message}`;
}
console.error(errorMessage);
return throwError(() => new Error(errorMessage));
}
}
21. 什么是HTTP拦截器?如何使用?
答案: HTTP拦截器可以拦截HTTP请求和响应,用于添加通用逻辑如身份验证、日志记录、错误处理等。
身份验证拦截器:
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
constructor(private auth: AuthService) {}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
// 添加授权头
const authToken = this.auth.getToken();
if (authToken) {
const authReq = req.clone({
headers: req.headers.set('Authorization', `Bearer ${authToken}`)
});
return next.handle(authReq);
}
return next.handle(req);
}
}
日志拦截器:
@Injectable()
export class LoggingInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
console.log('Request:', req.url);
return next.handle(req).pipe(
tap(event => {
if (event instanceof HttpResponse) {
console.log('Response:', event.status, event.body);
}
})
);
}
}
注册拦截器:
@NgModule({
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: AuthInterceptor,
multi: true
},
{
provide: HTTP_INTERCEPTORS,
useClass: LoggingInterceptor,
multi: true
}
]
})
export class AppModule {}
RxJS篇
22. 什么是Observable?与Promise的区别是什么?
答案: Observable是RxJS中表示异步数据流的核心概念。
Observable vs Promise:
特性 | Observable | Promise |
---|---|---|
执行时机 | 懒执行(订阅时执行) | 立即执行 |
值的数量 | 多个值 | 单个值 |
取消支持 | 支持取消 | 不支持取消 |
操作符 | 丰富的操作符 | 基本的then/catch |
错误处理 | 多种错误处理方式 | catch |
Observable示例:
// 创建Observable
const numbers$ = new Observable<number>(subscriber => {
subscriber.next(1);
subscriber.next(2);
subscriber.next(3);
subscriber.complete();
});
// 订阅
const subscription = numbers$.subscribe({
next: value => console.log(value),
error: err => console.error(err),
complete: () => console.log('Complete')
});
// 取消订阅
subscription.unsubscribe();
23. 常用的RxJS操作符有哪些?
答案:
创建操作符:
// of - 创建发出指定值的Observable
const numbers$ = of(1, 2, 3, 4, 5);
// from - 从数组、Promise等创建Observable
const fromArray$ = from([1, 2, 3]);
const fromPromise$ = from(fetch('/api/data'));
// interval - 定时发出数值
const timer$ = interval(1000); // 每秒发出递增数字
// fromEvent - 从DOM事件创建Observable
const clicks$ = fromEvent(document, 'click');
转换操作符:
// map - 转换值
const doubled$ = numbers$.pipe(
map(x => x * 2)
);
// mergeMap - 扁平化内部Observable
const searchResults$ = searchTerms$.pipe(
mergeMap(term => this.searchService.search(term))
);
// switchMap - 切换到新的Observable,取消之前的
const latestSearch$ = searchTerms$.pipe(
switchMap(term => this.searchService.search(term))
);
// concatMap - 按顺序连接Observable
const orderedResults$ = requests$.pipe(
concatMap(request => this.processRequest(request))
);
过滤操作符:
// filter - 过滤值
const evenNumbers$ = numbers$.pipe(
filter(x => x % 2 === 0)
);
// distinctUntilChanged - 去重相邻重复值
const uniqueValues$ = values$.pipe(
distinctUntilChanged()
);
// debounceTime - 防抖
const searchInput$ = fromEvent(input, 'input').pipe(
debounceTime(300),
map(event => event.target.value)
);
// throttleTime - 节流
const throttledClicks$ = clicks$.pipe(
throttleTime(1000)
);
组合操作符:
// combineLatest - 组合最新值
const combined$ = combineLatest([
firstName$,
lastName$
]).pipe(
map(([first, last]) => `${first} ${last}`)
);
// merge - 合并多个Observable
const allEvents$ = merge(clicks$, keyPresses$, scrolls$);
// zip - 配对值
const paired$ = zip(numbers$, letters$).pipe(
map(([num, letter]) => `${num}${letter}`)
);
24. 如何处理内存泄漏和取消订阅?
答案:
手动取消订阅:
export class MyComponent implements OnInit, OnDestroy {
private destroy$ = new Subject<void>();
ngOnInit() {
// 方法1:使用takeUntil
this.dataService.getData().pipe(
takeUntil(this.destroy$)
).subscribe(data => {
console.log(data);
});
// 方法2:保存subscription
this.subscription = this.dataService.getData().subscribe(data => {
console.log(data);
});
}
ngOnDestroy() {
// 方法1:发出销毁信号
this.destroy$.next();
this.destroy$.complete();
// 方法2:手动取消订阅
if (this.subscription) {
this.subscription.unsubscribe();
}
}
}
使用async管道(推荐):
export class MyComponent {
data$ = this.dataService.getData(); // 不需要手动取消订阅
constructor(private dataService: DataService) {}
}
// 模板中使用async管道
<div *ngIf="data$ | async as data">
{{data.name}}
</div>
自定义操作符:
// 自动取消订阅的操作符
export function untilDestroyed(component: any) {
return <T>(source: Observable<T>) => {
const destroy$ = new Subject();
const originalDestroy = component.ngOnDestroy;
component.ngOnDestroy = function() {
destroy$.next();
destroy$.complete();
if (originalDestroy) {
originalDestroy.apply(this, arguments);
}
};
return source.pipe(takeUntil(destroy$));
};
}
// 使用
this.dataService.getData().pipe(
untilDestroyed(this)
).subscribe(data => console.log(data));
测试篇
25. Angular中的测试类型有哪些?
答案:
测试类型:
1. 单元测试(Unit Tests):
describe('UserService', () => {
let service: UserService;
let httpMock: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [UserService]
});
service = TestBed.inject(UserService);
httpMock = TestBed.inject(HttpTestingController);
});
it('should fetch users', () => {
const mockUsers = [{ id: 1, name: 'John' }];
service.getUsers().subscribe(users => {
expect(users).toEqual(mockUsers);
});
const req = httpMock.expectOne('/api/users');
expect(req.request.method).toBe('GET');
req.flush(mockUsers);
});
afterEach(() => {
httpMock.verify();
});
});
2. 组件测试:
describe('UserComponent', () => {
let component: UserComponent;
let fixture: ComponentFixture<UserComponent>;
let userService: jasmine.SpyObj<UserService>;
beforeEach(() => {
const spy = jasmine.createSpyObj('UserService', ['getUsers']);
TestBed.configureTestingModule({
declarations: [UserComponent],
providers: [
{ provide: UserService, useValue: spy }
]
});
fixture = TestBed.createComponent(UserComponent);
component = fixture.componentInstance;
userService = TestBed.inject(UserService) as jasmine.SpyObj<UserService>;
});
it('should display users', () => {
const mockUsers = [{ id: 1, name: 'John' }];
userService.getUsers.and.returnValue(of(mockUsers));
fixture.detectChanges();
const compiled = fixture.nativeElement;
expect(compiled.querySelector('.user-name').textContent).toContain('John');
});
});
3. 集成测试:
describe('App Integration', () => {
it('should navigate to user detail', fakeAsync(() => {
const fixture = TestBed.createComponent(AppComponent);
const router = TestBed.inject(Router);
const location = TestBed.inject(Location);
fixture.detectChanges();
router.navigate(['/users/1']);
tick();
expect(location.path()).toBe('/users/1');
}));
});
26. 如何测试异步操作?
答案:
使用fakeAsync和tick:
it('should handle async operation', fakeAsync(() => {
let result: string;
setTimeout(() => {
result = 'async result';
}, 1000);
tick(1000); // 模拟时间流逝
expect(result).toBe('async result');
}));
测试Observable:
it('should handle observable', () => {
const testData = { id: 1, name: 'Test' };
service.getData().subscribe(data => {
expect(data).toEqual(testData);
});
const req = httpMock.expectOne('/api/data');
req.flush(testData);
});
使用done回调:
it('should handle promise', (done) => {
service.getDataPromise().then(data => {
expect(data).toBeTruthy();
done();
});
});
性能优化篇
27. Angular性能优化的策略有哪些?
答案:
变更检测优化:
// 1. OnPush变更检测策略
@Component({
selector: 'app-user',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `<div>{{user.name}}</div>`
})
export class UserComponent {
@Input() user: User;
}
// 2. 使用Immutable数据
updateUser(newName: string) {
this.user = { ...this.user, name: newName }; // 创建新对象
}
// 3. TrackBy函数优化*ngFor
trackByUserId(index: number, user: User): number {
return user.id;
}
// 模板中使用
<div *ngFor="let user of users; trackBy: trackByUserId">
{{user.name}}
</div>
懒加载和预加载:
// 路由懒加载
const routes: Routes = [
{
path: 'feature',
loadChildren: () => import('./feature/feature.module').then(m => m.FeatureModule)
}
];
// 预加载策略
RouterModule.forRoot(routes, {
preloadingStrategy: PreloadAllModules
})
代码分割和Tree Shaking:
// 动态导入
async loadUtility() {
const { heavyUtility } = await import('./heavy-utility');
return heavyUtility();
}
// 使用具体导入
import { map, filter } from 'rxjs/operators'; // ✅ 好
import * as rxjs from 'rxjs'; // ❌ 不好
服务端渲染(SSR):
ng add @nguniversal/express-engine
npm run build:ssr
npm run serve:ssr
28. 如何优化大型列表的渲染性能?
答案:
虚拟滚动:
// 安装CDK
npm install @angular/cdk
// 使用虚拟滚动
@Component({
template: `
<cdk-virtual-scroll-viewport itemSize="50" class="viewport">
<div *cdkVirtualFor="let item of items; trackBy: trackByFn">
{{item.name}}
</div>
</cdk-virtual-scroll-viewport>
`,
styles: [`
.viewport {
height: 400px;
width: 100%;
}
`]
})
export class VirtualScrollComponent {
items = Array.from({length: 10000}, (_, i) => ({id: i, name: `Item ${i}`}));
trackByFn(index: number, item: any) {
return item.id;
}
}
分页和无限滚动:
@Component({
template: `
<div *ngFor="let item of visibleItems">{{item.name}}</div>
<div #sentinel></div>
`
})
export class InfiniteScrollComponent implements OnInit, AfterViewInit {
@ViewChild('sentinel') sentinel: ElementRef;
allItems: Item[] = [];
visibleItems: Item[] = [];
private page = 0;
private pageSize = 20;
ngAfterViewInit() {
const observer = new IntersectionObserver(entries => {
if (entries[0].isIntersecting) {
this.loadMore();
}
});
observer.observe(this.sentinel.nativeElement);
}
loadMore() {
const start = this.page * this.pageSize;
const end = start + this.pageSize;
this.visibleItems = [...this.visibleItems, ...this.allItems.slice(start, end)];
this.page++;
}
}
状态管理篇
29. Angular中有哪些状态管理方案?
答案:
1. 服务 + BehaviorSubject:
@Injectable({
providedIn: 'root'
})
export class StateService {
private userSubject = new BehaviorSubject<User | null>(null);
user$ = this.userSubject.asObservable();
updateUser(user: User) {
this.userSubject.next(user);
}
getUser(): User | null {
return this.userSubject.value;
}
}
2. NgRx:
// Actions
export const loadUsers = createAction('[User] Load Users');
export const loadUsersSuccess = createAction(
'[User] Load Users Success',
props<{ users: User[] }>()
);
// Reducer
const userReducer = createReducer(
initialState,
on(loadUsers, state => ({ ...state, loading: true })),
on(loadUsersSuccess, (state, { users }) => ({
...state,
users,
loading: false
}))
);
// Effects
@Injectable()
export class UserEffects {
loadUsers$ = createEffect(() =>
this.actions$.pipe(
ofType(loadUsers),
switchMap(() =>
this.userService.getUsers().pipe(
map(users => loadUsersSuccess({ users }))
)
)
)
);
constructor(
private actions$: Actions,
private userService: UserService
) {}
}
// Selectors
export const selectUsers = createSelector(
selectUserState,
state => state.users
);
// 组件中使用
@Component({})
export class UserListComponent {
users$ = this.store.select(selectUsers);
constructor(private store: Store) {}
loadUsers() {
this.store.dispatch(loadUsers());
}
}
3. Akita:
// Store
export interface UserState extends EntityState<User> {
loading: boolean;
}
@Injectable({ providedIn: 'root' })
@StoreConfig({ name: 'user' })
export class UserStore extends EntityStore<UserState> {
constructor() {
super({ loading: false });
}
}
// Query
@Injectable({ providedIn: 'root' })
export class UserQuery extends QueryEntity<UserState> {
loading$ = this.select('loading');
constructor(protected store: UserStore) {
super(store);
}
}
// Service
@Injectable({ providedIn: 'root' })
export class UserService {
constructor(
private userStore: UserStore,
private http: HttpClient
) {}
getUsers() {
this.userStore.setLoading(true);
return this.http.get<User[]>('/api/users').pipe(
tap(users => {
this.userStore.set(users);
this.userStore.setLoading(false);
})
);
}
}
30. 如何在Angular中实现缓存?
答案:
HTTP缓存:
@Injectable()
export class CacheInterceptor implements HttpInterceptor {
private cache = new Map<string, HttpResponse<any>>();
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
if (req.method === 'GET') {
const cachedResponse = this.cache.get(req.url);
if (cachedResponse) {
return of(cachedResponse);
}
}
return next.handle(req).pipe(
tap(event => {
if (event instanceof HttpResponse && req.method === 'GET') {
this.cache.set(req.url, event);
}
})
);
}
}
内存缓存服务:
@Injectable({
providedIn: 'root'
})
export class CacheService {
private cache = new Map<string, { data: any; timestamp: number; ttl: number }>();
set(key: string, data: any, ttl: number = 300000): void { // 5分钟TTL
this.cache.set(key, {
data,
timestamp: Date.now(),
ttl
});
}
get(key: string): any | null {
const cached = this.cache.get(key);
if (!cached) {
return null;
}
if (Date.now() - cached.timestamp > cached.ttl) {
this.cache.delete(key);
return null;
}
return cached.data;
}
clear(): void {
this.cache.clear();
}
}
// 在服务中使用缓存
@Injectable()
export class DataService {
constructor(
private http: HttpClient,
private cache: CacheService
) {}
getData(id: string): Observable<any> {
const cacheKey = `data_${id}`;
const cached = this.cache.get(cacheKey);
if (cached) {
return of(cached);
}
return this.http.get(`/api/data/${id}`).pipe(
tap(data => this.cache.set(cacheKey, data))
);
}
}
安全篇
31. Angular中的安全防护措施有哪些?
答案:
XSS防护:
// Angular自动转义HTML
@Component({
template: `
<div>{{userInput}}</div> <!-- 自动转义 -->
<div [innerHTML]="trustedHtml"></div> <!-- 需要信任的HTML -->
`
})
export class SafeComponent {
userInput = '<script>alert("xss")</script>'; // 会被转义
constructor(private sanitizer: DomSanitizer) {}
get trustedHtml() {
return this.sanitizer.bypassSecurityTrustHtml(this.someHtml);
}
}
CSRF防护:
// HttpClientXsrfModule配置
@NgModule({
imports: [
HttpClientModule,
HttpClientXsrfModule.withOptions({
cookieName: 'XSRF-TOKEN',
headerName: 'X-XSRF-TOKEN'
})
]
})
export class AppModule {}
内容安全策略(CSP):
<!-- index.html -->
<meta http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self' 'unsafe-eval';">
JWT Token管理:
@Injectable()
export class AuthService {
private tokenKey = 'auth_token';
setToken(token: string): void {
localStorage.setItem(this.tokenKey, token);
}
getToken(): string | null {
return localStorage.getItem(this.tokenKey);
}
removeToken(): void {
localStorage.removeItem(this.tokenKey);
}
isTokenExpired(): boolean {
const token = this.getToken();
if (!token) return true;
const payload = JSON.parse(atob(token.split('.')[1]));
return Date.now() >= payload.exp * 1000;
}
}
32. 如何实现角色基础的访问控制?
答案:
权限守卫:
export enum Role {
Admin = 'admin',
User = 'user',
Guest = 'guest'
}
@Injectable()
export class RoleGuard implements CanActivate {
constructor(
private auth: AuthService,
private router: Router
) {}
canActivate(route: ActivatedRouteSnapshot): boolean {
const requiredRoles = route.data['roles'] as Role[];
const userRole = this.auth.getUserRole();
if (requiredRoles && !requiredRoles.includes(userRole)) {
this.router.navigate(['/unauthorized']);
return false;
}
return true;
}
}
// 路由配置
{
path: 'admin',
component: AdminComponent,
canActivate: [RoleGuard],
data: { roles: [Role.Admin] }
}
权限指令:
@Directive({
selector: '[hasRole]'
})
export class HasRoleDirective implements OnInit {
@Input() hasRole: Role[];
constructor(
private templateRef: TemplateRef<any>,
private viewContainer: ViewContainerRef,
private auth: AuthService
) {}
ngOnInit() {
const userRole = this.auth.getUserRole();
if (this.hasRole.includes(userRole)) {
this.viewContainer.createEmbeddedView(this.templateRef);
} else {
this.viewContainer.clear();
}
}
}
// 使用
<button *hasRole="[Role.Admin]">删除用户</button>
高级特性篇
33. Angular中的动态组件如何实现?
答案:
使用ComponentFactoryResolver(Angular 13之前):
@Component({
template: `<div #container></div>`
})
export class DynamicHostComponent {
@ViewChild('container', { read: ViewContainerRef }) container: ViewContainerRef;
constructor(private componentFactoryResolver: ComponentFactoryResolver) {}
loadComponent(componentType: any) {
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(componentType);
this.container.clear();
const componentRef = this.container.createComponent(componentFactory);
// 设置输入属性
componentRef.instance.data = 'some data';
return componentRef;
}
}
使用ViewContainerRef.createComponent(Angular 13+):
@Component({
template: `<div #container></div>`
})
export class DynamicHostComponent {
@ViewChild('container', { read: ViewContainerRef }) container: ViewContainerRef;
loadComponent(componentType: any) {
this.container.clear();
const componentRef = this.container.createComponent(componentType);
componentRef.setInput('data', 'some data');
return componentRef;
}
}
动态表单示例:
interface FieldConfig {
type: 'input' | 'select' | 'checkbox';
name: string;
label: string;
options?: string[];
}
@Component({
template: `
<form [formGroup]="form">
<div #fieldContainer></div>
</form>
`
})
export class DynamicFormComponent implements OnInit {
@ViewChild('fieldContainer', { read: ViewContainerRef }) container: ViewContainerRef;
@Input() fields: FieldConfig[] = [];
form: FormGroup;
constructor(private fb: FormBuilder) {}
ngOnInit() {
this.buildForm();
this.loadFields();
}
private buildForm() {
const group: any = {};
this.fields.forEach(field => {
group[field.name] = [''];
});
this.form = this.fb.group(group);
}
private loadFields() {
this.fields.forEach(field => {
const component = this.getComponentType(field.type);
const componentRef = this.container.createComponent(component);
componentRef.setInput('config', field);
componentRef.setInput('formControl', this.form.get(field.name));
});
}
private getComponentType(type: string) {
switch (type) {
case 'input': return InputFieldComponent;
case 'select': return SelectFieldComponent;
case 'checkbox': return CheckboxFieldComponent;
default: return InputFieldComponent;
}
}
}
34. 什么是Angular Elements?
答案: Angular Elements允许将Angular组件转换为自定义元素(Custom Elements),可以在任何HTML页面中使用。
创建Angular Element:
// 安装
npm install @angular/elements
// 创建自定义元素
import { createCustomElement } from '@angular/elements';
@Component({
selector: 'app-hello',
template: `<h1>Hello {{name}}!</h1>`,
encapsulation: ViewEncapsulation.ShadowDom
})
export class HelloComponent {
@Input() name = 'World';
}
@NgModule({
declarations: [HelloComponent],
imports: [BrowserModule],
entryComponents: [HelloComponent] // Angular 9之前需要
})
export class AppModule {
constructor(private injector: Injector) {}
ngDoBootstrap() {
const helloElement = createCustomElement(HelloComponent, { injector: this.injector });
customElements.define('hello-element', helloElement);
}
}
使用自定义元素:
<!-- 在任何HTML页面中使用 -->
<hello-element name="Angular"></hello-element>
<script>
// 动态设置属性
const element = document.querySelector('hello-element');
element.name = 'Dynamic Name';
</script>
35. Angular中的微前端架构如何实现?
答案:
使用Module Federation:
// webpack.config.js (子应用)
const ModuleFederationPlugin = require('@module-federation/webpack');
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'mfe1',
filename: 'remoteEntry.js',
exposes: {
'./Module': './src/app/feature/feature.module.ts'
},
shared: {
'@angular/core': { singleton: true },
'@angular/common': { singleton: true },
'@angular/router': { singleton: true }
}
})
]
};
// webpack.config.js (主应用)
new ModuleFederationPlugin({
name: 'shell',
remotes: {
mfe1: 'mfe1@http://localhost:4201/remoteEntry.js'
},
shared: {
'@angular/core': { singleton: true },
'@angular/common': { singleton: true },
'@angular/router': { singleton: true }
}
})
路由配置:
// 主应用路由
const routes: Routes = [
{
path: 'mfe1',
loadChildren: () => import('mfe1/Module').then(m => m.FeatureModule)
}
];
Single-SPA方案:
// 子应用入口
import { NgZone } from '@angular/core';
import { Router } from '@angular/router';
import { singleSpaAngular, getSingleSpaExtraProviders } from 'single-spa-angular';
const lifecycles = singleSpaAngular({
bootstrapFunction: singleSpaProps => {
return platformBrowserDynamic(getSingleSpaExtraProviders()).bootstrapModule(AppModule);
},
template: '<app-root />',
Router,
NgZone
});
export const bootstrap = lifecycles.bootstrap;
export const mount = lifecycles.mount;
export const unmount = lifecycles.unmount;
最佳实践篇
36. Angular项目的文件结构最佳实践
答案:
推荐的项目结构:
src/
├── app/
│ ├── core/ # 核心模块(单例服务)
│ │ ├── guards/
│ │ ├── interceptors/
│ │ ├── services/
│ │ └── core.module.ts
│ ├── shared/ # 共享模块
│ │ ├── components/
│ │ ├── directives/
│ │ ├── pipes/
│ │ └── shared.module.ts
│ ├── features/ # 特性模块
│ │ ├── user/
│ │ │ ├── components/
│ │ │ ├── services/
│ │ │ ├── user-routing.module.ts
│ │ │ └── user.module.ts
│ ├── layouts/ # 布局组件
│ ├── app-routing.module.ts
│ ├── app.component.ts
│ └── app.module.ts
├── assets/ # 静态资源
│ ├── images/
│ ├── icons/
│ └── i18n/
├── environments/ # 环境配置
└── styles/ # 全局样式
├── _variables.scss
├── _mixins.scss
└── styles.scss
模块组织原则:
- Core Module:放置单例服务,只被AppModule导入
- Shared Module:放置通用组件、指令、管道
- Feature Module:按业务功能组织,可懒加载
- Layout Module:布局相关组件
37. Angular编码规范和最佳实践
答案:
命名规范:
// 1. 使用kebab-case命名文件
user-detail.component.ts
user.service.ts
auth.guard.ts
// 2. 类名使用PascalCase
export class UserDetailComponent {}
export class UserService {}
export class AuthGuard {}
// 3. 变量和方法使用camelCase
userName: string;
getUserData(): void {}
// 4. 常量使用SCREAMING_SNAKE_CASE
export const API_BASE_URL = 'https://api.example.com';
组件设计原则:
// 1. 单一职责原则
@Component({
selector: 'app-user-list', // 只负责用户列表显示
template: `
<div *ngFor="let user of users">
<app-user-item [user]="user" (delete)="onDelete($event)"></app-user-item>
</div>
`
})
export class UserListComponent {
@Input() users: User[];
@Output() delete = new EventEmitter<number>();
onDelete(userId: number) {
this.delete.emit(userId);
}
}
// 2. 使用OnPush策略提高性能
@Component({
changeDetection: ChangeDetectionStrategy.OnPush
})
export class OptimizedComponent {}
// 3. 实现生命周期接口
export class MyComponent implements OnInit, OnDestroy {
ngOnInit(): void {}
ngOnDestroy(): void {}
}
// 4. 使用trackBy优化*ngFor
trackByUserId = (index: number, user: User) => user.id;
服务设计原则:
// 1. 使用依赖注入
@Injectable({
providedIn: 'root' // 单例服务
})
export class UserService {
constructor(private http: HttpClient) {}
}
// 2. 返回Observable而不是Promise
getUserById(id: number): Observable<User> {
return this.http.get<User>(`/api/users/${id}`);
}
// 3. 错误处理
getUserById(id: number): Observable<User> {
return this.http.get<User>(`/api/users/${id}`).pipe(
retry(3),
catchError(this.handleError)
);
}
private handleError(error: HttpErrorResponse): Observable<never> {
console.error('An error occurred:', error.error);
return throwError(() => new Error('Something went wrong'));
}
模板最佳实践:
<!-- 1. 使用async管道 -->
<div *ngIf="user$ | async as user">
{{user.name}}
</div>
<!-- 2. 避免在模板中调用方法 -->
<!-- 不好 -->
<div>{{getFullName()}}</div>
<!-- 好 -->
<div>{{fullName}}</div>
<!-- 3. 使用trackBy提高性能 -->
<div *ngFor="let item of items; trackBy: trackByFn">
{{item.name}}
</div>
<!-- 4. 合理使用*ngIf和hidden -->
<!-- 频繁切换使用hidden -->
<div [hidden]="!isVisible">Content</div>
<!-- 不频繁切换使用*ngIf -->
<div *ngIf="isVisible">Content</div>
38. 如何进行Angular应用的SEO优化?
答案:
服务端渲染(SSR):
# 添加Angular Universal
ng add @nguniversal/express-engine
# 构建和运行SSR
npm run build:ssr
npm run serve:ssr
预渲染(Prerendering):
# 添加预渲染功能
ng add @nguniversal/express-engine
# 构建预渲染版本
npm run prerender
Meta标签管理:
import { Meta, Title } from '@angular/platform-browser';
@Component({})
export class ProductDetailComponent implements OnInit {
constructor(
private meta: Meta,
private title: Title,
private route: ActivatedRoute
) {}
ngOnInit() {
this.route.data.subscribe(data => {
const product = data.product;
// 设置页面标题
this.title.setTitle(`${product.name} - 商品详情`);
// 设置Meta标签
this.meta.updateTag({ name: 'description', content: product.description });
this.meta.updateTag({ name: 'keywords', content: product.keywords });
// Open Graph标签
this.meta.updateTag({ property: 'og:title', content: product.name });
this.meta.updateTag({ property: 'og:description', content: product.description });
this.meta.updateTag({ property: 'og:image', content: product.image });
// Twitter Card标签
this.meta.updateTag({ name: 'twitter:card', content: 'summary_large_image' });
this.meta.updateTag({ name: 'twitter:title', content: product.name });
});
}
}
结构化数据:
@Injectable()
export class SeoService {
constructor(@Inject(DOCUMENT) private document: Document) {}
addStructuredData(data: any) {
const script = this.document.createElement('script');
script.type = 'application/ld+json';
script.text = JSON.stringify(data);
this.document.head.appendChild(script);
}
addProductStructuredData(product: Product) {
const structuredData = {
'@context': 'https://schema.org/',
'@type': 'Product',
'name': product.name,
'description': product.description,
'image': product.image,
'offers': {
'@type': 'Offer',
'price': product.price,
'priceCurrency': 'CNY'
}
};
this.addStructuredData(structuredData);
}
}
39. Angular国际化(i18n)如何实现?
答案:
配置国际化:
# 添加国际化支持
ng add @angular/localize
# 提取文本
ng extract-i18n
# 构建不同语言版本
ng build --localize
标记需要翻译的文本:
<!-- 使用i18n属性 -->
<h1 i18n="@@welcome">欢迎</h1>
<!-- 带描述和含义 -->
<p i18n="user.greeting|问候用户">你好,{{userName}}</p>
<!-- 复数形式 -->
<span i18n>{count, plural, =0 {没有消息} =1 {1条消息} other {{{count}}条消息}}</span>
<!-- ICU表达式 -->
<span i18n>{gender, select, male {他} female {她} other {它}}</span>
在组件中使用:
import { LOCALE_ID, Inject } from '@angular/core';
@Component({})
export class MyComponent {
constructor(@Inject(LOCALE_ID) public locale: string) {}
// 使用$localize标记字符串
message = $localize`欢迎使用我们的应用`;
// 带插值的本地化
greetUser(name: string) {
return $localize`你好,${name}`;
}
}
日期和数字管道:
<!-- 日期本地化 -->
<p>{{ today | date:'medium':locale }}</p>
<!-- 数字本地化 -->
<p>{{ price | currency:'CNY':'symbol':'1.2-2':locale }}</p>
<!-- 百分比 -->
<p>{{ ratio | percent:'1.1-1':locale }}</p>
动态切换语言:
@Injectable()
export class LocaleService {
private currentLocale = 'zh-CN';
setLocale(locale: string) {
this.currentLocale = locale;
// 重新加载应用或动态加载翻译文件
window.location.reload();
}
getCurrentLocale(): string {
return this.currentLocale;
}
}
40. Angular应用的部署策略有哪些?
答案:
构建优化:
# 生产环境构建
ng build --prod
# 启用AOT编译
ng build --aot
# 分析包大小
ng build --prod --source-map
npm install -g webpack-bundle-analyzer
webpack-bundle-analyzer dist/*/main.js
部署配置文件:
// angular.json
{
"projects": {
"my-app": {
"architect": {
"build": {
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"extractCss": true,
"namedChunks": false,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"budgets": [
{
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "5mb"
}
]
}
}
}
}
}
}
}
Nginx配置示例:
server {
listen 80;
server_name your-domain.com;
root /var/www/your-app/dist;
index index.html;
# 处理Angular路由
location / {
try_files $uri $uri/ /index.html;
}
# 静态资源缓存
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# 压缩
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
}
Docker部署:
# 多阶段构建
FROM node:16-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build --prod
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
CI/CD流水线(GitHub Actions示例):
name: Deploy Angular App
on:
push:
branches: [main]
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: '16'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm run test -- --watch=false --browsers=ChromeHeadless
- name: Build
run: npm run build --prod
- name: Deploy to S3
run: aws s3 sync dist/ s3://your-bucket-name --delete
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
面试技巧和项目经验篇
41. 常见的Angular面试陷阱题
问题1:为什么需要使用OnPush变更检测策略? 答案: 默认的变更检测策略会检查所有组件,OnPush策略只在以下情况触发检测:
- @Input属性引用发生变化
- 事件触发(DOM事件、@Output事件)
- 手动调用ChangeDetectorRef.markForCheck()
- Observable发出新值(使用async管道)
问题2:Angular中的单例服务是如何保证的? 答案: 通过providedIn: 'root'或在根模块的providers中注册,Angular的依赖注入系统会为整个应用创建唯一实例。子模块中重复提供会创建新实例。
问题3:ngFor和ngIf能同时使用吗? 答案: 不能在同一个元素上同时使用。解决方案:
<!-- 使用ng-container -->
<ng-container *ngFor="let item of items">
<div *ngIf="item.visible">{{item.name}}</div>
</ng-container>
<!-- 或者使用管道过滤 -->
<div *ngFor="let item of items | filter:isVisible">{{item.name}}</div>
42. 项目经验相关问题的回答框架
问题:描述一个你在Angular项目中遇到的性能问题及解决方案
回答框架:
- 背景描述:项目规模、遇到的具体性能问题
- 问题分析:使用的分析工具(Chrome DevTools、Angular DevTools)
- 解决方案:具体采用的优化策略
- 效果评估:优化前后的性能对比数据
- 经验总结:从中学到的经验和最佳实践
示例回答: "在一个电商项目中,商品列表页面在显示1000+商品时出现卡顿。通过Chrome DevTools分析发现变更检测耗时过长。我采用了以下优化方案:
- 使用OnPush变更检测策略
- 实现虚拟滚动(CDK Virtual Scrolling)
- 添加trackBy函数优化*ngFor
- 使用异步管道避免手动订阅 最终页面渲染时间从3秒降至500ms,用户体验显著提升。"
问题:如何设计一个可复用的Angular组件库?
回答要点:
- API设计:简洁明了的输入输出接口
- 主题定制:支持CSS变量和主题切换
- 无障碍访问:遵循ARIA标准
- 文档和示例:详细的使用文档和在线示例
- 测试覆盖:单元测试和端到端测试
- 版本管理:语义化版本控制
- TypeScript支持:完整的类型定义
43. Angular新特性和发展趋势
Angular 15+ 新特性:
- Standalone Components:独立组件,减少模块样板代码
- Optional Injectors:可选的依赖注入
- Directive Composition API:指令组合
- Image Optimization:图片优化指令
- Angular CLI Auto-completion:CLI自动补全
Standalone Components示例:
@Component({
selector: 'app-standalone',
standalone: true,
imports: [CommonModule, FormsModule],
template: `<div>Standalone Component</div>`
})
export class StandaloneComponent {}
// 引导应用
bootstrapApplication(StandaloneComponent, {
providers: [
importProvidersFrom(BrowserModule),
provideRouter(routes)
]
});
未来发展趋势:
- 更好的性能:Ivy渲染引擎持续优化
- 更小的包体积:Tree-shaking和代码分割
- 更好的开发体验:更强的TypeScript集成
- Web Components:更好的自定义元素支持
- 微前端:Module Federation集成
总结
以上43个问题涵盖了Angular的核心概念、高级特性、最佳实践等各个方面。在准备面试时,建议:
- 深入理解原理:不仅要知道怎么用,更要知道为什么这样设计
- 结合实际项目:用具体的项目经验来回答问题
- 关注新特性:了解Angular的最新发展和趋势
- 练习代码实现:能够手写核心功能的简单实现
- 准备问题反问:准备一些有深度的问题向面试官提问
记住,面试不仅是展示技术能力,也是展示解决问题的思路和学习能力的机会。