【前端】Angular框架详解:企业级前端开发平台
Angular是由Google开发和维护的开源前端框架,专为构建大型、复杂的单页应用程序而设计。它采用TypeScript作为主要开发语言,提供了完整的开发生态系统和企业级特性。
目录
一、Angular基础概念
(一)核心特性
1. TypeScript优先
Angular从设计之初就采用TypeScript,提供强类型支持和现代JavaScript特性。
// 接口定义
interface User {
id: number;
name: string;
email: string;
isActive: boolean;
}
// 类型安全的组件
@Component({
selector: 'app-user',
template: `
<div class="user-card">
<h3>{{ user.name }
(二)响应式表单
1. 基础响应式表单
// 自定义验证器
export class CustomValidators {
// 密码强度验证
static passwordStrength(control: AbstractControl): ValidationErrors | null {
const value = control.value;
if (!value) return null;
const hasNumber = /[0-9]/.test(value);
const hasUpper = /[A-Z]/.test(value);
const hasLower = /[a-z]/.test(value);
const hasSpecial = /[#?!@$%^&*-]/.test(value);
const valid = hasNumber && hasUpper && hasLower && hasSpecial && value.length >= 8;
if (!valid) {
return {
passwordStrength: {
hasNumber,
hasUpper,
hasLower,
hasSpecial,
minLength: value.length >= 8
}
};
}
return null;
}
// 确认密码验证
static confirmPassword(passwordField: string) {
return (control: AbstractControl): ValidationErrors | null => {
const password = control.parent?.get(passwordField)?.value;
const confirmPassword = control.value;
if (password !== confirmPassword) {
return { confirmPassword: true };
}
return null;
};
}
// 异步邮箱唯一性验证
static emailUnique(userService: UserService) {
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))
);
};
}
}
@Component({
selector: 'app-reactive-form',
template: `
<form [formGroup]="userForm" (ngSubmit)="onSubmit()" class="reactive-form">
<h3>响应式用户注册表单</h3>
<!-- 基本信息组 -->
<div formGroupName="basicInfo" class="form-section">
<h4>基本信息</h4>
<!-- 用户名 -->
<div class="form-group">
<label for="username">用户名 *</label>
<input
type="text"
id="username"
formControlName="username"
class="form-control"
[class.is-invalid]="isFieldInvalid('basicInfo.username')">
<div *ngIf="isFieldInvalid('basicInfo.username')" class="invalid-feedback">
<div *ngIf="getFieldError('basicInfo.username', 'required')">用户名是必填项</div>
<div *ngIf="getFieldError('basicInfo.username', 'minlength')">用户名至少需要3个字符</div>
<div *ngIf="getFieldError('basicInfo.username', 'pattern')">用户名只能包含字母、数字和下划线</div>
</div>
</div>
<!-- 邮箱 -->
<div class="form-group">
<label for="email">邮箱 *</label>
<input
type="email"
id="email"
formControlName="email"
class="form-control"
[class.is-invalid]="isFieldInvalid('basicInfo.email')">
<div *ngIf="isFieldInvalid('basicInfo.email')" class="invalid-feedback">
<div *ngIf="getFieldError('basicInfo.email', 'required')">邮箱是必填项</div>
<div *ngIf="getFieldError('basicInfo.email', 'email')">请输入有效的邮箱地址</div>
<div *ngIf="getFieldError('basicInfo.email', 'emailExists')">该邮箱已被注册</div>
</div>
<div *ngIf="userForm.get('basicInfo.email')?.pending" class="form-text">
正在验证邮箱...
</div>
</div>
<!-- 手机号 -->
<div class="form-group">
<label for="phone">手机号</label>
<input
type="tel"
id="phone"
formControlName="phone"
class="form-control"
[class.is-invalid]="isFieldInvalid('basicInfo.phone')">
<div *ngIf="isFieldInvalid('basicInfo.phone')" class="invalid-feedback">
<div *ngIf="getFieldError('basicInfo.phone', 'pattern')">请输入有效的手机号码</div>
</div>
</div>
</div>
<!-- 密码组 -->
<div formGroupName="passwordGroup" class="form-section">
<h4>密码设置</h4>
<!-- 密码 -->
<div class="form-group">
<label for="password">密码 *</label>
<input
type="password"
id="password"
formControlName="password"
class="form-control"
[class.is-invalid]="isFieldInvalid('passwordGroup.password')">
<div *ngIf="isFieldInvalid('passwordGroup.password')" class="invalid-feedback">
<div *ngIf="getFieldError('passwordGroup.password', 'required')">密码是必填项</div>
<div *ngIf="getFieldError('passwordGroup.password', 'passwordStrength')">
<div>密码必须包含:</div>
<ul>
<li [class.valid]="getFieldError('passwordGroup.password', 'passwordStrength')?.hasNumber">
至少一个数字
</li>
<li [class.valid]="getFieldError('passwordGroup.password', 'passwordStrength')?.hasUpper">
至少一个大写字母
</li>
<li [class.valid]="getFieldError('passwordGroup.password', 'passwordStrength')?.hasLower">
至少一个小写字母
</li>
<li [class.valid]="getFieldError('passwordGroup.password', 'passwordStrength')?.hasSpecial">
至少一个特殊字符
</li>
<li [class.valid]="getFieldError('passwordGroup.password', 'passwordStrength')?.minLength">
至少8个字符
</li>
</ul>
</div>
</div>
</div>
<!-- 确认密码 -->
<div class="form-group">
<label for="confirmPassword">确认密码 *</label>
<input
type="password"
id="confirmPassword"
formControlName="confirmPassword"
class="form-control"
[class.is-invalid]="isFieldInvalid('passwordGroup.confirmPassword')">
<div *ngIf="isFieldInvalid('passwordGroup.confirmPassword')" class="invalid-feedback">
<div *ngIf="getFieldError('passwordGroup.confirmPassword', 'required')">请确认密码</div>
<div *ngIf="getFieldError('passwordGroup.confirmPassword', 'confirmPassword')">两次输入的密码不一致</div>
</div>
</div>
</div>
<!-- 个人信息组 -->
<div formGroupName="personalInfo" class="form-section">
<h4>个人信息</h4>
<!-- 姓名 -->
<div class="form-group">
<label for="fullName">姓名</label>
<input
type="text"
id="fullName"
formControlName="fullName"
class="form-control">
</div>
<!-- 生日 -->
<div class="form-group">
<label for="birthDate">生日</label>
<input
type="date"
id="birthDate"
formControlName="birthDate"
class="form-control">
</div>
<!-- 性别 -->
<div class="form-group">
<label>性别</label>
<div class="radio-group">
<label class="radio-label">
<input type="radio" formControlName="gender" value="male">
男性
</label>
<label class="radio-label">
<input type="radio" formControlName="gender" value="female">
女性
</label>
<label class="radio-label">
<input type="radio" formControlName="gender" value="other">
其他
</label>
</div>
</div>
</div>
<!-- 兴趣爱好 -->
<div class="form-section">
<h4>兴趣爱好</h4>
<div formArrayName="interests" class="interests-array">
<div *ngFor="let interest of interests.controls; let i = index"
[formGroupName]="i"
class="interest-item">
<input
type="text"
formControlName="name"
placeholder="输入兴趣爱好"
class="form-control">
<button
type="button"
(click)="removeInterest(i)"
class="btn btn-danger btn-sm">
删除
</button>
</div>
<button
type="button"
(click)="addInterest()"
class="btn btn-secondary">
添加兴趣
</button>
</div>
</div>
<!-- 同意条款 -->
<div class="form-group">
<label class="checkbox-label">
<input
type="checkbox"
formControlName="agreedToTerms">
我同意服务条款和隐私政策 *
</label>
<div *ngIf="isFieldInvalid('agreedToTerms')" class="invalid-feedback">
<div *ngIf="getFieldError('agreedToTerms', 'required')">必须同意服务条款才能继续</div>
</div>
</div>
<!-- 表单状态显示 -->
<div class="form-debug" *ngIf="showDebugInfo">
<h4>表单状态调试信息</h4>
<p>表单有效: {{ userForm.valid }}</p>
<p>表单值: {{ userForm.value | json }}</p>
<p>表单错误: {{ userForm.errors | json }}</p>
</div>
<!-- 提交按钮 -->
<div class="form-actions">
<button
type="submit"
class="btn btn-primary"
[disabled]="userForm.invalid || isSubmitting">
{{ isSubmitting ? '注册中...' : '注册' }}
</button>
<button
type="button"
class="btn btn-secondary"
(click)="resetForm()">
重置
</button>
<button
type="button"
class="btn btn-info"
(click)="toggleDebugInfo()">
{{ showDebugInfo ? '隐藏' : '显示' }}调试信息
</button>
</div>
</form>
`
})
export class ReactiveFormComponent implements OnInit {
userForm!: FormGroup;
isSubmitting = false;
showDebugInfo = false;
constructor(
private fb: FormBuilder,
private userService: UserService
) {}
ngOnInit(): void {
this.createForm();
this.setupFormSubscriptions();
}
private createForm(): void {
this.userForm = this.fb.group({
basicInfo: this.fb.group({
username: ['', [
Validators.required,
Validators.minLength(3),
Validators.pattern(/^[a-zA-Z0-9_]+$/)
]],
email: ['',
[Validators.required, Validators.email],
[CustomValidators.emailUnique(this.userService)]
],
phone: ['', [Validators.pattern(/^1[3-9]\d{9}$/)]]
}),
passwordGroup: this.fb.group({
password: ['', [Validators.required, CustomValidators.passwordStrength]],
confirmPassword: ['', [Validators.required]]
}, { validators: this.passwordMatchValidator }),
personalInfo: this.fb.group({
fullName: [''],
birthDate: [''],
gender: ['male']
}),
interests: this.fb.array([]),
agreedToTerms: [false, Validators.requiredTrue]
});
}
private setupFormSubscriptions(): void {
// 监听密码字段变化,重新验证确认密码
this.userForm.get('passwordGroup.password')?.valueChanges.subscribe(() => {
this.userForm.get('passwordGroup.confirmPassword')?.updateValueAndValidity();
});
// 监听表单值变化
this.userForm.valueChanges.pipe(
debounceTime(300)
).subscribe(value => {
console.log('表单值变化:', value);
});
}
// 密码匹配验证器
private passwordMatchValidator(group: AbstractControl): ValidationErrors | null {
const password = group.get('password')?.value;
const confirmPassword = group.get('confirmPassword')?.value;
if (password !== confirmPassword) {
group.get('confirmPassword')?.setErrors({ confirmPassword: true });
return { passwordMismatch: true };
} else {
const confirmPasswordControl = group.get('confirmPassword');
if (confirmPasswordControl?.errors?.['confirmPassword']) {
delete confirmPasswordControl.errors['confirmPassword'];
if (Object.keys(confirmPasswordControl.errors).length === 0) {
confirmPasswordControl.setErrors(null);
}
}
}
return null;
}
get interests(): FormArray {
return this.userForm.get('interests') as FormArray;
}
addInterest(): void {
const interestGroup = this.fb.group({
name: ['', Validators.required]
});
this.interests.push(interestGroup);
}
removeInterest(index: number): void {
this.interests.removeAt(index);
}
isFieldInvalid(fieldName: string): boolean {
const field = this.userForm.get(fieldName);
return !!(field && field.invalid && (field.dirty || field.touched));
}
getFieldError(fieldName: string, errorType: string): any {
const field = this.userForm.get(fieldName);
return field?.errors?.[errorType];
}
onSubmit(): void {
if (this.userForm.valid) {
this.isSubmitting = true;
const formData = this.userForm.value;
console.log('提交的表单数据:', formData);
// 模拟API调用
setTimeout(() => {
alert('注册成功!');
this.isSubmitting = false;
this.resetForm();
}, 2000);
} else {
console.log('表单验证失败');
this.markFormGroupTouched(this.userForm);
}
}
resetForm(): void {
this.userForm.reset();
this.userForm.get('personalInfo.gender')?.setValue('male');
this.userForm.get('agreedToTerms')?.setValue(false);
// 清空兴趣数组
while (this.interests.length !== 0) {
this.interests.removeAt(0);
}
}
toggleDebugInfo(): void {
this.showDebugInfo = !this.showDebugInfo;
}
private markFormGroupTouched(formGroup: FormGroup | FormArray): void {
Object.keys(formGroup.controls).forEach(key => {
const control = formGroup.get(key);
if (control instanceof FormGroup || control instanceof FormArray) {
this.markFormGroupTouched(control);
} else {
control?.markAsTouched();
}
});
}
}
八、路由与导航
(一)基础路由配置
1. 路由模块设置
// app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AuthGuard } from './guards/auth.guard';
import { AdminGuard } from './guards/admin.guard';
import { UnsavedChangesGuard } from './guards/unsaved-changes.guard';
const routes: Routes = [
// 重定向
{ path: '', redirectTo: '/home', pathMatch: 'full' },
// 基础路由
{ path: 'home', component: HomeComponent },
{ path: 'about', component: AboutComponent },
{ path: 'contact', component: ContactComponent },
// 带参数的路由
{ path: 'user/:id', component: UserDetailComponent },
{ path: 'product/:category/:id', component: ProductDetailComponent },
// 查询参数路由
{ path: 'search', component: SearchComponent },
// 受保护的路由
{
path: 'profile',
component: ProfileComponent,
canActivate: [AuthGuard],
canDeactivate: [UnsavedChangesGuard]
},
// 管理员路由
{
path: 'admin',
loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule),
canActivate: [AuthGuard, AdminGuard]
},
// 懒加载模块
{
path: 'features',
loadChildren: () => import('./features/features.module').then(m => m.FeaturesModule)
},
// 嵌套路由
{
path: 'dashboard',
component: DashboardComponent,
canActivate: [AuthGuard],
children: [
{ path: '', component: DashboardHomeComponent },
{ path: 'analytics', component: AnalyticsComponent },
{ path: 'reports', component: ReportsComponent },
{ path: 'settings', component: SettingsComponent }
]
},
// 通配符路由(必须放在最后)
{ path: '**', component: NotFoundComponent }
];
@NgModule({
imports: [RouterModule.forRoot(routes, {
enableTracing: false, // 开发时可设为true来调试路由
preloadingStrategy: PreloadAllModules, // 预加载策略
scrollPositionRestoration: 'top' // 路由切换时滚动到顶部
})],
exports: [RouterModule]
})
export class AppRoutingModule { }
2. 路由组件示例
// 用户详情组件
@Component({
selector: 'app-user-detail',
template: `
<div class="user-detail" *ngIf="user">
<div class="header">
<button (click)="goBack()" class="btn btn-secondary">
← 返回
</button>
<h2>{{ user.name }}</h2>
</div>
<div class="content">
<div class="user-info">
<p><strong>ID:</strong> {{ user.id }}</p>
<p><strong>邮箱:</strong> {{ user.email }}</p>
<p><strong>注册时间:</strong> {{ user.createdAt | date:'medium' }}</p>
</div>
<div class="actions">
<button (click)="editUser()" class="btn btn-primary">
编辑用户
</button>
<button (click)="deleteUser()" class="btn btn-danger">
删除用户
</button>
</div>
</div>
<div class="navigation">
<button (click)="goToPrevious()" [disabled]="!hasPrevious">
上一个用户
</button>
<button (click)="goToNext()" [disabled]="!hasNext">
下一个用户
</button>
</div>
</div>
<div *ngIf="!user && !loading" class="error">
用户不存在
</div>
<div *ngIf="loading" class="loading">
正在加载用户信息...
</div>
`
})
export class UserDetailComponent implements OnInit, OnDestroy {
user: User | null = null;
loading = false;
hasPrevious = false;
hasNext = false;
private destroy$ = new Subject<void>();
constructor(
private route: ActivatedRoute,
private router: Router,
private userService: UserService,
private location: Location
) {}
ngOnInit(): void {
// 监听路由参数变化
this.route.params.pipe(
takeUntil(this.destroy$),
switchMap(params => {
const userId = +params['id'];
if (isNaN(userId)) {
this.router.navigate(['/users']);
return EMPTY;
}
this.loading = true;
return this.userService.getUserById(userId);
})
).subscribe({
next: user => {
this.user = user;
this.loading = false;
this.checkNavigation();
},
error: error => {
console.error('加载用户失败:', error);
this.loading = false;
}
});
// 监听查询参数
this.route.queryParams.pipe(
takeUntil(this.destroy$)
).subscribe(params => {
console.log('查询参数:', params);
});
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
private checkNavigation(): void {
if (this.user) {
// 检查是否有上一个/下一个用户
this.userService.hasAdjacentUsers(this.user.id).subscribe(result => {
this.hasPrevious = result.hasPrevious;
this.hasNext = result.hasNext;
});
}
}
goBack(): void {
this.location.back();
}
editUser(): void {
if (this.user) {
this.router.navigate(['/user', this.user.id, 'edit']);
}
}
deleteUser(): void {
if (this.user && confirm('确定要删除这个用户吗?')) {
this.userService.deleteUser(this.user.id).subscribe(() => {
this.router.navigate(['/users']);
});
}
}
goToPrevious(): void {
if (this.user && this.hasPrevious) {
this.router.navigate(['/user', this.user.id - 1]);
}
}
goToNext(): void {
if (this.user && this.hasNext) {
this.router.navigate(['/user', this.user.id + 1]);
}
}
}
// 搜索组件
@Component({
selector: 'app-search',
template: `
<div class="search-page">
<div class="search-form">
<input
type="text"
[(ngModel)]="searchQuery"
(keyup.enter)="search()"
placeholder="搜索..."
class="form-control">
<button (click)="search()" class="btn btn-primary">
搜索
</button>
</div>
<div class="filters">
<select [(ngModel)]="category" (change)="search()" class="form-control">
<option value="">所有分类</option>
<option value="users">用户</option>
<option value="products">产品</option>
<option value="articles">文章</option>
</select>
<select [(ngModel)]="sortBy" (change)="search()" class="form-control">
<option value="relevance">相关性</option>
<option value="date">日期</option>
<option value="name">名称</option>
</select>
</div>
<div class="results" *ngIf="results.length > 0">
<h3>搜索结果 ({{ totalResults }})</h3>
<div *ngFor="let result of results" class="result-item">
<h4>{{ result.title }}</h4>
<p>{{ result.description }}</p>
<a [routerLink]="result.link">查看详情</a>
</div>
<!-- 分页 -->
<div class="pagination">
<button
(click)="goToPage(currentPage - 1)"
[disabled]="currentPage <= 1"
class="btn btn-secondary">
上一页
</button>
<span>第 {{ currentPage }} 页,共 {{ totalPages }} 页</span>
<button
(click)="goToPage(currentPage + 1)"
[disabled]="currentPage >= totalPages"
class="btn btn-secondary">
下一页
</button>
</div>
</div>
<div *ngIf="searched && results.length === 0" class="no-results">
没有找到相关结果
</div>
</div>
`
})
export class SearchComponent implements OnInit, OnDestroy {
searchQuery = '';
category = '';
sortBy = 'relevance';
currentPage = 1;
pageSize = 10;
results: any[] = [];
totalResults = 0;
totalPages = 0;
searched = false;
private destroy$ = new Subject<void>();
constructor(
private route: ActivatedRoute,
private router: Router,
private searchService: SearchService
) {}
ngOnInit(): void {
// 从查询参数初始化搜索状态
this.route.queryParams.pipe(
takeUntil(this.destroy$)
).subscribe(params => {
this.searchQuery = params['q'] || '';
this.category = params['category'] || '';
this.sortBy = params['sort'] || 'relevance';
this.currentPage = +params['page'] || 1;
if (this.searchQuery) {
this.performSearch();
}
});
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
search(): void {
this.currentPage = 1;
this.updateUrl();
}
goToPage(page: number): void {
this.currentPage = page;
this.updateUrl();
}
private updateUrl(): void {
const queryParams: any = {};
if (this.searchQuery) queryParams.q = this.searchQuery;
if (this.category) queryParams.category = this.category;
if (this.sortBy !== 'relevance') queryParams.sort = this.sortBy;
if (this.currentPage > 1) queryParams.page = this.currentPage;
this.router.navigate([], {
relativeTo: this.route,
queryParams,
queryParamsHandling: 'replace'
});
}
private performSearch(): void {
if (!this.searchQuery.trim()) {
this.results = [];
this.searched = false;
return;
}
const searchParams = {
query: this.searchQuery,
category: this.category,
sortBy: this.sortBy,
page: this.currentPage,
pageSize: this.pageSize
};
this.searchService.search(searchParams).subscribe(response => {
this.results = response.results;
this.totalResults = response.total;
this.totalPages = Math.ceil(this.totalResults / this.pageSize);
this.searched = true;
});
}
}
```}</h3>
<p>{{ user.email }}</p>
<span [class.active]="user.isActive">状态</span>
</div>
`
})
export class UserComponent {
@Input() user!: User; // 强类型输入属性
@Output() userClick = new EventEmitter<User>();
onUserClick(): void {
this.userClick.emit(this.user);
}
}
2. 组件化架构
// 父组件
@Component({
selector: 'app-user-list',
template: `
<div class="user-list">
<app-user
*ngFor="let user of users; trackBy: trackByUserId"
[user]="user"
(userClick)="onUserSelected($event)">
</app-user>
</div>
`
})
export class UserListComponent {
users: User[] = [
{ id: 1, name: '张三', email: 'zhangsan@example.com', isActive: true },
{ id: 2, name: '李四', email: 'lisi@example.com', isActive: false }
];
trackByUserId(index: number, user: User): number {
return user.id;
}
onUserSelected(user: User): void {
console.log('选中用户:', user);
}
}
3. 依赖注入系统
// 服务定义
@Injectable({
providedIn: 'root'
})
export class UserService {
private apiUrl = 'https://api.example.com/users';
constructor(private http: HttpClient) {}
getUsers(): Observable<User[]> {
return this.http.get<User[]>(this.apiUrl);
}
getUserById(id: number): Observable<User> {
return this.http.get<User>(`${this.apiUrl}/${id}`);
}
}
// 组件中注入服务
@Component({
selector: 'app-user-container',
template: `
<div *ngIf="loading">加载中...</div>
<app-user-list
*ngIf="!loading"
[users]="users">
</app-user-list>
`
})
export class UserContainerComponent implements OnInit {
users: User[] = [];
loading = true;
constructor(private userService: UserService) {}
ngOnInit(): void {
this.loadUsers();
}
private loadUsers(): void {
this.userService.getUsers().subscribe({
next: (users) => {
this.users = users;
this.loading = false;
},
error: (error) => {
console.error('加载用户失败:', error);
this.loading = false;
}
})
export class CoreModule {}
(二)响应式编程与RxJS
1. Observable基础
// 数据服务示例
@Injectable({
providedIn: 'root'
})
export class DataService {
private dataSubject = new BehaviorSubject<any[]>([]);
public data$ = this.dataSubject.asObservable();
private loadingSubject = new BehaviorSubject<boolean>(false);
public loading$ = this.loadingSubject.asObservable();
constructor(private http: HttpClient) {}
// 加载数据
loadData(): void {
this.loadingSubject.next(true);
this.http.get<any[]>('/api/data').pipe(
delay(1000), // 模拟网络延迟
finalize(() => this.loadingSubject.next(false))
).subscribe({
next: data => this.dataSubject.next(data),
error: error => console.error('加载数据失败:', error)
});
}
// 添加数据项
addItem(item: any): void {
const currentData = this.dataSubject.value;
this.dataSubject.next([...currentData, item]);
}
// 更新数据项
updateItem(id: number, updates: Partial<any>): void {
const currentData = this.dataSubject.value;
const updatedData = currentData.map(item =>
item.id === id ? { ...item, ...updates } : item
);
this.dataSubject.next(updatedData);
}
// 删除数据项
deleteItem(id: number): void {
const currentData = this.dataSubject.value;
const filteredData = currentData.filter(item => item.id !== id);
this.dataSubject.next(filteredData);
}
}
// 使用Observable的组件
@Component({
selector: 'app-data-list',
template: `
<div class="data-list">
<div class="header">
<h3>数据列表</h3>
<button (click)="loadData()" [disabled]="loading$ | async">
{{ (loading$ | async) ? '加载中...' : '刷新数据' }}
</button>
</div>
<div *ngIf="loading$ | async" class="loading">
正在加载数据...
</div>
<div *ngIf="!(loading$ | async)" class="content">
<div *ngFor="let item of data$ | async; trackBy: trackByFn"
class="data-item">
<span>{{ item.name }}</span>
<button (click)="editItem(item)">编辑</button>
<button (click)="deleteItem(item.id)">删除</button>
</div>
<div *ngIf="(data$ | async)?.length === 0" class="empty">
暂无数据
</div>
</div>
</div>
`
})
export class DataListComponent implements OnInit, OnDestroy {
data$ = this.dataService.data$;
loading$ = this.dataService.loading$;
private destroy$ = new Subject<void>();
constructor(private dataService: DataService) {}
ngOnInit(): void {
// 组合多个Observable
const combinedData$ = combineLatest([
this.data$,
this.loading$
]).pipe(
takeUntil(this.destroy$),
map(([data, loading]) => ({ data, loading, count: data.length }))
);
combinedData$.subscribe(result => {
console.log('数据状态:', result);
});
this.loadData();
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
loadData(): void {
this.dataService.loadData();
}
editItem(item: any): void {
// 实现编辑逻辑
const updates = { name: item.name + ' (已编辑)' };
this.dataService.updateItem(item.id, updates);
}
deleteItem(id: number): void {
this.dataService.deleteItem(id);
}
trackByFn(index: number, item: any): any {
return item.id;
}
}
2. 高级RxJS操作符
@Component({
selector: 'app-search',
template: `
<div class="search-container">
<input
#searchInput
type="text"
placeholder="搜索..."
(input)="onSearchInput($event)">
<div *ngIf="loading" class="loading">搜索中...</div>
<div class="results">
<div *ngFor="let result of searchResults$ | async" class="result-item">
{{ result.title }}
</div>
</div>
</div>
`
})
export class SearchComponent implements OnInit, OnDestroy {
private searchSubject = new Subject<string>();
searchResults$!: Observable<any[]>;
loading = false;
private destroy$ = new Subject<void>();
constructor(private searchService: SearchService) {}
ngOnInit(): void {
this.searchResults$ = this.searchSubject.pipe(
// 防抖:等待用户停止输入300ms后再搜索
debounceTime(300),
// 去重:只有当搜索词发生变化时才搜索
distinctUntilChanged(),
// 过滤:只搜索长度大于2的词
filter(term => term.length > 2),
// 切换到搜索Observable,自动取消之前的请求
switchMap(term => {
this.loading = true;
return this.searchService.search(term).pipe(
// 错误处理:搜索失败时返回空数组
catchError(error => {
console.error('搜索失败:', error);
return of([]);
}),
// 完成时设置loading为false
finalize(() => this.loading = false)
);
}),
// 组件销毁时自动取消订阅
takeUntil(this.destroy$)
);
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
onSearchInput(event: Event): void {
const target = event.target as HTMLInputElement;
this.searchSubject.next(target.value);
}
}
// 搜索服务
@Injectable({
providedIn: 'root'
})
export class SearchService {
constructor(private http: HttpClient) {}
search(term: string): Observable<any[]> {
return this.http.get<any[]>(`/api/search?q=${encodeURIComponent(term)}`);
}
}
七、表单处理
(一)模板驱动表单
1. 基础表单
// 用户模型
export interface User {
id?: number;
name: string;
email: string;
age: number;
gender: 'male' | 'female' | 'other';
interests: string[];
address: {
street: string;
city: string;
zipCode: string;
};
agreedToTerms: boolean;
}
@Component({
selector: 'app-template-form',
template: `
<form #userForm="ngForm" (ngSubmit)="onSubmit(userForm)" class="user-form">
<h3>用户信息表单</h3>
<!-- 姓名字段 -->
<div class="form-group">
<label for="name">姓名 *</label>
<input
type="text"
id="name"
name="name"
[(ngModel)]="user.name"
#name="ngModel"
required
minlength="2"
maxlength="50"
class="form-control"
[class.is-invalid]="name.invalid && name.touched">
<div *ngIf="name.invalid && name.touched" class="invalid-feedback">
<div *ngIf="name.errors?.['required']">姓名是必填项</div>
<div *ngIf="name.errors?.['minlength']">姓名至少需要2个字符</div>
<div *ngIf="name.errors?.['maxlength']">姓名不能超过50个字符</div>
</div>
</div>
<!-- 邮箱字段 -->
<div class="form-group">
<label for="email">邮箱 *</label>
<input
type="email"
id="email"
name="email"
[(ngModel)]="user.email"
#email="ngModel"
required
email
class="form-control"
[class.is-invalid]="email.invalid && email.touched">
<div *ngIf="email.invalid && email.touched" class="invalid-feedback">
<div *ngIf="email.errors?.['required']">邮箱是必填项</div>
<div *ngIf="email.errors?.['email']">请输入有效的邮箱地址</div>
</div>
</div>
<!-- 年龄字段 -->
<div class="form-group">
<label for="age">年龄</label>
<input
type="number"
id="age"
name="age"
[(ngModel)]="user.age"
#age="ngModel"
min="18"
max="120"
class="form-control"
[class.is-invalid]="age.invalid && age.touched">
<div *ngIf="age.invalid && age.touched" class="invalid-feedback">
<div *ngIf="age.errors?.['min']">年龄不能小于18岁</div>
<div *ngIf="age.errors?.['max']">年龄不能大于120岁</div>
</div>
</div>
<!-- 性别字段 -->
<div class="form-group">
<label>性别</label>
<div class="radio-group">
<label class="radio-label">
<input
type="radio"
name="gender"
value="male"
[(ngModel)]="user.gender">
男性
</label>
<label class="radio-label">
<input
type="radio"
name="gender"
value="female"
[(ngModel)]="user.gender">
女性
</label>
<label class="radio-label">
<input
type="radio"
name="gender"
value="other"
[(ngModel)]="user.gender">
其他
</label>
</div>
</div>
<!-- 兴趣爱好 -->
<div class="form-group">
<label>兴趣爱好</label>
<div class="checkbox-group">
<label class="checkbox-label" *ngFor="let interest of availableInterests">
<input
type="checkbox"
[value]="interest"
(change)="onInterestChange($event, interest)">
{{ interest }}
</label>
</div>
</div>
<!-- 地址信息 -->
<fieldset class="form-group">
<legend>地址信息</legend>
<div class="form-group">
<label for="street">街道地址</label>
<input
type="text"
id="street"
name="street"
[(ngModel)]="user.address.street"
class="form-control">
</div>
<div class="form-row">
<div class="form-group col-md-6">
<label for="city">城市</label>
<input
type="text"
id="city"
name="city"
[(ngModel)]="user.address.city"
class="form-control">
</div>
<div class="form-group col-md-6">
<label for="zipCode">邮政编码</label>
<input
type="text"
id="zipCode"
name="zipCode"
[(ngModel)]="user.address.zipCode"
pattern="[0-9]{6}"
#zipCode="ngModel"
class="form-control"
[class.is-invalid]="zipCode.invalid && zipCode.touched">
<div *ngIf="zipCode.invalid && zipCode.touched" class="invalid-feedback">
<div *ngIf="zipCode.errors?.['pattern']">请输入6位数字的邮政编码</div>
</div>
</div>
</div>
</fieldset>
<!-- 同意条款 -->
<div class="form-group">
<label class="checkbox-label">
<input
type="checkbox"
name="agreedToTerms"
[(ngModel)]="user.agreedToTerms"
#terms="ngModel"
required>
我同意服务条款和隐私政策 *
</label>
<div *ngIf="terms.invalid && terms.touched" class="invalid-feedback">
<div *ngIf="terms.errors?.['required']">必须同意服务条款才能继续</div>
</div>
</div>
<!-- 表单状态显示 -->
<div class="form-debug" *ngIf="showDebugInfo">
<h4>表单状态调试信息</h4>
<p>表单有效: {{ userForm.valid }}</p>
<p>表单已提交: {{ userForm.submitted }}</p>
<p>表单已修改: {{ userForm.dirty }}</p>
<p>表单已触摸: {{ userForm.touched }}</p>
<pre>{{ user | json }}</pre>
</div>
<!-- 提交按钮 -->
<div class="form-actions">
<button
type="submit"
class="btn btn-primary"
[disabled]="userForm.invalid || isSubmitting">
{{ isSubmitting ? '提交中...' : '提交' }}
</button>
<button
type="button"
class="btn btn-secondary"
(click)="resetForm(userForm)">
重置
</button>
<button
type="button"
class="btn btn-info"
(click)="toggleDebugInfo()">
{{ showDebugInfo ? '隐藏' : '显示' }}调试信息
</button>
</div>
</form>
`
})
export class TemplateFormComponent {
user: User = {
name: '',
email: '',
age: 18,
gender: 'male',
interests: [],
address: {
street: '',
city: '',
zipCode: ''
},
agreedToTerms: false
};
availableInterests = ['阅读', '运动', '音乐', '旅行', '编程', '摄影'];
isSubmitting = false;
showDebugInfo = false;
onSubmit(form: NgForm): void {
if (form.valid) {
this.isSubmitting = true;
// 模拟API调用
setTimeout(() => {
console.log('提交的用户数据:', this.user);
alert('用户信息提交成功!');
this.isSubmitting = false;
}, 2000);
} else {
console.log('表单验证失败');
this.markFormGroupTouched(form);
}
}
onInterestChange(event: Event, interest: string): void {
const target = event.target as HTMLInputElement;
if (target.checked) {
this.user.interests.push(interest);
} else {
const index = this.user.interests.indexOf(interest);
if (index > -1) {
this.user.interests.splice(index, 1);
}
}
}
resetForm(form: NgForm): void {
form.resetForm();
this.user = {
name: '',
email: '',
age: 18,
gender: 'male',
interests: [],
address: {
street: '',
city: '',
zipCode: ''
},
agreedToTerms: false
};
}
toggleDebugInfo(): void {
this.showDebugInfo = !this.showDebugInfo;
}
private markFormGroupTouched(form: NgForm): void {
Object.keys(form.controls).forEach(key => {
const control = form.controls[key];
control.markAsTouched();
});
}
}
```;
}
}
(二)Angular架构
1. 模块系统
// 特性模块
@NgModule({
declarations: [
UserComponent,
UserListComponent,
UserContainerComponent
],
imports: [
CommonModule,
HttpClientModule,
RouterModule
],
providers: [
UserService
],
exports: [
UserContainerComponent
]
})
export class UserModule {}
// 应用根模块
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
AppRoutingModule,
UserModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule {}
2. 生命周期钩子
@Component({
selector: 'app-lifecycle-demo',
template: `
<div>
<h3>生命周期演示</h3>
<p>计数器: {{ counter }}</p>
<button (click)="increment()">增加</button>
</div>
`
})
export class LifecycleDemoComponent implements
OnInit, OnChanges, DoCheck, AfterContentInit,
AfterContentChecked, AfterViewInit, AfterViewChecked, OnDestroy {
@Input() inputValue: string = '';
counter = 0;
constructor() {
console.log('Constructor: 组件实例化');
}
ngOnChanges(changes: SimpleChanges): void {
console.log('OnChanges: 输入属性变化', changes);
}
ngOnInit(): void {
console.log('OnInit: 组件初始化完成');
}
ngDoCheck(): void {
console.log('DoCheck: 变更检测');
}
ngAfterContentInit(): void {
console.log('AfterContentInit: 内容投影初始化');
}
ngAfterContentChecked(): void {
console.log('AfterContentChecked: 内容投影检查');
}
ngAfterViewInit(): void {
console.log('AfterViewInit: 视图初始化完成');
}
ngAfterViewChecked(): void {
console.log('AfterViewChecked: 视图检查完成');
}
ngOnDestroy(): void {
console.log('OnDestroy: 组件销毁');
}
increment(): void {
this.counter++;
}
}
二、项目结构与CLI
(一)Angular CLI
1. 项目创建与基本命令
# 安装Angular CLI
npm install -g @angular/cli
# 创建新项目
ng new my-angular-app
cd my-angular-app
# 启动开发服务器
ng serve
# 构建生产版本
ng build --prod
# 运行测试
ng test
# 运行端到端测试
ng e2e
2. 代码生成
# 生成组件
ng generate component user-profile
ng g c user-profile --skip-tests
# 生成服务
ng generate service user
ng g s user --skip-tests
# 生成模块
ng generate module user --routing
ng g m user --routing
# 生成指令
ng generate directive highlight
ng g d highlight
# 生成管道
ng generate pipe currency-format
ng g p currency-format
# 生成守卫
ng generate guard auth
ng g g auth
(二)项目结构
src/
├── app/
│ ├── core/ # 核心模块(单例服务)
│ │ ├── services/
│ │ ├── guards/
│ │ ├── interceptors/
│ │ └── core.module.ts
│ ├── shared/ # 共享模块
│ │ ├── components/
│ │ ├── directives/
│ │ ├── pipes/
│ │ └── shared.module.ts
│ ├── features/ # 特性模块
│ │ ├── user/
│ │ │ ├── components/
│ │ │ ├── services/
│ │ │ ├── models/
│ │ │ └── user.module.ts
│ │ └── product/
│ ├── app-routing.module.ts
│ ├── app.component.ts
│ ├── app.component.html
│ ├── app.component.scss
│ └── app.module.ts
├── assets/ # 静态资源
├── environments/ # 环境配置
│ ├── environment.ts
│ └── environment.prod.ts
├── index.html
├── main.ts
├── polyfills.ts
└── styles.scss
3. 环境配置
// environments/environment.ts
export const environment = {
production: false,
apiUrl: 'http://localhost:3000/api',
enableLogging: true
};
// environments/environment.prod.ts
export const environment = {
production: true,
apiUrl: 'https://api.example.com',
enableLogging: false
};
// 在服务中使用环境配置
import { environment } from '../environments/environment';
@Injectable({
providedIn: 'root'
})
export class ApiService {
private baseUrl = environment.apiUrl;
constructor(private http: HttpClient) {
if (environment.enableLogging) {
console.log('API服务初始化,基础URL:', this.baseUrl);
}
}
}
三、组件系统
(一)组件基础
1. 组件定义与元数据
@Component({
selector: 'app-product-card',
templateUrl: './product-card.component.html',
styleUrls: ['./product-card.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.Emulated
})
export class ProductCardComponent implements OnInit {
@Input() product!: Product;
@Input() showActions = true;
@Output() addToCart = new EventEmitter<Product>();
@Output() viewDetails = new EventEmitter<number>();
@ViewChild('priceElement', { static: true })
priceElement!: ElementRef<HTMLElement>;
@ViewChildren('actionButton')
actionButtons!: QueryList<ElementRef<HTMLButtonElement>>;
isLoading = false;
constructor(
private cdr: ChangeDetectorRef,
private renderer: Renderer2
) {}
ngOnInit(): void {
this.updatePriceDisplay();
}
onAddToCart(): void {
if (this.product && !this.isLoading) {
this.isLoading = true;
this.addToCart.emit(this.product);
// 模拟异步操作
setTimeout(() => {
this.isLoading = false;
this.cdr.markForCheck(); // 手动触发变更检测
}, 1000);
}
}
onViewDetails(): void {
this.viewDetails.emit(this.product.id);
}
private updatePriceDisplay(): void {
if (this.priceElement) {
const element = this.priceElement.nativeElement;
if (this.product.discountPrice) {
this.renderer.addClass(element, 'discounted');
}
}
}
}
2. 组件模板
<!-- product-card.component.html -->
<div class="product-card" [class.loading]="isLoading">
<div class="product-image">
<img [src]="product.imageUrl" [alt]="product.name" loading="lazy">
<div class="discount-badge" *ngIf="product.discountPrice">
{{ ((product.price - product.discountPrice) / product.price * 100) | number:'1.0-0' }}% OFF
</div>
</div>
<div class="product-info">
<h3 class="product-name">{{ product.name }}</h3>
<p class="product-description">{{ product.description | slice:0:100 }}...</p>
<div class="price-section" #priceElement>
<span class="current-price">
{{ (product.discountPrice || product.price) | currency:'CNY':'symbol':'1.2-2' }}
</span>
<span class="original-price" *ngIf="product.discountPrice">
{{ product.price | currency:'CNY':'symbol':'1.2-2' }}
</span>
</div>
<div class="rating">
<span class="stars">
<i *ngFor="let star of [1,2,3,4,5]"
class="star"
[class.filled]="star <= product.rating">
★
</i>
</span>
<span class="rating-text">({{ product.reviewCount }})</span>
</div>
</div>
<div class="product-actions" *ngIf="showActions">
<button
#actionButton
type="button"
class="btn btn-primary"
[disabled]="isLoading || !product.inStock"
(click)="onAddToCart()">
<span *ngIf="!isLoading">加入购物车</span>
<span *ngIf="isLoading">添加中...</span>
</button>
<button
#actionButton
type="button"
class="btn btn-secondary"
(click)="onViewDetails()">
查看详情
</button>
</div>
</div>
3. 组件样式
// product-card.component.scss
.product-card {
border: 1px solid #e0e0e0;
border-radius: 8px;
overflow: hidden;
transition: all 0.3s ease;
background: white;
&:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
}
&.loading {
opacity: 0.7;
pointer-events: none;
}
.product-image {
position: relative;
height: 200px;
overflow: hidden;
img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s ease;
}
&:hover img {
transform: scale(1.05);
}
.discount-badge {
position: absolute;
top: 10px;
right: 10px;
background: #ff4444;
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: bold;
}
}
.product-info {
padding: 16px;
.product-name {
margin: 0 0 8px 0;
font-size: 18px;
font-weight: 600;
color: #333;
}
.product-description {
margin: 0 0 12px 0;
color: #666;
font-size: 14px;
line-height: 1.4;
}
.price-section {
margin-bottom: 12px;
.current-price {
font-size: 20px;
font-weight: bold;
color: #e74c3c;
}
.original-price {
margin-left: 8px;
font-size: 16px;
color: #999;
text-decoration: line-through;
}
&.discounted .current-price {
color: #27ae60;
}
}
.rating {
display: flex;
align-items: center;
gap: 8px;
.stars {
.star {
color: #ddd;
font-size: 16px;
&.filled {
color: #ffc107;
}
}
}
.rating-text {
font-size: 14px;
color: #666;
}
}
}
.product-actions {
padding: 16px;
border-top: 1px solid #f0f0f0;
display: flex;
gap: 8px;
.btn {
flex: 1;
padding: 10px 16px;
border: none;
border-radius: 4px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
&:disabled {
opacity: 0.6;
cursor: not-allowed;
}
&.btn-primary {
background: #007bff;
color: white;
&:hover:not(:disabled) {
background: #0056b3;
}
}
&.btn-secondary {
background: #6c757d;
color: white;
&:hover:not(:disabled) {
background: #545b62;
}
}
}
}
}
(二)组件通信
1. 父子组件通信
// 产品接口
interface Product {
id: number;
name: string;
description: string;
price: number;
discountPrice?: number;
imageUrl: string;
rating: number;
reviewCount: number;
inStock: boolean;
category: string;
}
// 父组件
@Component({
selector: 'app-product-list',
template: `
<div class="product-list">
<div class="filters">
<select [(ngModel)]="selectedCategory" (change)="onCategoryChange()">
<option value="">所有分类</option>
<option *ngFor="let category of categories" [value]="category">
{{ category }}
</option>
</select>
<input
type="text"
[(ngModel)]="searchTerm"
(input)="onSearch()"
placeholder="搜索产品...">
</div>
<div class="products-grid">
<app-product-card
*ngFor="let product of filteredProducts; trackBy: trackByProductId"
[product]="product"
[showActions]="true"
(addToCart)="onAddToCart($event)"
(viewDetails)="onViewDetails($event)">
</app-product-card>
</div>
<div *ngIf="filteredProducts.length === 0" class="no-products">
没有找到匹配的产品
</div>
</div>
`
})
export class ProductListComponent implements OnInit {
products: Product[] = [];
filteredProducts: Product[] = [];
categories: string[] = [];
selectedCategory = '';
searchTerm = '';
constructor(
private productService: ProductService,
private cartService: CartService,
private router: Router
) {}
ngOnInit(): void {
this.loadProducts();
}
trackByProductId(index: number, product: Product): number {
return product.id;
}
onCategoryChange(): void {
this.filterProducts();
}
onSearch(): void {
this.filterProducts();
}
onAddToCart(product: Product): void {
this.cartService.addToCart(product).subscribe({
next: () => {
console.log('产品已添加到购物车:', product.name);
},
error: (error) => {
console.error('添加到购物车失败:', error);
}
});
}
onViewDetails(productId: number): void {
this.router.navigate(['/products', productId]);
}
private loadProducts(): void {
this.productService.getProducts().subscribe({
next: (products) => {
this.products = products;
this.filteredProducts = products;
this.categories = [...new Set(products.map(p => p.category))];
},
error: (error) => {
console.error('加载产品失败:', error);
}
});
}
private filterProducts(): void {
this.filteredProducts = this.products.filter(product => {
const matchesCategory = !this.selectedCategory ||
product.category === this.selectedCategory;
const matchesSearch = !this.searchTerm ||
product.name.toLowerCase().includes(this.searchTerm.toLowerCase()) ||
product.description.toLowerCase().includes(this.searchTerm.toLowerCase());
return matchesCategory && matchesSearch;
});
}
}
2. 服务通信
// 购物车服务
@Injectable({
providedIn: 'root'
})
export class CartService {
private cartItems$ = new BehaviorSubject<CartItem[]>([]);
private cartTotal$ = new BehaviorSubject<number>(0);
// 公开的Observable
readonly cartItems = this.cartItems$.asObservable();
readonly cartTotal = this.cartTotal$.asObservable();
readonly cartItemCount = this.cartItems.pipe(
map(items => items.reduce((count, item) => count + item.quantity, 0))
);
constructor(private http: HttpClient) {
this.loadCartFromStorage();
}
addToCart(product: Product, quantity = 1): Observable<void> {
return new Observable(observer => {
const currentItems = this.cartItems$.value;
const existingItem = currentItems.find(item => item.product.id === product.id);
let updatedItems: CartItem[];
if (existingItem) {
updatedItems = currentItems.map(item =>
item.product.id === product.id
? { ...item, quantity: item.quantity + quantity }
: item
);
} else {
updatedItems = [...currentItems, { product, quantity }];
}
this.updateCart(updatedItems);
observer.next();
observer.complete();
});
}
removeFromCart(productId: number): Observable<void> {
return new Observable(observer => {
const currentItems = this.cartItems$.value;
const updatedItems = currentItems.filter(item => item.product.id !== productId);
this.updateCart(updatedItems);
observer.next();
observer.complete();
});
}
updateQuantity(productId: number, quantity: number): Observable<void> {
return new Observable(observer => {
if (quantity <= 0) {
this.removeFromCart(productId).subscribe(() => {
observer.next();
observer.complete();
});
return;
}
const currentItems = this.cartItems$.value;
const updatedItems = currentItems.map(item =>
item.product.id === productId
? { ...item, quantity }
: item
);
this.updateCart(updatedItems);
observer.next();
observer.complete();
});
}
clearCart(): Observable<void> {
return new Observable(observer => {
this.updateCart([]);
observer.next();
observer.complete();
});
}
private updateCart(items: CartItem[]): void {
this.cartItems$.next(items);
const total = items.reduce((sum, item) => {
const price = item.product.discountPrice || item.product.price;
return sum + (price * item.quantity);
}, 0);
this.cartTotal$.next(total);
this.saveCartToStorage(items);
}
private loadCartFromStorage(): void {
const savedCart = localStorage.getItem('cart');
if (savedCart) {
try {
const items = JSON.parse(savedCart) as CartItem[];
this.updateCart(items);
} catch (error) {
console.error('加载购物车数据失败:', error);
}
}
}
private saveCartToStorage(items: CartItem[]): void {
localStorage.setItem('cart', JSON.stringify(items));
}
}
// 购物车项目接口
interface CartItem {
product: Product;
quantity: number;
}
四、模板语法
(一)数据绑定
1. 插值表达式
@Component({
selector: 'app-interpolation-demo',
template: `
<div class="demo-container">
<!-- 基本插值 -->
<h2>{{ title }}</h2>
<p>当前时间: {{ currentTime | date:'yyyy-MM-dd HH:mm:ss' }}</p>
<!-- 表达式计算 -->
<p>总价: {{ price * quantity | currency:'CNY' }}</p>
<p>折扣价: {{ calculateDiscountPrice() | currency:'CNY' }}</p>
<!-- 条件表达式 -->
<p>状态: {{ isActive ? '激活' : '未激活' }}</p>
<p>等级: {{ user?.level || '普通用户' }}</p>
<!-- 方法调用 -->
<p>格式化名称: {{ formatUserName(user) }}</p>
<!-- 避免副作用的表达式 -->
<p>商品数量: {{ items.length }}</p>
<p>第一个商品: {{ items[0]?.name || '暂无商品' }}</p>
</div>
`
})
export class InterpolationDemoComponent {
title = 'Angular插值演示';
currentTime = new Date();
price = 99.99;
quantity = 2;
discountRate = 0.1;
isActive = true;
user = {
name: '张三',
level: 'VIP',
firstName: '三',
lastName: '张'
};
items = [
{ name: '商品1', price: 100 },
{ name: '商品2', price: 200 }
];
constructor() {
// 定时更新时间
setInterval(() => {
this.currentTime = new Date();
}, 1000);
}
calculateDiscountPrice(): number {
return this.price * this.quantity * (1 - this.discountRate);
}
formatUserName(user: any): string {
return user ? `${user.lastName}${user.firstName}` : '未知用户';
}
}
2. 属性绑定
@Component({
selector: 'app-property-binding-demo',
template: `
<div class="demo-container">
<!-- HTML属性绑定 -->
<img [src]="imageUrl" [alt]="imageAlt" [width]="imageWidth">
<!-- 类绑定 -->
<div [class]="cssClass">基本类绑定</div>
<div [class.active]="isActive">条件类绑定</div>
<div [ngClass]="getClasses()">多类绑定</div>
<!-- 样式绑定 -->
<div [style.color]="textColor">颜色绑定</div>
<div [style.font-size.px]="fontSize">字体大小绑定</div>
<div [ngStyle]="getStyles()">多样式绑定</div>
<!-- 表单控件属性 -->
<input [value]="inputValue" [disabled]="isDisabled" [placeholder]="placeholder">
<button [disabled]="!canSubmit">提交</button>
<!-- 自定义属性 -->
<div [attr.data-id]="userId" [attr.aria-label]="ariaLabel">
自定义属性绑定
</div>
<!-- 内容绑定 -->
<div [innerHTML]="htmlContent"></div>
<div [textContent]="textContent"></div>
</div>
`
})
export class PropertyBindingDemoComponent {
imageUrl = 'https://via.placeholder.com/300x200';
imageAlt = '示例图片';
imageWidth = 300;
cssClass = 'highlight';
isActive = true;
textColor = '#007bff';
fontSize = 16;
inputValue = '默认值';
isDisabled = false;
placeholder = '请输入内容';
canSubmit = true;
userId = 123;
ariaLabel = '用户信息';
htmlContent = '<strong>这是HTML内容</strong>';
textContent = '这是纯文本内容';
getClasses(): any {
return {
'active': this.isActive,
'disabled': this.isDisabled,
'highlight': true
};
}
getStyles(): any {
return {
'color': this.textColor,
'font-size': this.fontSize + 'px',
'font-weight': this.isActive ? 'bold' : 'normal'
};
}
}
3. 事件绑定
@Component({
selector: 'app-event-binding-demo',
template: `
<div class="demo-container">
<!-- 基本事件绑定 -->
<button (click)="onClick()">点击事件</button>
<button (click)="onClickWithParam('参数')">带参数点击</button>
<!-- 事件对象 -->
<input (input)="onInput($event)" (keyup.enter)="onEnter($event)">
<div (mouseover)="onMouseOver($event)" (mouseleave)="onMouseLeave($event)">
鼠标悬停区域
</div>
<!-- 键盘事件 -->
<input
(keydown)="onKeyDown($event)"
(keyup.escape)="onEscape()"
(keyup.ctrl.s)="onSave()"
placeholder="键盘事件演示">
<!-- 表单事件 -->
<form (submit)="onSubmit($event)" (reset)="onReset()">
<input [(ngModel)]="formData.name" name="name" placeholder="姓名">
<input [(ngModel)]="formData.email" name="email" placeholder="邮箱">
<button type="submit">提交</button>
<button type="reset">重置</button>
</form>
<!-- 自定义事件 -->
<app-custom-component
(customEvent)="onCustomEvent($event)"
(dataChange)="onDataChange($event)">
</app-custom-component>
<!-- 事件修饰符 -->
<div (click)="onOuterClick()" class="outer">
外层
<div (click)="onInnerClick($event)" class="inner">
内层(点击测试事件冒泡)
</div>
<div (click.stop)="onStopPropagation()" class="inner">
阻止冒泡
</div>
</div>
<!-- 防抖事件 -->
<input
(input)="onDebouncedInput($event)"
placeholder="防抖输入">
<div class="event-log">
<h4>事件日志:</h4>
<div *ngFor="let log of eventLogs" class="log-item">
{{ log.timestamp | date:'HH:mm:ss' }} - {{ log.message }}
</div>
</div>
</div>
`
})
export class EventBindingDemoComponent {
formData = {
name: '',
email: ''
};
eventLogs: Array<{timestamp: Date, message: string}> = [];
private debounceTimer: any;
onClick(): void {
this.addLog('按钮被点击');
}
onClickWithParam(param: string): void {
this.addLog(`带参数点击: ${param}`);
}
onInput(event: Event): void {
const target = event.target as HTMLInputElement;
this.addLog(`输入内容: ${target.value}`);
}
onEnter(event: KeyboardEvent): void {
const target = event.target as HTMLInputElement;
this.addLog(`回车确认: ${target.value}`);
}
onMouseOver(event: MouseEvent): void {
this.addLog('鼠标悬停');
}
onMouseLeave(event: MouseEvent): void {
this.addLog('鼠标离开');
}
onKeyDown(event: KeyboardEvent): void {
this.addLog(`按键按下: ${event.key}`);
}
onEscape(): void {
this.addLog('ESC键被按下');
}
onSave(): void {
this.addLog('Ctrl+S 保存快捷键');
}
onSubmit(event: Event): void {
event.preventDefault();
this.addLog(`表单提交: ${JSON.stringify(this.formData)}`);
}
onReset(): void {
this.formData = { name: '', email: '' };
this.addLog('表单重置');
}
onCustomEvent(data: any): void {
this.addLog(`自定义事件: ${JSON.stringify(data)}`);
}
onDataChange(data: any): void {
this.addLog(`数据变化: ${JSON.stringify(data)}`);
}
onOuterClick(): void {
this.addLog('外层点击');
}
onInnerClick(event: Event): void {
this.addLog('内层点击');
}
onStopPropagation(): void {
this.addLog('阻止冒泡点击');
}
onDebouncedInput(event: Event): void {
const target = event.target as HTMLInputElement;
// 清除之前的定时器
if (this.debounceTimer) {
clearTimeout(this.debounceTimer);
}
// 设置新的定时器
this.debounceTimer = setTimeout(() => {
this.addLog(`防抖输入: ${target.value}`);
}, 500);
}
private addLog(message: string): void {
this.eventLogs.unshift({
timestamp: new Date(),
message
});
// 保持日志数量在合理范围内
if (this.eventLogs.length > 20) {
this.eventLogs = this.eventLogs.slice(0, 20);
}
}
}
4. 双向数据绑定
@Component({
selector: 'app-two-way-binding-demo',
template: `
<div class="demo-container">
<h3>双向数据绑定演示</h3>
<!-- 基本双向绑定 -->
<div class="form-group">
<label>姓名:</label>
<input [(ngModel)]="user.name" placeholder="请输入姓名">
<p>当前值: {{ user.name }}</p>
</div>
<!-- 复选框 -->
<div class="form-group">
<label>
<input type="checkbox" [(ngModel)]="user.isActive">
激活状态
</label>
<p>状态: {{ user.isActive ? '激活' : '未激活' }}</p>
</div>
<!-- 单选按钮 -->
<div class="form-group">
<label>性别:</label>
<label>
<input type="radio" [(ngModel)]="user.gender" value="male">
男
</label>
<label>
<input type="radio" [(ngModel)]="user.gender" value="female">
女
</label>
<p>选择: {{ user.gender }}</p>
</div>
<!-- 下拉选择 -->
<div class="form-group">
<label>城市:</label>
<select [(ngModel)]="user.city">
<option value="">请选择城市</option>
<option *ngFor="let city of cities" [value]="city.value">
{{ city.label }}
</option>
</select>
<p>选择的城市: {{ user.city }}</p>
</div>
<!-- 多选 -->
<div class="form-group">
<label>兴趣爱好:</label>
<div *ngFor="let hobby of hobbies">
<label>
<input
type="checkbox"
[checked]="user.hobbies.includes(hobby.value)"
(change)="onHobbyChange(hobby.value, $event)">
{{ hobby.label }}
</label>
</div>
<p>选择的爱好: {{ user.hobbies.join(', ') }}</p>
</div>
<!-- 文本域 -->
<div class="form-group">
<label>个人简介:</label>
<textarea [(ngModel)]="user.bio" rows="4" placeholder="请输入个人简介"></textarea>
<p>字符数: {{ user.bio.length }}</p>
</div>
<!-- 自定义双向绑定组件 -->
<div class="form-group">
<label>评分:</label>
<app-rating [(rating)]="user.rating" [maxRating]="5"></app-rating>
<p>当前评分: {{ user.rating }}</p>
</div>
<!-- 数据预览 -->
<div class="data-preview">
<h4>用户数据:</h4>
<pre>{{ user | json }}</pre>
</div>
<!-- 操作按钮 -->
<div class="actions">
<button (click)="resetForm()">重置表单</button>
<button (click)="saveUser()" [disabled]="!isFormValid()">保存用户</button>
</div>
</div>
`
})
export class TwoWayBindingDemoComponent {
user = {
name: '',
isActive: false,
gender: '',
city: '',
hobbies: [] as string[],
bio: '',
rating: 0
};
cities = [
{ value: 'beijing', label: '北京' },
{ value: 'shanghai', label: '上海' },
{ value: 'guangzhou', label: '广州' },
{ value: 'shenzhen', label: '深圳' }
];
hobbies = [
{ value: 'reading', label: '阅读' },
{ value: 'music', label: '音乐' },
{ value: 'sports', label: '运动' },
{ value: 'travel', label: '旅行' },
{ value: 'coding', label: '编程' }
];
onHobbyChange(hobby: string, event: Event): void {
const target = event.target as HTMLInputElement;
if (target.checked) {
if (!this.user.hobbies.includes(hobby)) {
this.user.hobbies.push(hobby);
}
} else {
this.user.hobbies = this.user.hobbies.filter(h => h !== hobby);
}
}
resetForm(): void {
this.user = {
name: '',
isActive: false,
gender: '',
city: '',
hobbies: [],
bio: '',
rating: 0
};
}
saveUser(): void {
console.log('保存用户数据:', this.user);
alert('用户数据已保存!');
}
isFormValid(): boolean {
return this.user.name.trim().length > 0 &&
this.user.gender !== '' &&
this.user.city !== '';
}
}
(二)结构指令
1. *ngIf 条件渲染
@Component({
selector: 'app-ngif-demo',
template: `
<div class="demo-container">
<h3>*ngIf 条件渲染演示</h3>
<!-- 基本条件渲染 -->
<div class="controls">
<button (click)="toggleVisibility()">切换显示</button>
<button (click)="toggleUser()">切换用户</button>
<button (click)="toggleLoading()">切换加载状态</button>
</div>
<!-- 简单条件 -->
<div *ngIf="isVisible" class="message">
这个内容是条件显示的
</div>
<!-- 条件表达式 -->
<div *ngIf="user && user.isActive" class="user-info">
用户 {{ user.name }} 当前是激活状态
</div>
<!-- if-else 模式 -->
<div *ngIf="isLoading; else contentTemplate" class="loading">
正在加载...
</div>
<ng-template #contentTemplate>
<div class="content">
内容已加载完成
</div>
</ng-template>
<!-- if-then-else 模式 -->
<div *ngIf="user; then userTemplate; else noUserTemplate"></div>
<ng-template #userTemplate>
<div class="user-card">
<h4>{{ user.name }}</h4>
<p>邮箱: {{ user.email }}</p>
<p>状态: {{ user.isActive ? '激活' : '未激活' }}</p>
</div>
</ng-template>
<ng-template #noUserTemplate>
<div class="no-user">
暂无用户信息
</div>
</ng-template>
<!-- 复杂条件 -->
<div *ngIf="user && user.permissions && user.permissions.includes('admin')"
class="admin-panel">
管理员面板
</div>
<!-- 嵌套条件 -->
<div *ngIf="user" class="user-details">
<h4>用户详情</h4>
<div *ngIf="user.profile" class="profile">
<div *ngIf="user.profile.avatar" class="avatar">
<img [src]="user.profile.avatar" [alt]="user.name">
</div>
<div *ngIf="user.profile.bio" class="bio">
{{ user.profile.bio }}
</div>
</div>
<div *ngIf="!user.profile" class="no-profile">
用户未完善个人资料
</div>
</div>
<!-- 性能优化:使用trackBy -->
<div *ngIf="showExpensiveComponent" class="expensive">
<app-expensive-component [data]="expensiveData"></app-expensive-component>
</div>
</div>
`
})
export class NgIfDemoComponent {
isVisible = true;
isLoading = false;
showExpensiveComponent = false;
user: any = {
name: '张三',
email: 'zhangsan@example.com',
isActive: true,
permissions: ['user', 'admin'],
profile: {
avatar: 'https://via.placeholder.com/50',
bio: '这是用户的个人简介'
}
};
expensiveData = { /* 复杂数据 */ };
toggleVisibility(): void {
this.isVisible = !this.isVisible;
}
toggleUser(): void {
this.user = this.user ? null : {
name: '李四',
email: 'lisi@example.com',
isActive: false,
permissions: ['user'],
profile: null
};
}
toggleLoading(): void {
this.isLoading = !this.isLoading;
}
}
2. *ngFor 列表渲染
@Component({
selector: 'app-ngfor-demo',
template: `
<div class="demo-container">
<h3>*ngFor 列表渲染演示</h3>
<!-- 基本列表 -->
<div class="section">
<h4>基本列表</h4>
<ul>
<li *ngFor="let item of items">{{ item }}</li>
</ul>
</div>
<!-- 带索引的列表 -->
<div class="section">
<h4>带索引的列表</h4>
<ul>
<li *ngFor="let item of items; let i = index">
{{ i + 1 }}. {{ item }}
</li>
</ul>
</div>
<!-- 复杂对象列表 -->
<div class="section">
<h4>用户列表</h4>
<div class="user-grid">
<div
*ngFor="let user of users; let i = index; let first = first; let last = last; let even = even; let odd = odd; trackBy: trackByUserId"
class="user-card"
[class.first]="first"
[class.last]="last"
[class.even]="even"
[class.odd]="odd">
<div class="user-header">
<span class="index">#{{ i + 1 }}</span>
<span class="status" [class.active]="user.isActive"></span>
</div>
<div class="user-info">
<h5>{{ user.name }}</h5>
<p>{{ user.email }}</p>
<p>年龄: {{ user.age }}</p>
</div>
<div class="user-actions">
<button (click)="editUser(user)">编辑</button>
<button (click)="deleteUser(user.id)">删除</button>
</div>
</div>
</div>
</div>
<!-- 嵌套循环 -->
<div class="section">
<h4>分类商品</h4>
<div *ngFor="let category of categories; trackBy: trackByCategoryId" class="category">
<h5>{{ category.name }}</h5>
<div class="products">
<div
*ngFor="let product of category.products; trackBy: trackByProductId"
class="product-item">
<span>{{ product.name }}</span>
<span class="price">¥{{ product.price }}</span>
</div>
</div>
</div>
</div>
<!-- 条件循环 -->
<div class="section">
<h4>激活用户</h4>
<div *ngFor="let user of users; trackBy: trackByUserId">
<div *ngIf="user.isActive" class="active-user">
{{ user.name }} - {{ user.email }}
</div>
</div>
</div>
<!-- 空列表处理 -->
<div class="section">
<h4>搜索结果</h4>
<input [(ngModel)]="searchTerm" placeholder="搜索用户...">
<div *ngIf="filteredUsers.length > 0; else noResults">
<div *ngFor="let user of filteredUsers; trackBy: trackByUserId" class="search-result">
{{ user.name }} - {{ user.email }}
</div>
</div>
<ng-template #noResults>
<div class="no-results">
<p *ngIf="searchTerm">没有找到匹配 "{{ searchTerm }}" 的用户</p>
<p *ngIf="!searchTerm">请输入搜索关键词</p>
</div>
</ng-template>
</div>
<!-- 操作按钮 -->
<div class="actions">
<button (click)="addUser()">添加用户</button>
<button (click)="shuffleUsers()">随机排序</button>
<button (click)="sortUsers()">按名称排序</button>
<button (click)="clearUsers()">清空列表</button>
</div>
</div>
`
})
export class NgForDemoComponent implements OnInit {
items = ['苹果', '香蕉', '橙子', '葡萄'];
users = [
{ id: 1, name: '张三', email: 'zhangsan@example.com', age: 25, isActive: true },
{ id: 2, name: '李四', email: 'lisi@example.com', age: 30, isActive: false },
{ id: 3, name: '王五', email: 'wangwu@example.com', age: 28, isActive: true },
{ id: 4, name: '赵六', email: 'zhaoliu@example.com', age: 35, isActive: true }
];
categories = [
{
id: 1,
name: '电子产品',
products: [
{ id: 1, name: '手机', price: 3999 },
{ id: 2, name: '电脑', price: 8999 },
{ id: 3, name: '平板', price: 2999 }
]
},
{
id: 2,
name: '服装',
products: [
{ id: 4, name: 'T恤', price: 99 },
{ id: 5, name: '牛仔裤', price: 299 },
{ id: 6, name: '运动鞋', price: 599 }
]
}
];
searchTerm = '';
get filteredUsers() {
if (!this.searchTerm) {
return [];
}
return this.users.filter(user =>
user.name.toLowerCase().includes(this.searchTerm.toLowerCase()) ||
user.email.toLowerCase().includes(this.searchTerm.toLowerCase())
);
}
ngOnInit(): void {
console.log('NgFor演示组件初始化');
}
// TrackBy函数优化性能
trackByUserId(index: number, user: any): number {
return user.id;
}
trackByCategoryId(index: number, category: any): number {
return category.id;
}
trackByProductId(index: number, product: any): number {
return product.id;
}
editUser(user: any): void {
console.log('编辑用户:', user);
}
deleteUser(userId: number): void {
this.users = this.users.filter(user => user.id !== userId);
}
addUser(): void {
const newId = Math.max(...this.users.map(u => u.id)) + 1;
const newUser = {
id: newId,
name: `用户${newId}`,
email: `user${newId}@example.com`,
age: Math.floor(Math.random() * 40) + 20,
isActive: Math.random() > 0.5
};
this.users.push(newUser);
}
shuffleUsers(): void {
this.users = [...this.users].sort(() => Math.random() - 0.5);
}
sortUsers(): void {
this.users = [...this.users].sort((a, b) => a.name.localeCompare(b.name));
}
clearUsers(): void {
this.users = [];
}
}
3. ngSwitch 多条件渲染
@Component({
selector: 'app-ngswitch-demo',
template: `
<div class="demo-container">
<h3>ngSwitch 多条件渲染演示</h3>
<!-- 控制面板 -->
<div class="controls">
<label>选择视图类型:</label>
<select [(ngModel)]="viewType">
<option value="list">列表视图</option>
<option value="grid">网格视图</option>
<option value="card">卡片视图</option>
<option value="table">表格视图</option>
</select>
<label>用户状态:</label>
<select [(ngModel)]="userStatus">
<option value="active">激活</option>
<option value="inactive">未激活</option>
<option value="pending">待审核</option>
<option value="banned">已禁用</option>
</select>
</div>
<!-- 基本 ngSwitch -->
<div class="section">
<h4>视图切换</h4>
<div [ngSwitch]="viewType">
<div *ngSwitchCase="'list'" class="list-view">
<h5>列表视图</h5>
<ul>
<li *ngFor="let user of users">{{ user.name }} - {{ user.email }}</li>
</ul>
</div>
<div *ngSwitchCase="'grid'" class="grid-view">
<h5>网格视图</h5>
<div class="user-grid">
<div *ngFor="let user of users" class="grid-item">
<strong>{{ user.name }}</strong>
<br>
<small>{{ user.email }}</small>
</div>
</div>
</div>
<div *ngSwitchCase="'card'" class="card-view">
<h5>卡片视图</h5>
<div class="card-container">
<div *ngFor="let user of users" class="user-card">
<div class="card-header">{{ user.name }}</div>
<div class="card-body">
<p>邮箱: {{ user.email }}</p>
<p>年龄: {{ user.age }}</p>
</div>
</div>
</div>
</div>
<div *ngSwitchCase="'table'" class="table-view">
<h5>表格视图</h5>
<table>
<thead>
<tr>
<th>姓名</th>
<th>邮箱</th>
<th>年龄</th>
<th>状态</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let user of users">
<td>{{ user.name }}</td>
<td>{{ user.email }}</td>
<td>{{ user.age }}</td>
<td>{{ user.isActive ? '激活' : '未激活' }}</td>
</tr>
</tbody>
</table>
</div>
<div *ngSwitchDefault class="default-view">
<p>未知的视图类型</p>
</div>
</div>
</div>
<!-- 用户状态显示 -->
<div class="section">
<h4>用户状态</h4>
<div [ngSwitch]="userStatus" class="status-display">
<div *ngSwitchCase="'active'" class="status active">
<i class="icon-check"></i>
<span>用户状态正常,可以正常使用所有功能</span>
</div>
<div *ngSwitchCase="'inactive'" class="status inactive">
<i class="icon-pause"></i>
<span>用户未激活,请检查邮箱并完成激活</span>
</div>
<div *ngSwitchCase="'pending'" class="status pending">
<i class="icon-clock"></i>
<span>用户待审核,管理员正在审核中</span>
</div>
<div *ngSwitchCase="'banned'" class="status banned">
<i class="icon-ban"></i>
<span>用户已被禁用,如有疑问请联系管理员</span>
</div>
<div *ngSwitchDefault class="status unknown">
<i class="icon-question"></i>
<span>未知状态</span>
</div>
</div>
</div>
<!-- 嵌套 ngSwitch -->
<div class="section">
<h4>嵌套条件</h4>
<div [ngSwitch]="viewType">
<div *ngSwitchCase="'card'">
<div [ngSwitch]="userStatus">
<div *ngSwitchCase="'active'" class="nested-content">
激活用户的卡片视图
</div>
<div *ngSwitchCase="'inactive'" class="nested-content">
未激活用户的卡片视图
</div>
<div *ngSwitchDefault class="nested-content">
其他状态用户的卡片视图
</div>
</div>
</div>
<div *ngSwitchDefault>
非卡片视图模式
</div>
</div>
</div>
</div>
`
})
export class NgSwitchDemoComponent {
viewType = 'list';
userStatus = 'active';
users = [
{ id: 1, name: '张三', email: 'zhangsan@example.com', age: 25, isActive: true },
{ id: 2, name: '李四', email: 'lisi@example.com', age: 30, isActive: false },
{ id: 3, name: '王五', email: 'wangwu@example.com', age: 28, isActive: true }
];
}
五、依赖注入
(一)服务与依赖注入基础
1. 创建和注册服务
// 基础服务
@Injectable({
providedIn: 'root' // 在根注入器中提供,创建单例
})
export class LoggerService {
private logs: string[] = [];
log(message: string): void {
const timestamp = new Date().toISOString();
const logEntry = `[${timestamp}] ${message}`;
this.logs.push(logEntry);
console.log(logEntry);
}
error(message: string): void {
const timestamp = new Date().toISOString();
const logEntry = `[${timestamp}] ERROR: ${message}`;
this.logs.push(logEntry);
console.error(logEntry);
}
getLogs(): string[] {
return [...this.logs];
}
clearLogs(): void {
this.logs = [];
}
}
// HTTP服务
@Injectable({
providedIn: 'root'
})
export class ApiService {
private baseUrl = environment.apiUrl;
constructor(
private http: HttpClient,
private logger: LoggerService
) {
this.logger.log('ApiService 初始化');
}
get<T>(endpoint: string): Observable<T> {
const url = `${this.baseUrl}/${endpoint}`;
this.logger.log(`GET 请求: ${url}`);
return this.http.get<T>(url).pipe(
tap(() => this.logger.log(`GET 请求成功: ${url}`)),
catchError(error => {
this.logger.error(`GET 请求失败: ${url} - ${error.message}`);
return throwError(() => error);
})
);
}
post<T>(endpoint: string, data: any): Observable<T> {
const url = `${this.baseUrl}/${endpoint}`;
this.logger.log(`POST 请求: ${url}`);
return this.http.post<T>(url, data).pipe(
tap(() => this.logger.log(`POST 请求成功: ${url}`)),
catchError(error => {
this.logger.error(`POST 请求失败: ${url} - ${error.message}`);
return throwError(() => error);
})
);
}
}
2. 不同的注入方式
// 在模块中提供服务
@NgModule({
providers: [
// 基本提供方式
UserService,
// 使用类提供
{ provide: UserService, useClass: UserService },
// 使用现有服务
{ provide: 'UserServiceAlias', useExisting: UserService },
// 使用工厂函数
{
provide: 'ApiConfig',
useFactory: (env: any) => {
return {
baseUrl: env.production ? 'https://api.prod.com' : 'http://localhost:3000',
timeout: 5000
};
},
deps: ['Environment']
},
// 使用值
{ provide: 'API_URL', useValue: 'https://api.example.com' },
// 条件提供
{
provide: StorageService,
useClass: environment.production ? CloudStorageService : LocalStorageService
}
]
})
export class AppModule {}
// 在组件中提供服务(组件级别的实例)
@Component({
selector: 'app-user-profile',
template: `...`,
providers: [
// 每个组件实例都会有自己的服务实例
UserProfileService,
// 使用不同的实现
{ provide: NotificationService, useClass: ToastNotificationService }
]
})
export class UserProfileComponent {
constructor(
private userProfileService: UserProfileService,
private notificationService: NotificationService
) {}
}
3. 注入令牌和接口
// 定义接口
export interface StorageInterface {
get(key: string): any;
set(key: string, value: any): void;
remove(key: string): void;
clear(): void;
}
// 创建注入令牌
export const STORAGE_SERVICE = new InjectionToken<StorageInterface>('StorageService');
// 实现接口的服务
@Injectable()
export class LocalStorageService implements StorageInterface {
get(key: string): any {
const item = localStorage.getItem(key);
try {
return item ? JSON.parse(item) : null;
} catch {
return item;
}
}
set(key: string, value: any): void {
localStorage.setItem(key, JSON.stringify(value));
}
remove(key: string): void {
localStorage.removeItem(key);
}
clear(): void {
localStorage.clear();
}
}
@Injectable()
export class SessionStorageService implements StorageInterface {
get(key: string): any {
const item = sessionStorage.getItem(key);
try {
return item ? JSON.parse(item) : null;
} catch {
return item;
}
}
set(key: string, value: any): void {
sessionStorage.setItem(key, JSON.stringify(value));
}
remove(key: string): void {
sessionStorage.removeItem(key);
}
clear(): void {
sessionStorage.clear();
}
}
// 在模块中配置
@NgModule({
providers: [
{
provide: STORAGE_SERVICE,
useClass: LocalStorageService
}
]
})
export class AppModule {}
// 在组件中使用
@Component({
selector: 'app-settings',
template: `
<div>
<h3>用户设置</h3>
<label>
<input type="checkbox" [(ngModel)]="darkMode" (change)="saveSetting('darkMode', darkMode)">
深色模式
</label>
<label>
<select [(ngModel)]="language" (change)="saveSetting('language', language)">
<option value="zh">中文</option>
<option value="en">English</option>
</select>
</label>
</div>
`
})
export class SettingsComponent implements OnInit {
darkMode = false;
language = 'zh';
constructor(
@Inject(STORAGE_SERVICE) private storage: StorageInterface
) {}
ngOnInit(): void {
this.loadSettings();
}
saveSetting(key: string, value: any): void {
this.storage.set(key, value);
}
private loadSettings(): void {
this.darkMode = this.storage.get('darkMode') || false;
this.language = this.storage.get('language') || 'zh';
}
}
(二)路由守卫
1. 认证守卫
// auth.guard.ts
@Injectable({
providedIn: 'root'
})
export class AuthGuard implements CanActivate, CanActivateChild {
constructor(
private authService: AuthService,
private router: Router,
private snackBar: MatSnackBar
) {}
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
return this.checkAuth(state.url);
}
canActivateChild(
childRoute: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
return this.canActivate(childRoute, state);
}
private checkAuth(url: string): Observable<boolean | UrlTree> {
return this.authService.isAuthenticated().pipe(
map(isAuth => {
if (isAuth) {
return true;
} else {
this.snackBar.open('请先登录', '关闭', { duration: 3000 });
return this.router.createUrlTree(['/login'], {
queryParams: { returnUrl: url }
});
}
}),
catchError(() => {
return of(this.router.createUrlTree(['/login']));
})
);
}
}
// admin.guard.ts
@Injectable({
providedIn: 'root'
})
export class AdminGuard implements CanActivate {
constructor(
private authService: AuthService,
private router: Router
) {}
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<boolean | UrlTree> {
return this.authService.getCurrentUser().pipe(
map(user => {
if (user && user.role === 'admin') {
return true;
} else {
this.router.navigate(['/unauthorized']);
return false;
}
})
);
}
}
// unsaved-changes.guard.ts
export interface CanComponentDeactivate {
canDeactivate(): Observable<boolean> | Promise<boolean> | boolean;
}
@Injectable({
providedIn: 'root'
})
export class UnsavedChangesGuard implements CanDeactivate<CanComponentDeactivate> {
canDeactivate(
component: CanComponentDeactivate,
currentRoute: ActivatedRouteSnapshot,
currentState: RouterStateSnapshot,
nextState?: RouterStateSnapshot
): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
if (component.canDeactivate) {
return component.canDeactivate();
}
return true;
}
}
// 实现CanComponentDeactivate接口的组件
@Component({
selector: 'app-profile-edit',
template: `
<form [formGroup]="profileForm" (ngSubmit)="onSubmit()">
<div class="form-group">
<label for="name">姓名</label>
<input type="text" id="name" formControlName="name" class="form-control">
</div>
<div class="form-group">
<label for="email">邮箱</label>
<input type="email" id="email" formControlName="email" class="form-control">
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary" [disabled]="profileForm.invalid">
保存
</button>
<button type="button" (click)="cancel()" class="btn btn-secondary">
取消
</button>
</div>
</form>
`
})
export class ProfileEditComponent implements OnInit, CanComponentDeactivate {
profileForm!: FormGroup;
private originalValue: any;
constructor(
private fb: FormBuilder,
private router: Router,
private dialog: MatDialog
) {}
ngOnInit(): void {
this.profileForm = this.fb.group({
name: ['', Validators.required],
email: ['', [Validators.required, Validators.email]]
});
// 保存原始值
this.originalValue = this.profileForm.value;
}
canDeactivate(): Observable<boolean> | boolean {
if (this.hasUnsavedChanges()) {
const dialogRef = this.dialog.open(ConfirmDialogComponent, {
data: {
title: '未保存的更改',
message: '您有未保存的更改,确定要离开吗?',
confirmText: '离开',
cancelText: '取消'
}
});
return dialogRef.afterClosed();
}
return true;
}
private hasUnsavedChanges(): boolean {
return JSON.stringify(this.originalValue) !== JSON.stringify(this.profileForm.value);
}
onSubmit(): void {
if (this.profileForm.valid) {
// 保存逻辑
this.originalValue = this.profileForm.value;
}
}
cancel(): void {
this.router.navigate(['/profile']);
}
}
2. 数据预加载守卫
// data-resolver.service.ts
@Injectable({
providedIn: 'root'
})
export class UserResolver implements Resolve<User> {
constructor(private userService: UserService, private router: Router) {}
resolve(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<User> | Promise<User> | User {
const userId = +route.paramMap.get('id')!;
return this.userService.getUserById(userId).pipe(
catchError(error => {
console.error('加载用户数据失败:', error);
this.router.navigate(['/users']);
return EMPTY;
})
);
}
}
// 在路由配置中使用
const routes: Routes = [
{
path: 'user/:id',
component: UserDetailComponent,
resolve: {
user: UserResolver
}
}
];
// 在组件中使用预加载的数据
@Component({
selector: 'app-user-detail',
template: `
<div class="user-detail">
<h2>{{ user.name }}</h2>
<p>{{ user.email }}</p>
</div>
`
})
export class UserDetailComponent implements OnInit {
user!: User;
constructor(private route: ActivatedRoute) {}
ngOnInit(): void {
// 从路由数据中获取预加载的用户信息
this.user = this.route.snapshot.data['user'];
// 或者监听数据变化
this.route.data.subscribe(data => {
this.user = data['user'];
});
}
}
九、生命周期钩子
(一)组件生命周期
1. 完整生命周期示例
@Component({
selector: 'app-lifecycle-demo',
template: `
<div class="lifecycle-demo">
<h3>生命周期演示组件</h3>
<p>计数器: {{ counter }}</p>
<p>输入值: {{ inputValue }}</p>
<input
type="text"
[(ngModel)]="inputValue"
placeholder="输入一些文本">
<button (click)="increment()">增加计数</button>
<button (click)="triggerError()">触发错误</button>
<div class="lifecycle-log">
<h4>生命周期日志:</h4>
<ul>
<li *ngFor="let log of lifecycleLogs">{{ log }}</li>
</ul>
<button (click)="clearLogs()">清空日志</button>
</div>
</div>
`
})
export class LifecycleDemoComponent implements
OnInit, OnDestroy, OnChanges, DoCheck,
AfterContentInit, AfterContentChecked,
AfterViewInit, AfterViewChecked {
@Input() externalData: any;
counter = 0;
inputValue = '';
lifecycleLogs: string[] = [];
private intervalId?: number;
private previousInputValue = '';
constructor() {
this.log('Constructor: 组件实例创建');
}
// 1. ngOnChanges - 输入属性变化时调用
ngOnChanges(changes: SimpleChanges): void {
this.log('ngOnChanges: 输入属性发生变化');
for (const propName in changes) {
const change = changes[propName];
const current = JSON.stringify(change.currentValue);
const previous = JSON.stringify(change.previousValue);
this.log(` ${propName}: ${previous} => ${current}`);
}
}
// 2. ngOnInit - 组件初始化完成
ngOnInit(): void {
this.log('ngOnInit: 组件初始化完成');
// 启动定时器
this.intervalId = window.setInterval(() => {
// 这里可以执行一些定期任务
}, 5000);
// 初始化数据
this.loadInitialData();
}
// 3. ngDoCheck - 每次变更检测时调用
ngDoCheck(): void {
// 注意:这个方法会频繁调用,避免在这里执行重操作
if (this.inputValue !== this.previousInputValue) {
this.log(`ngDoCheck: 检测到输入值变化 ${this.previousInputValue} => ${this.inputValue}`);
this.previousInputValue = this.inputValue;
}
}
// 4. ngAfterContentInit - 内容投影初始化完成
ngAfterContentInit(): void {
this.log('ngAfterContentInit: 内容投影初始化完成');
}
// 5. ngAfterContentChecked - 内容投影检查完成
ngAfterContentChecked(): void {
// 这个方法也会频繁调用
// this.log('ngAfterContentChecked: 内容投影检查完成');
}
// 6. ngAfterViewInit - 视图初始化完成
ngAfterViewInit(): void {
this.log('ngAfterViewInit: 视图初始化完成');
// 在这里可以安全地访问视图中的元素
this.setupViewRelatedFeatures();
}
// 7. ngAfterViewChecked - 视图检查完成
ngAfterViewChecked(): void {
// 这个方法也会频繁调用
// this.log('ngAfterViewChecked: 视图检查完成');
}
// 8. ngOnDestroy - 组件销毁前
ngOnDestroy(): void {
this.log('ngOnDestroy: 组件即将销毁');
// 清理工作
if (this.intervalId) {
clearInterval(this.intervalId);
}
// 取消订阅
this.cleanup();
}
// 辅助方法
private log(message: string): void {
const timestamp = new Date().toLocaleTimeString();
this.lifecycleLogs.push(`[${timestamp}] ${message}`);
console.log(message);
// 限制日志数量
if (this.lifecycleLogs.length > 20) {
this.lifecycleLogs.shift();
}
}
private loadInitialData(): void {
// 模拟数据加载
setTimeout(() => {
this.log('初始数据加载完成');
}, 1000);
}
private setupViewRelatedFeatures(): void {
// 设置视图相关功能
this.log('设置视图相关功能');
}
private cleanup(): void {
// 清理订阅、事件监听器等
this.log('执行清理工作');
}
// 组件方法
increment(): void {
this.counter++;
this.log(`计数器增加到: ${this.counter}`);
}
triggerError(): void {
throw new Error('这是一个测试错误');
}
clearLogs(): void {
this.lifecycleLogs = [];
}
}
2. 实际应用场景
// 数据加载组件
@Component({
selector: 'app-data-loader',
template: `
<div class="data-loader">
<div *ngIf="loading" class="loading">
正在加载数据...
</div>
<div *ngIf="!loading && data" class="data-content">
<h3>{{ data.title }}</h3>
<p>{{ data.description }}</p>
<ul>
<li *ngFor="let item of data.items">{{ item.name }}</li>
</ul>
</div>
<div *ngIf="!loading && error" class="error">
加载失败: {{ error.message }}
<button (click)="retry()">重试</button>
</div>
</div>
`
})
export class DataLoaderComponent implements OnInit, OnDestroy {
@Input() dataId!: string;
data: any = null;
loading = false;
error: any = null;
private destroy$ = new Subject<void>();
private retryCount = 0;
private maxRetries = 3;
constructor(private dataService: DataService) {}
ngOnInit(): void {
this.loadData();
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
private loadData(): void {
if (!this.dataId) {
this.error = { message: '缺少数据ID' };
return;
}
this.loading = true;
this.error = null;
this.dataService.getData(this.dataId).pipe(
takeUntil(this.destroy$),
retry(this.maxRetries),
catchError(error => {
this.error = error;
this.loading = false;
return EMPTY;
})
).subscribe({
next: data => {
this.data = data;
this.loading = false;
this.retryCount = 0;
},
error: error => {
this.error = error;
this.loading = false;
}
});
}
retry(): void {
if (this.retryCount < this.maxRetries) {
this.retryCount++;
this.loadData();
}
}
}
// 表单组件
@Component({
selector: 'app-smart-form',
template: `
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<div class="form-group" *ngFor="let field of formFields">
<label [for]="field.name">{{ field.label }}</label>
<input
[id]="field.name"
[type]="field.type"
[formControlName]="field.name"
[placeholder]="field.placeholder"
class="form-control">
<div *ngIf="form.get(field.name)?.invalid && form.get(field.name)?.touched"
class="error-message">
{{ getErrorMessage(field.name) }}
</div>
</div>
<div class="form-actions">
<button type="submit" [disabled]="form.invalid || submitting">
{{ submitting ? '提交中...' : '提交' }}
</button>
<button type="button" (click)="reset()">重置</button>
</div>
<div *ngIf="autoSaveEnabled" class="auto-save-status">
{{ autoSaveStatus }}
</div>
</form>
`
})
export class SmartFormComponent implements OnInit, OnDestroy, OnChanges {
@Input() formConfig!: FormConfig;
@Input() initialData?: any;
@Input() autoSaveEnabled = false;
@Output() formSubmit = new EventEmitter<any>();
form!: FormGroup;
formFields: FormField[] = [];
submitting = false;
autoSaveStatus = '';
private destroy$ = new Subject<void>();
private autoSaveSubscription?: Subscription;
constructor(private fb: FormBuilder) {}
ngOnChanges(changes: SimpleChanges): void {
if (changes['formConfig'] && this.formConfig) {
this.buildForm();
}
if (changes['initialData'] && this.initialData && this.form) {
this.form.patchValue(this.initialData);
}
if (changes['autoSaveEnabled']) {
this.setupAutoSave();
}
}
ngOnInit(): void {
if (this.formConfig) {
this.buildForm();
}
this.setupAutoSave();
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
if (this.autoSaveSubscription) {
this.autoSaveSubscription.unsubscribe();
}
}
private buildForm(): void {
const formControls: { [key: string]: FormControl } = {};
this.formFields = this.formConfig.fields;
this.formFields.forEach(field => {
const validators = this.buildValidators(field.validation || {});
formControls[field.name] = new FormControl(
field.defaultValue || '',
validators
);
});
this.form = this.fb.group(formControls);
if (this.initialData) {
this.form.patchValue(this.initialData);
}
}
private buildValidators(validation: any): ValidatorFn[] {
const validators: ValidatorFn[] = [];
if (validation.required) {
validators.push(Validators.required);
}
if (validation.minLength) {
validators.push(Validators.minLength(validation.minLength));
}
if (validation.maxLength) {
validators.push(Validators.maxLength(validation.maxLength));
}
if (validation.pattern) {
validators.push(Validators.pattern(validation.pattern));
}
if (validation.email) {
validators.push(Validators.email);
}
return validators;
}
private setupAutoSave(): void {
if (this.autoSaveSubscription) {
this.autoSaveSubscription.unsubscribe();
}
if (this.autoSaveEnabled && this.form) {
this.autoSaveSubscription = this.form.valueChanges.pipe(
debounceTime(2000),
distinctUntilChanged(),
takeUntil(this.destroy$)
).subscribe(value => {
this.autoSave(value);
});
}
}
private autoSave(value: any): void {
this.autoSaveStatus = '正在自动保存...';
// 模拟自动保存
setTimeout(() => {
this.autoSaveStatus = '已自动保存';
setTimeout(() => {
this.autoSaveStatus = '';
}, 2000);
}, 500);
}
getErrorMessage(fieldName: string): string {
const control = this.form.get(fieldName);
if (!control || !control.errors) return '';
const errors = control.errors;
if (errors['required']) return '此字段为必填项';
if (errors['minlength']) return `最少需要${errors['minlength'].requiredLength}个字符`;
if (errors['maxlength']) return `最多允许${errors['maxlength'].requiredLength}个字符`;
if (errors['email']) return '请输入有效的邮箱地址';
if (errors['pattern']) return '格式不正确';
return '输入无效';
}
onSubmit(): void {
if (this.form.valid) {
this.submitting = true;
this.formSubmit.emit(this.form.value);
// 模拟提交
setTimeout(() => {
this.submitting = false;
}, 2000);
}
}
reset(): void {
this.form.reset();
if (this.initialData) {
this.form.patchValue(this.initialData);
}
}
}
// 接口定义
interface FormConfig {
fields: FormField[];
}
interface FormField {
name: string;
label: string;
type: string;
placeholder?: string;
defaultValue?: any;
validation?: {
required?: boolean;
minLength?: number;
maxLength?: number;
pattern?: string;
email?: boolean;
};
}
十、性能优化
(一)变更检测优化
1. OnPush 策略
// 使用OnPush策略的组件
@Component({
selector: 'app-optimized-list',
template: `
<div class="optimized-list">
<h3>优化列表组件</h3>
<p>当前时间: {{ currentTime }}</p>
<div class="list-container">
<div
*ngFor="let item of items; trackBy: trackByFn"
class="list-item"
[class.selected]="item.selected">
<span>{{ item.name }}</span>
<span>{{ item.value }}</span>
<button (click)="selectItem(item)">选择</button>
</div>
</div>
<div class="actions">
<button (click)="addItem()">添加项目</button>
<button (click)="updateTime()">更新时间</button>
</div>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class OptimizedListComponent implements OnInit {
@Input() items: ListItem[] = [];
@Output() itemSelected = new EventEmitter<ListItem>();
currentTime = new Date().toLocaleTimeString();
constructor(private cdr: ChangeDetectorRef) {}
ngOnInit(): void {
// 使用OnPush策略时,需要手动触发变更检测
setInterval(() => {
this.currentTime = new Date().toLocaleTimeString();
this.cdr.markForCheck(); // 标记需要检查
}, 1000);
}
trackByFn(index: number, item: ListItem): any {
return item.id; // 使用唯一标识符进行跟踪
}
selectItem(item: ListItem): void {
// 创建新的数组引用以触发变更检测
this.items = this.items.map(i => ({
...i,
selected: i.id === item.id
}));
this.itemSelected.emit(item);
this.cdr.markForCheck();
}
addItem(): void {
const newItem: ListItem = {
id: Date.now(),
name: `Item ${this.items.length + 1}`,
value: Math.random(),
selected: false
};
// 创建新的数组引用
this.items = [...this.items, newItem];
this.cdr.markForCheck();
}
updateTime(): void {
this.currentTime = new Date().toLocaleTimeString();
this.cdr.markForCheck();
}
}
interface ListItem {
id: number;
name: string;
value: number;
selected: boolean;
}
2. 异步管道优化
@Component({
selector: 'app-async-data',
template: `
<div class="async-data">
<!-- 使用异步管道自动处理订阅和取消订阅 -->
<div *ngIf="loading$ | async" class="loading">
正在加载...
</div>
<div *ngIf="error$ | async as error" class="error">
错误: {{ error.message }}
</div>
<div *ngIf="data$ | async as data" class="data">
<h3>{{ data.title }}</h3>
<ul>
<li *ngFor="let item of data.items">{{ item.name }}</li>
</ul>
</div>
<!-- 组合多个Observable -->
<div *ngIf="viewModel$ | async as vm" class="view-model">
<p>用户: {{ vm.user.name }}</p>
<p>权限: {{ vm.permissions.join(', ') }}</p>
<p>设置: {{ vm.settings | json }}</p>
</div>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AsyncDataComponent implements OnInit {
data$!: Observable<any>;
loading$!: Observable<boolean>;
error$!: Observable<any>;
viewModel$!: Observable<ViewModel>;
constructor(
private dataService: DataService,
private userService: UserService
) {}
ngOnInit(): void {
// 创建loading状态流
const loadingSubject = new BehaviorSubject<boolean>(false);
this.loading$ = loadingSubject.asObservable();
// 创建错误状态流
const errorSubject = new BehaviorSubject<any>(null);
this.error$ = errorSubject.asObservable();
// 数据流
this.data$ = this.dataService.getData().pipe(
tap(() => loadingSubject.next(true)),
catchError(error => {
errorSubject.next(error);
loadingSubject.next(false);
return EMPTY;
}),
tap(() => loadingSubject.next(false))
);
// 组合多个数据流
this.viewModel$ = combineLatest([
this.userService.getCurrentUser(),
this.userService.getUserPermissions(),
this.userService.getUserSettings()
]).pipe(
map(([user, permissions, settings]) => ({
user,
permissions,
settings
})),
shareReplay(1) // 缓存最新值
);
}
}
interface ViewModel {
user: any;
permissions: string[];
settings: any;
}
(二)懒加载和代码分割
1. 模块懒加载
// app-routing.module.ts
const routes: Routes = [
{
path: '',
redirectTo: '/dashboard',
pathMatch: 'full'
},
{
path: 'dashboard',
loadChildren: () => import('./dashboard/dashboard.module').then(m => m.DashboardModule)
},
{
path: 'users',
loadChildren: () => import('./users/users.module').then(m => m.UsersModule),
canLoad: [AuthGuard] // 懒加载守卫
},
{
path: 'admin',
loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule),
canLoad: [AdminGuard]
},
{
path: 'reports',
loadChildren: () => import('./reports/reports.module').then(m => m.ReportsModule)
}
];
// 懒加载模块示例
@NgModule({
declarations: [
UsersComponent,
UserListComponent,
UserDetailComponent
],
imports: [
CommonModule,
UsersRoutingModule,
SharedModule
]
})
export class UsersModule {}
2. 组件懒加载
// 动态组件加载服务
@Injectable({
providedIn: 'root'
})
export class DynamicComponentService {
constructor(
private componentFactoryResolver: ComponentFactoryResolver,
private injector: Injector
) {}
async loadComponent(
componentName: string,
container: ViewContainerRef
): Promise<ComponentRef<any>> {
let component: any;
// 根据组件名动态导入
switch (componentName) {
case 'chart':
const chartModule = await import('./chart/chart.component');
component = chartModule.ChartComponent;
break;
case 'table':
const tableModule = await import('./table/table.component');
component = tableModule.TableComponent;
break;
case 'form':
const formModule = await import('./form/form.component');
component = formModule.FormComponent;
break;
default:
throw new Error(`Unknown component: ${componentName}`);
}
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(component);
return container.createComponent(componentFactory, undefined, this.injector);
}
}
// 使用动态组件的容器
@Component({
selector: 'app-dynamic-container',
template: `
<div class="dynamic-container">
<div class="component-selector">
<button
*ngFor="let comp of availableComponents"
(click)="loadComponent(comp.name)"
[disabled]="loading">
加载 {{ comp.label }}
</button>
</div>
<div class="component-container" #componentContainer></div>
<div *ngIf="loading" class="loading">
正在加载组件...
</div>
</div>
`
})
export class DynamicContainerComponent {
@ViewChild('componentContainer', { read: ViewContainerRef, static: true })
componentContainer!: ViewContainerRef;
availableComponents = [
{ name: 'chart', label: '图表' },
{ name: 'table', label: '表格' },
{ name: 'form', label: '表单' }
];
loading = false;
currentComponent?: ComponentRef<any>;
constructor(private dynamicComponentService: DynamicComponentService) {}
async loadComponent(componentName: string): Promise<void> {
this.loading = true;
try {
// 清除之前的组件
if (this.currentComponent) {
this.currentComponent.destroy();
}
this.componentContainer.clear();
// 加载新组件
this.currentComponent = await this.dynamicComponentService.loadComponent(
componentName,
this.componentContainer
);
// 传递数据给动态组件
if (this.currentComponent.instance.data !== undefined) {
this.currentComponent.instance.data = this.getComponentData(componentName);
}
} catch (error) {
console.error('加载组件失败:', error);
} finally {
this.loading = false;
}
}
private getComponentData(componentName: string): any {
// 根据组件类型返回相应的数据
switch (componentName) {
case 'chart':
return {
type: 'line',
data: [1, 2, 3, 4, 5]
};
case 'table':
return {
columns: ['Name', 'Age', 'Email'],
rows: [
['John', 25, 'john@example.com'],
['Jane', 30, 'jane@example.com']
]
};
case 'form':
return {
fields: [
{ name: 'name', type: 'text', label: '姓名' },
{ name: 'email', type: 'email', label: '邮箱' }
]
};
default:
return {};
}
}
}
(三)虚拟滚动
@Component({
selector: 'app-virtual-scroll',
template: `
<div class="virtual-scroll-container">
<h3>虚拟滚动列表 ({{ items.length }} 项)</h3>
<cdk-virtual-scroll-viewport
itemSize="50"
class="viewport"
[style.height.px]="400">
<div
*cdkVirtualFor="let item of items; trackBy: trackByFn"
class="list-item"
[style.height.px]="50">
<div class="item-content">
<span class="item-id">#{{ item.id }}</span>
<span class="item-name">{{ item.name }}</span>
<span class="item-value">{{ item.value | currency }}</span>
<button (click)="selectItem(item)">选择</button>
</div>
</div>
</cdk-virtual-scroll-viewport>
<div class="actions">
<button (click)="generateItems(1000)">生成1000项</button>
<button (click)="generateItems(10000)">生成10000项</button>
<button (click)="generateItems(100000)">生成100000项</button>
</div>
</div>
`,
styles: [`
.viewport {
border: 1px solid #ccc;
border-radius: 4px;
}
.list-item {
display: flex;
align-items: center;
padding: 0 16px;
border-bottom: 1px solid #eee;
}
.item-content {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
.item-id {
font-weight: bold;
color: #666;
}
`]
})
export class VirtualScrollComponent {
items: VirtualItem[] = [];
constructor() {
this.generateItems(1000);
}
trackByFn(index: number, item: VirtualItem): number {
return item.id;
}
generateItems(count: number): void {
console.time('生成数据');
this.items = Array.from({ length: count }, (_, index) => ({
id: index + 1,
name: `Item ${index + 1}`,
value: Math.random() * 1000,
description: `这是第 ${index + 1} 个项目的描述`
}));
console.timeEnd('生成数据');
}
selectItem(item: VirtualItem): void {
console.log('选择项目:', item);
}
}
interface VirtualItem {
id: number;
name: string;
value: number;
description: string;
}
十一、测试
(一)单元测试
1. 组件测试
// counter.component.spec.ts
describe('CounterComponent', () => {
let component: CounterComponent;
let fixture: ComponentFixture<CounterComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [CounterComponent]
}).compileComponents();
fixture = TestBed.createComponent(CounterComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should initialize with count 0', () => {
expect(component.count).toBe(0);
});
it('should increment count when increment is called', () => {
component.increment();
expect(component.count).toBe(1);
});
it('should decrement count when decrement is called', () => {
component.count = 5;
component.decrement();
expect(component.count).toBe(4);
});
it('should display current count in template', () => {
component.count = 42;
fixture.detectChanges();
const compiled = fixture.nativeElement;
expect(compiled.querySelector('.count').textContent).toContain('42');
});
it('should call increment when increment button is clicked', () => {
spyOn(component, 'increment');
const button = fixture.nativeElement.querySelector('.increment-btn');
button.click();
expect(component.increment).toHaveBeenCalled();
});
it('should emit countChange when count changes', () => {
spyOn(component.countChange, 'emit');
component.increment();
expect(component.countChange.emit).toHaveBeenCalledWith(1);
});
});
// user-list.component.spec.ts
describe('UserListComponent', () => {
let component: UserListComponent;
let fixture: ComponentFixture<UserListComponent>;
let userService: jasmine.SpyObj<UserService>;
beforeEach(async () => {
const spy = jasmine.createSpyObj('UserService', ['getUsers', 'deleteUser']);
await TestBed.configureTestingModule({
declarations: [UserListComponent],
providers: [
{ provide: UserService, useValue: spy }
]
}).compileComponents();
fixture = TestBed.createComponent(UserListComponent);
component = fixture.componentInstance;
userService = TestBed.inject(UserService) as jasmine.SpyObj<UserService>;
});
it('should load users on init', () => {
const mockUsers = [
{ id: 1, name: 'John', email: 'john@example.com' },
{ id: 2, name: 'Jane', email: 'jane@example.com' }
];
userService.getUsers.and.returnValue(of(mockUsers));
component.ngOnInit();
expect(userService.getUsers).toHaveBeenCalled();
expect(component.users).toEqual(mockUsers);
});
it('should handle error when loading users fails', () => {
userService.getUsers.and.returnValue(throwError('Error loading users'));
component.ngOnInit();
expect(component.error).toBe('Error loading users');
expect(component.loading).toBe(false);
});
it('should delete user when deleteUser is called', () => {
const userId = 1;
userService.deleteUser.and.returnValue(of({}));
component.users = [
{ id: 1, name: 'John', email: 'john@example.com' },
{ id: 2, name: 'Jane', email: 'jane@example.com' }
];
component.deleteUser(userId);
expect(userService.deleteUser).toHaveBeenCalledWith(userId);
expect(component.users.length).toBe(1);
expect(component.users[0].id).toBe(2);
});
});
2. 服务测试
// user.service.spec.ts
describe('UserService', () => {
let service: UserService;
let httpMock: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [UserService]
});
service = TestBed.inject(UserService);
httpMock = TestBed.inject(HttpTestingController);
});
afterEach(() => {
httpMock.verify();
});
it('should be created', () => {
expect(service).toBeTruthy();
});
it('should get users', () => {
const mockUsers = [
{ id: 1, name: 'John', email: 'john@example.com' },
{ id: 2, name: 'Jane', email: 'jane@example.com' }
];
service.getUsers().subscribe(users => {
expect(users).toEqual(mockUsers);
});
const req = httpMock.expectOne('/api/users');
expect(req.request.method).toBe('GET');
req.flush(mockUsers);
});
it('should create user', () => {
const newUser = { name: 'Bob', email: 'bob@example.com' };
const createdUser = { id: 3, ...newUser };
service.createUser(newUser).subscribe(user => {
expect(user).toEqual(createdUser);
});
const req = httpMock.expectOne('/api/users');
expect(req.request.method).toBe('POST');
expect(req.request.body).toEqual(newUser);
req.flush(createdUser);
});
it('should handle error when getting users fails', () => {
service.getUsers().subscribe(
() => fail('should have failed'),
error => {
expect(error.status).toBe(500);
}
);
const req = httpMock.expectOne('/api/users');
req.flush('Server error', { status: 500, statusText: 'Internal Server Error' });
});
});
(二)集成测试
// app.component.spec.ts
describe('AppComponent Integration', () => {
let component: AppComponent;
let fixture: ComponentFixture<AppComponent>;
let router: Router;
let location: Location;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [AppComponent, HomeComponent, AboutComponent],
imports: [RouterTestingModule.withRoutes([
{ path: '', component: HomeComponent },
{ path: 'about', component: AboutComponent }
])]
}).compileComponents();
fixture = TestBed.createComponent(AppComponent);
component = fixture.componentInstance;
router = TestBed.inject(Router);
location = TestBed.inject(Location);
fixture.detectChanges();
});
it('should navigate to home', fakeAsync(() => {
router.navigate(['']);
tick();
expect(location.path()).toBe('');
}));
it('should navigate to about', fakeAsync(() => {
router.navigate(['/about']);
tick();
expect(location.path()).toBe('/about');
}));
});
十二、最佳实践
(一)组件设计原则
1. 单一职责原则
// ❌ 不好的例子 - 组件职责过多
@Component({
selector: 'app-bad-user-profile',
template: `
<div class="user-profile">
<!-- 用户信息显示 -->
<div class="user-info">
<img [src]="user.avatar" [alt]="user.name">
<h2>{{ user.name }}</h2>
<p>{{ user.email }}</p>
</div>
<!-- 用户设置表单 -->
<form [formGroup]="settingsForm" (ngSubmit)="saveSettings()">
<input formControlName="theme" placeholder="主题">
<input formControlName="language" placeholder="语言">
<button type="submit">保存设置</button>
</form>
<!-- 用户活动日志 -->
<div class="activity-log">
<h3>活动日志</h3>
<ul>
<li *ngFor="let activity of activities">{{ activity.description }}</li>
</ul>
</div>
<!-- 通知设置 -->
<div class="notifications">
<h3>通知设置</h3>
<label>
<input type="checkbox" [(ngModel)]="emailNotifications">
邮件通知
</label>
</div>
</div>
`
})
export class BadUserProfileComponent {
user: User;
settingsForm: FormGroup;
activities: Activity[];
emailNotifications: boolean;
// 太多的职责混在一个组件中
constructor(
private userService: UserService,
private settingsService: SettingsService,
private activityService: ActivityService,
private notificationService: NotificationService,
private fb: FormBuilder
) {}
// 方法过多,职责不清
ngOnInit() { /* 初始化所有数据 */ }
saveSettings() { /* 保存设置 */ }
loadActivities() { /* 加载活动 */ }
updateNotifications() { /* 更新通知 */ }
}
// ✅ 好的例子 - 职责分离
@Component({
selector: 'app-user-profile',
template: `
<div class="user-profile">
<app-user-info [user]="user"></app-user-info>
<app-user-settings [userId]="user.id"></app-user-settings>
<app-activity-log [userId]="user.id"></app-activity-log>
<app-notification-settings [userId]="user.id"></app-notification-settings>
</div>
`
})
export class UserProfileComponent implements OnInit {
user: User;
constructor(private userService: UserService) {}
ngOnInit(): void {
this.loadUser();
}
private loadUser(): void {
this.userService.getCurrentUser().subscribe(user => {
this.user = user;
});
}
}
// 分离的子组件
@Component({
selector: 'app-user-info',
template: `
<div class="user-info">
<img [src]="user.avatar" [alt]="user.name">
<h2>{{ user.name }}</h2>
<p>{{ user.email }}</p>
</div>
`
})
export class UserInfoComponent {
@Input() user: User;
}
@Component({
selector: 'app-user-settings',
template: `
<form [formGroup]="settingsForm" (ngSubmit)="saveSettings()">
<input formControlName="theme" placeholder="主题">
<input formControlName="language" placeholder="语言">
<button type="submit" [disabled]="settingsForm.invalid">保存设置</button>
</form>
`
})
export class UserSettingsComponent implements OnInit {
@Input() userId: number;
settingsForm: FormGroup;
constructor(
private settingsService: SettingsService,
private fb: FormBuilder
) {}
ngOnInit(): void {
this.buildForm();
this.loadSettings();
}
private buildForm(): void {
this.settingsForm = this.fb.group({
theme: ['', Validators.required],
language: ['', Validators.required]
});
}
private loadSettings(): void {
this.settingsService.getUserSettings(this.userId).subscribe(settings => {
this.settingsForm.patchValue(settings);
});
}
saveSettings(): void {
if (this.settingsForm.valid) {
this.settingsService.updateUserSettings(
this.userId,
this.settingsForm.value
).subscribe();
}
}
}
2. 智能组件与展示组件
// 智能组件(容器组件)
@Component({
selector: 'app-todo-container',
template: `
<div class="todo-container">
<app-todo-form
(todoAdd)="addTodo($event)"
[loading]="adding">
</app-todo-form>
<app-todo-list
[todos]="todos$ | async"
[loading]="loading$ | async"
(todoToggle)="toggleTodo($event)"
(todoDelete)="deleteTodo($event)">
</app-todo-list>
<app-todo-stats
[todos]="todos$ | async">
</app-todo-stats>
</div>
`
})
export class TodoContainerComponent implements OnInit {
todos$: Observable<Todo[]>;
loading$: Observable<boolean>;
adding = false;
constructor(private todoService: TodoService) {}
ngOnInit(): void {
this.todos$ = this.todoService.getTodos();
this.loading$ = this.todoService.loading$;
}
addTodo(text: string): void {
this.adding = true;
this.todoService.addTodo(text).subscribe(() => {
this.adding = false;
});
}
toggleTodo(todo: Todo): void {
this.todoService.toggleTodo(todo.id).subscribe();
}
deleteTodo(todo: Todo): void {
this.todoService.deleteTodo(todo.id).subscribe();
}
}
// 展示组件
@Component({
selector: 'app-todo-list',
template: `
<div class="todo-list">
<div *ngIf="loading" class="loading">加载中...</div>
<div *ngIf="!loading && todos?.length === 0" class="empty">
暂无待办事项
</div>
<div
*ngFor="let todo of todos; trackBy: trackByFn"
class="todo-item"
[class.completed]="todo.completed">
<input
type="checkbox"
[checked]="todo.completed"
(change)="onToggle(todo)">
<span class="todo-text">{{ todo.text }}</span>
<button
class="delete-btn"
(click)="onDelete(todo)"
aria-label="删除">
×
</button>
</div>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TodoListComponent {
@Input() todos: Todo[] | null = null;
@Input() loading = false;
@Output() todoToggle = new EventEmitter<Todo>();
@Output() todoDelete = new EventEmitter<Todo>();
trackByFn(index: number, todo: Todo): number {
return todo.id;
}
onToggle(todo: Todo): void {
this.todoToggle.emit(todo);
}
onDelete(todo: Todo): void {
this.todoDelete.emit(todo);
}
}
(二)性能优化建议
1. 避免在模板中使用函数
// ❌ 不好的例子
@Component({
template: `
<div *ngFor="let item of items">
<!-- 每次变更检测都会调用函数 -->
<span>{{ formatPrice(item.price) }}</span>
<span>{{ isExpensive(item.price) ? '昂贵' : '便宜' }}</span>
</div>
`
})
export class BadPriceListComponent {
items = [];
formatPrice(price: number): string {
console.log('formatPrice called'); // 会频繁调用
return `$${price.toFixed(2)}`;
}
isExpensive(price: number): boolean {
console.log('isExpensive called'); // 会频繁调用
return price > 100;
}
}
// ✅ 好的例子
@Component({
template: `
<div *ngFor="let item of processedItems">
<span>{{ item.formattedPrice }}</span>
<span>{{ item.isExpensive ? '昂贵' : '便宜' }}</span>
</div>
`
})
export class GoodPriceListComponent implements OnInit {
items = [];
processedItems = [];
ngOnInit(): void {
this.processItems();
}
private processItems(): void {
this.processedItems = this.items.map(item => ({
...item,
formattedPrice: `$${item.price.toFixed(2)}`,
isExpensive: item.price > 100
}));
}
}
2. 合理使用管道
// 自定义管道
@Pipe({
name: 'currency',
pure: true // 纯管道,只有输入变化时才重新计算
})
export class CurrencyPipe implements PipeTransform {
transform(value: number, currency: string = 'USD'): string {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: currency
}).format(value);
}
}
// 使用管道
@Component({
template: `
<div *ngFor="let item of items">
<span>{{ item.price | currency:'USD' }}</span>
</div>
`
})
export class PriceListComponent {
items = [];
}
十三、总结
Angular是一个功能强大的企业级前端开发平台,具有以下核心特点:
核心优势
- TypeScript优先:提供强类型支持,提高代码质量和开发效率
- 组件化架构:模块化开发,代码复用性强
- 依赖注入:松耦合设计,便于测试和维护
- 强大的CLI工具:自动化开发流程
- 完整的生态系统:路由、表单、HTTP客户端等一应俱全
重要概念
- 组件:Angular应用的基本构建块
- 服务:提供业务逻辑和数据访问
- 模块:组织和配置应用的功能单元
- 依赖注入:管理组件和服务之间的依赖关系
- 生命周期钩子:在组件生命周期的特定时刻执行代码
- 响应式编程:使用RxJS处理异步数据流
最佳实践
- 遵循单一职责原则,保持组件和服务的职责清晰
- 使用OnPush变更检测策略优化性能
- 合理使用懒加载减少初始包大小
- 编写全面的测试确保代码质量
- 使用TypeScript的类型系统提高代码安全性
- 遵循Angular风格指南保持代码一致性
学习建议
- 掌握TypeScript基础
- 理解组件化开发思想
- 熟练使用Angular CLI
- 学习RxJS响应式编程
- 实践测试驱动开发
- 关注性能优化技巧
参考资料
(二)层次化注入器
1. 注入器层次结构
// 根级服务(全局单例)
@Injectable({
providedIn: 'root'
})
export class GlobalConfigService {
private config = {
appName: 'My Angular App',
version: '1.0.0',
apiUrl: environment.apiUrl
};
getConfig() {
return this.config;
}
updateConfig(newConfig: Partial<typeof this.config>) {
this.config = { ...this.config, ...newConfig };
}
}
// 模块级服务
@Injectable()
export class FeatureService {
constructor(private globalConfig: GlobalConfigService) {
console.log('FeatureService 创建,应用名称:', globalConfig.getConfig().appName);
}
getFeatureData() {
return 'Feature specific data';
}
}
// 组件级服务
@Injectable()
export class ComponentScopedService {
private instanceId = Math.random().toString(36).substr(2, 9);
constructor() {
console.log('ComponentScopedService 实例创建:', this.instanceId);
}
getInstanceId() {
return this.instanceId;
}
}
// 特性模块
@NgModule({
declarations: [FeatureComponent],
providers: [
FeatureService // 模块级别提供
]
})
export class FeatureModule {}
// 父组件
@Component({
selector: 'app-parent',
template: `
<div class="parent">
<h3>父组件</h3>
<p>服务实例ID: {{ serviceInstanceId }}</p>
<button (click)="createChild()">创建子组件</button>
<app-child *ngFor="let child of children; trackBy: trackByIndex"></app-child>
</div>
`,
providers: [
ComponentScopedService // 组件级别提供
]
})
export class ParentComponent {
children: number[] = [];
serviceInstanceId: string;
constructor(private componentService: ComponentScopedService) {
this.serviceInstanceId = componentService.getInstanceId();
}
createChild() {
this.children.push(this.children.length);
}
trackByIndex(index: number): number {
return index;
}
}
// 子组件(继承父组件的服务实例)
@Component({
selector: 'app-child',
template: `
<div class="child">
<h4>子组件</h4>
<p>继承的服务实例ID: {{ serviceInstanceId }}</p>
<p>自己的服务实例ID: {{ ownServiceInstanceId }}</p>
</div>
`,
providers: [
// 如果提供了自己的服务实例,会覆盖父组件的
// ComponentScopedService
]
})
export class ChildComponent {
serviceInstanceId: string;
ownServiceInstanceId: string;
constructor(
private componentService: ComponentScopedService,
@Optional() @SkipSelf() private parentService?: ComponentScopedService
) {
this.serviceInstanceId = componentService.getInstanceId();
this.ownServiceInstanceId = parentService?.getInstanceId() || 'N/A';
}
}
2. 注入修饰符
@Component({
selector: 'app-injection-modifiers',
template: `
<div>
<h3>注入修饰符演示</h3>
<p>可选服务状态: {{ optionalServiceStatus }}</p>
<p>自身服务状态: {{ selfServiceStatus }}</p>
<p>跳过自身服务状态: {{ skipSelfServiceStatus }}</p>
</div>
`
})
export class InjectionModifiersComponent {
optionalServiceStatus: string;
selfServiceStatus: string;
skipSelfServiceStatus: string;
constructor(
// @Optional() - 如果服务不存在,注入null而不是抛出错误
@Optional() private optionalService: OptionalService | null,
// @Self() - 只从当前注入器查找服务
@Self() @Optional() private selfService: ComponentScopedService | null,
// @SkipSelf() - 跳过当前注入器,从父注入器查找
@SkipSelf() @Optional() private skipSelfService: ComponentScopedService | null,
// @Host() - 只在宿主组件的注入器中查找
@Host() @Optional() private hostService: ComponentScopedService | null
) {
this.optionalServiceStatus = optionalService ? '存在' : '不存在';
this.selfServiceStatus = selfService ? '存在' : '不存在';
this.skipSelfServiceStatus = skipSelfService ? '存在' : '不存在';
}
}
六、服务与HTTP客户端
(一)HTTP客户端基础
1. 基本HTTP操作
// 用户数据接口
export interface User {
id: number;
name: string;
email: string;
avatar?: string;
createdAt: string;
updatedAt: string;
}
export interface CreateUserRequest {
name: string;
email: string;
avatar?: string;
}
export interface UpdateUserRequest {
name?: string;
email?: string;
avatar?: string;
}
// 用户服务
@Injectable({
providedIn: 'root'
})
export class UserService {
private apiUrl = `${environment.apiUrl}/users`;
constructor(
private http: HttpClient,
private logger: LoggerService
) {}
// GET 请求 - 获取所有用户
getUsers(): Observable<User[]> {
return this.http.get<User[]>(this.apiUrl).pipe(
tap(users => this.logger.log(`获取到 ${users.length} 个用户`)),
catchError(this.handleError<User[]>('getUsers', []))
);
}
// GET 请求 - 根据ID获取用户
getUserById(id: number): Observable<User> {
const url = `${this.apiUrl}/${id}`;
return this.http.get<User>(url).pipe(
tap(user => this.logger.log(`获取用户: ${user.name}`)),
catchError(this.handleError<User>(`getUserById id=${id}`))
);
}
// POST 请求 - 创建用户
createUser(user: CreateUserRequest): Observable<User> {
return this.http.post<User>(this.apiUrl, user, {
headers: new HttpHeaders({
'Content-Type': 'application/json'
})
}).pipe(
tap(newUser => this.logger.log(`创建用户: ${newUser.name}`)),
catchError(this.handleError<User>('createUser'))
);
}
// PUT 请求 - 更新用户
updateUser(id: number, user: UpdateUserRequest): Observable<User> {
const url = `${this.apiUrl}/${id}`;
return this.http.put<User>(url, user, {
headers: new HttpHeaders({
'Content-Type': 'application/json'
})
}).pipe(
tap(updatedUser => this.logger.log(`更新用户: ${updatedUser.name}`)),
catchError(this.handleError<User>('updateUser'))
);
}
// DELETE 请求 - 删除用户
deleteUser(id: number): Observable<void> {
const url = `${this.apiUrl}/${id}`;
return this.http.delete<void>(url).pipe(
tap(() => this.logger.log(`删除用户 ID: ${id}`)),
catchError(this.handleError<void>('deleteUser'))
);
}
// 搜索用户
searchUsers(query: string, page = 1, limit = 10): Observable<{users: User[], total: number}> {
const params = new HttpParams()
.set('q', query)
.set('page', page.toString())
.set('limit', limit.toString());
return this.http.get<{users: User[], total: number}>(`${this.apiUrl}/search`, { params }).pipe(
tap(result => this.logger.log(`搜索到 ${result.users.length} 个用户`)),
catchError(this.handleError<{users: User[], total: number}>('searchUsers', {users: [], total: 0}))
);
}
// 上传用户头像
uploadAvatar(userId: number, file: File): Observable<{avatarUrl: string}> {
const formData = new FormData();
formData.append('avatar', file);
return this.http.post<{avatarUrl: string}>(`${this.apiUrl}/${userId}/avatar`, formData, {
reportProgress: true,
observe: 'events'
}).pipe(
filter(event => event.type === HttpEventType.Response),
map(event => (event as HttpResponse<{avatarUrl: string}>).body!),
catchError(this.handleError<{avatarUrl: string}>('uploadAvatar'))
);
}
// 错误处理
private handleError<T>(operation = 'operation', result?: T) {
return (error: any): Observable<T> => {
this.logger.error(`${operation} 失败: ${error.message}`);
// 可以在这里添加用户友好的错误处理
// 比如显示通知、重定向等
return of(result as T);
};
}
}
2. HTTP拦截器
// 认证拦截器
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
constructor(private authService: AuthService) {}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
// 获取认证令牌
const authToken = this.authService.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 {
constructor(private logger: LoggerService) {}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const startTime = Date.now();
this.logger.log(`HTTP请求开始: ${req.method} ${req.url}`);
return next.handle(req).pipe(
tap(
event => {
if (event.type === HttpEventType.Response) {
const duration = Date.now() - startTime;
this.logger.log(`HTTP请求完成: ${req.method} ${req.url} - ${event.status} (${duration}ms)`);
}
},
error => {
const duration = Date.now() - startTime;
this.logger.error(`HTTP请求失败: ${req.method} ${req.url} - ${error.status} (${duration}ms)`);
}
)
);
}
}
// 错误处理拦截器
@Injectable()
export class ErrorInterceptor implements HttpInterceptor {
constructor(
private notificationService: NotificationService,
private router: Router
) {}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return next.handle(req).pipe(
catchError((error: HttpErrorResponse) => {
let errorMessage = '发生未知错误';
if (error.error instanceof ErrorEvent) {
// 客户端错误
errorMessage = `客户端错误: ${error.error.message}`;
} else {
// 服务器错误
switch (error.status) {
case 400:
errorMessage = '请求参数错误';
break;
case 401:
errorMessage = '未授权,请重新登录';
this.router.navigate(['/login']);
break;
case 403:
errorMessage = '禁止访问';
break;
case 404:
errorMessage = '请求的资源不存在';
break;
case 500:
errorMessage = '服务器内部错误';
break;
default:
errorMessage = `服务器错误: ${error.status}`;
}
}
this.notificationService.showError(errorMessage);
return throwError(() => error);
})
);
}
}
// 缓存拦截器
@Injectable()
export class CacheInterceptor implements HttpInterceptor {
private cache = new Map<string, HttpResponse<any>>();
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
// 只缓存GET请求
if (req.method !== 'GET') {
return next.handle(req);
}
// 检查缓存
const cachedResponse = this.cache.get(req.url);
if (cachedResponse) {
return of(cachedResponse);
}
// 发送请求并缓存响应
return next.handle(req).pipe(
tap(event => {
if (event instanceof HttpResponse) {
this.cache.set(req.url, event);
}
})
);
}
}
// 在模块中注册拦截器
@NgModule({
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: AuthInterceptor,
multi: true
},
{
provide: HTTP_INTERCEPTORS,
useClass: LoggingInterceptor,
multi: true
},
{
provide: HTTP_INTERCEPTORS,
useClass: ErrorInterceptor,
multi: true
},
{
provide: HTTP_INTERCEPTORS,
useClass: CacheInterceptor,
multi: true
}
]
})
export class CoreModule {}