angular-development
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseAngular Development Skill
Angular开发技能指南
When to Use This Skill
何时使用本技能
Use this skill when working with Angular applications, including:
- Building modern Angular applications with standalone components
- Creating reactive UIs with Angular's component system
- Implementing dependency injection patterns
- Setting up routing with lazy loading and guards
- Building reactive forms with validation
- Managing state with Signals and RxJS
- Creating custom directives and pipes
- Implementing HTTP client integrations
- Migrating from older Angular patterns to modern approaches
- Optimizing Angular applications for performance
- Setting up Angular projects with best practices
在开发Angular应用时使用本技能,包括:
- 使用独立组件构建现代化Angular应用
- 利用Angular组件系统创建响应式UI
- 实现依赖注入模式
- 设置带有懒加载和守卫的路由
- 构建带验证的响应式表单
- 使用Signals和RxJS管理状态
- 创建自定义指令和管道
- 实现HTTP客户端集成
- 从旧版Angular模式迁移到现代化方案
- 优化Angular应用性能
- 按照最佳实践搭建Angular项目
Core Concepts
核心概念
Components
Components
Components are the fundamental building blocks of Angular applications. They control a portion of the screen called a view.
Modern Standalone Component Pattern:
typescript
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-user-profile',
standalone: true,
imports: [CommonModule],
template: `
<div class="profile">
<h2>{{ user.name }}</h2>
<p>{{ user.email }}</p>
<button (click)="updateProfile()">Update</button>
</div>
`,
styles: [`
.profile {
padding: 20px;
border: 1px solid #ccc;
border-radius: 8px;
}
`]
})
export class UserProfileComponent {
user = {
name: 'John Doe',
email: 'john@example.com'
};
updateProfile() {
console.log('Updating profile...');
}
}Component Lifecycle Hooks:
typescript
import { Component, OnInit, OnDestroy, AfterViewInit } from '@angular/core';
import { Subscription } from 'rxjs';
@Component({
selector: 'app-lifecycle-demo',
standalone: true,
template: `<div>{{ message }}</div>`
})
export class LifecycleDemoComponent implements OnInit, OnDestroy, AfterViewInit {
message = '';
private subscription?: Subscription;
ngOnInit() {
// Called once after component initialization
console.log('Component initialized');
this.message = 'Component ready';
}
ngAfterViewInit() {
// Called after view initialization
console.log('View initialized');
}
ngOnDestroy() {
// Called before component destruction
console.log('Component destroyed');
this.subscription?.unsubscribe();
}
}组件是Angular应用的基本构建块,它们控制屏幕上称为视图的一部分区域。
现代化独立组件模式:
typescript
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-user-profile',
standalone: true,
imports: [CommonModule],
template: `
<div class="profile">
<h2>{{ user.name }}</h2>
<p>{{ user.email }}</p>
<button (click)="updateProfile()">Update</button>
</div>
`,
styles: [`
.profile {
padding: 20px;
border: 1px solid #ccc;
border-radius: 8px;
}
`]
})
export class UserProfileComponent {
user = {
name: 'John Doe',
email: 'john@example.com'
};
updateProfile() {
console.log('Updating profile...');
}
}组件生命周期钩子:
typescript
import { Component, OnInit, OnDestroy, AfterViewInit } from '@angular/core';
import { Subscription } from 'rxjs';
@Component({
selector: 'app-lifecycle-demo',
standalone: true,
template: `<div>{{ message }}</div>`
})
export class LifecycleDemoComponent implements OnInit, OnDestroy, AfterViewInit {
message = '';
private subscription?: Subscription;
ngOnInit() {
// 组件初始化后调用一次
console.log('Component initialized');
this.message = 'Component ready';
}
ngAfterViewInit() {
// 视图初始化后调用
console.log('View initialized');
}
ngOnDestroy() {
// 组件销毁前调用
console.log('Component destroyed');
this.subscription?.unsubscribe();
}
}Services and Dependency Injection
Services and Dependency Injection
Services provide shared functionality across components. Angular's dependency injection system makes services available throughout your application.
Modern Injectable Service with inject() Function:
typescript
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
export interface User {
id: number;
name: string;
email: string;
}
@Injectable({
providedIn: 'root' // Singleton service available app-wide
})
export class UserService {
// Modern inject() function instead of constructor injection
private http = inject(HttpClient);
private apiUrl = 'https://api.example.com/users';
getUsers(): Observable<User[]> {
return this.http.get<User[]>(this.apiUrl);
}
getUserById(id: number): Observable<User> {
return this.http.get<User>(`${this.apiUrl}/${id}`);
}
createUser(user: Omit<User, 'id'>): Observable<User> {
return this.http.post<User>(this.apiUrl, user);
}
updateUser(id: number, user: Partial<User>): Observable<User> {
return this.http.patch<User>(`${this.apiUrl}/${id}`, user);
}
deleteUser(id: number): Observable<void> {
return this.http.delete<void>(`${this.apiUrl}/${id}`);
}
}Using Services in Components:
typescript
import { Component, OnInit, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { UserService, User } from './user.service';
@Component({
selector: 'app-user-list',
standalone: true,
imports: [CommonModule],
template: `
<div class="user-list">
<h2>Users</h2>
@if (loading) {
<p>Loading...</p>
} @else if (error) {
<p class="error">{{ error }}</p>
} @else {
<ul>
@for (user of users; track user.id) {
<li>{{ user.name }} - {{ user.email }}</li>
}
</ul>
}
</div>
`
})
export class UserListComponent implements OnInit {
private userService = inject(UserService);
users: User[] = [];
loading = false;
error = '';
ngOnInit() {
this.loadUsers();
}
loadUsers() {
this.loading = true;
this.userService.getUsers().subscribe({
next: (users) => {
this.users = users;
this.loading = false;
},
error: (err) => {
this.error = 'Failed to load users';
this.loading = false;
console.error(err);
}
});
}
}服务为多个组件提供共享功能,Angular的依赖注入系统可让服务在整个应用中可用。
使用inject()函数的现代化可注入服务:
typescript
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
export interface User {
id: number;
name: string;
email: string;
}
@Injectable({
providedIn: 'root' // 全应用可用的单例服务
})
export class UserService {
// 使用现代化inject()函数替代构造函数注入
private http = inject(HttpClient);
private apiUrl = 'https://api.example.com/users';
getUsers(): Observable<User[]> {
return this.http.get<User[]>(this.apiUrl);
}
getUserById(id: number): Observable<User> {
return this.http.get<User>(`${this.apiUrl}/${id}`);
}
createUser(user: Omit<User, 'id'>): Observable<User> {
return this.http.post<User>(this.apiUrl, user);
}
updateUser(id: number, user: Partial<User>): Observable<User> {
return this.http.patch<User>(`${this.apiUrl}/${id}`, user);
}
deleteUser(id: number): Observable<void> {
return this.http.delete<void>(`${this.apiUrl}/${id}`);
}
}在组件中使用服务:
typescript
import { Component, OnInit, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { UserService, User } from './user.service';
@Component({
selector: 'app-user-list',
standalone: true,
imports: [CommonModule],
template: `
<div class="user-list">
<h2>Users</h2>
@if (loading) {
<p>Loading...</p>
} @else if (error) {
<p class="error">{{ error }}</p>
} @else {
<ul>
@for (user of users; track user.id) {
<li>{{ user.name }} - {{ user.email }}</li>
}
</ul>
}
</div>
`
})
export class UserListComponent implements OnInit {
private userService = inject(UserService);
users: User[] = [];
loading = false;
error = '';
ngOnInit() {
this.loadUsers();
}
loadUsers() {
this.loading = true;
this.userService.getUsers().subscribe({
next: (users) => {
this.users = users;
this.loading = false;
},
error: (err) => {
this.error = 'Failed to load users';
this.loading = false;
console.error(err);
}
});
}
}Signals - Modern Reactive State Management
Signals - 现代化响应式状态管理
Signals provide a new way to manage reactive state in Angular with fine-grained reactivity.
typescript
import { Component, signal, computed, effect } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-counter',
standalone: true,
imports: [CommonModule],
template: `
<div class="counter">
<h2>Counter: {{ count() }}</h2>
<p>Double: {{ doubleCount() }}</p>
<p>Status: {{ status() }}</p>
<button (click)="increment()">Increment</button>
<button (click)="decrement()">Decrement</button>
<button (click)="reset()">Reset</button>
</div>
`
})
export class CounterComponent {
// Writable signal
count = signal(0);
// Computed signal - automatically updates when count changes
doubleCount = computed(() => this.count() * 2);
status = computed(() => {
const value = this.count();
if (value < 0) return 'Negative';
if (value === 0) return 'Zero';
return 'Positive';
});
constructor() {
// Effect runs whenever signals it reads change
effect(() => {
console.log(`Count changed to: ${this.count()}`);
});
}
increment() {
this.count.update(value => value + 1);
}
decrement() {
this.count.update(value => value - 1);
}
reset() {
this.count.set(0);
}
}Advanced Signals Pattern - Shopping Cart:
typescript
import { Injectable, signal, computed } from '@angular/core';
export interface CartItem {
id: number;
name: string;
price: number;
quantity: number;
}
@Injectable({
providedIn: 'root'
})
export class CartService {
private items = signal<CartItem[]>([]);
// Computed values
totalItems = computed(() =>
this.items().reduce((sum, item) => sum + item.quantity, 0)
);
totalPrice = computed(() =>
this.items().reduce((sum, item) => sum + (item.price * item.quantity), 0)
);
// Read-only access to items
getItems = this.items.asReadonly();
addItem(item: Omit<CartItem, 'quantity'>) {
this.items.update(currentItems => {
const existing = currentItems.find(i => i.id === item.id);
if (existing) {
return currentItems.map(i =>
i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i
);
}
return [...currentItems, { ...item, quantity: 1 }];
});
}
removeItem(id: number) {
this.items.update(currentItems =>
currentItems.filter(item => item.id !== id)
);
}
updateQuantity(id: number, quantity: number) {
if (quantity <= 0) {
this.removeItem(id);
return;
}
this.items.update(currentItems =>
currentItems.map(item =>
item.id === id ? { ...item, quantity } : item
)
);
}
clear() {
this.items.set([]);
}
}Signals为Angular提供了一种新的响应式状态管理方式,支持细粒度响应性。
typescript
import { Component, signal, computed, effect } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-counter',
standalone: true,
imports: [CommonModule],
template: `
<div class="counter">
<h2>Counter: {{ count() }}</h2>
<p>Double: {{ doubleCount() }}</p>
<p>Status: {{ status() }}</p>
<button (click)="increment()">Increment</button>
<button (click)="decrement()">Decrement</button>
<button (click)="reset()">Reset</button>
</div>
`
})
export class CounterComponent {
// 可写信号
count = signal(0);
// 计算信号 - 当count变化时自动更新
doubleCount = computed(() => this.count() * 2);
status = computed(() => {
const value = this.count();
if (value < 0) return 'Negative';
if (value === 0) return 'Zero';
return 'Positive';
});
constructor() {
// 当读取的信号变化时,effect会运行
effect(() => {
console.log(`Count changed to: ${this.count()}`);
});
}
increment() {
this.count.update(value => value + 1);
}
decrement() {
this.count.update(value => value - 1);
}
reset() {
this.count.set(0);
}
}高级Signals模式 - 购物车:
typescript
import { Injectable, signal, computed } from '@angular/core';
export interface CartItem {
id: number;
name: string;
price: number;
quantity: number;
}
@Injectable({
providedIn: 'root'
})
export class CartService {
private items = signal<CartItem[]>([]);
// 计算值
totalItems = computed(() =>
this.items().reduce((sum, item) => sum + item.quantity, 0)
);
totalPrice = computed(() =>
this.items().reduce((sum, item) => sum + (item.price * item.quantity), 0)
);
// 对items的只读访问
getItems = this.items.asReadonly();
addItem(item: Omit<CartItem, 'quantity'>) {
this.items.update(currentItems => {
const existing = currentItems.find(i => i.id === item.id);
if (existing) {
return currentItems.map(i =>
i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i
);
}
return [...currentItems, { ...item, quantity: 1 }];
});
}
removeItem(id: number) {
this.items.update(currentItems =>
currentItems.filter(item => item.id !== id)
);
}
updateQuantity(id: number, quantity: number) {
if (quantity <= 0) {
this.removeItem(id);
return;
}
this.items.update(currentItems =>
currentItems.map(item =>
item.id === id ? { ...item, quantity } : item
)
);
}
clear() {
this.items.set([]);
}
}Routing
Routing
Angular's router enables navigation between views and lazy loading of feature modules.
Modern Route Configuration with Lazy Loading:
typescript
import { Routes } from '@angular/router';
export const routes: Routes = [
{
path: '',
redirectTo: '/home',
pathMatch: 'full'
},
{
path: 'home',
loadComponent: () => import('./home/home.component').then(m => m.HomeComponent)
},
{
path: 'users',
loadComponent: () => import('./users/user-list.component').then(m => m.UserListComponent)
},
{
path: 'users/:id',
loadComponent: () => import('./users/user-detail.component').then(m => m.UserDetailComponent)
},
{
path: 'admin',
loadComponent: () => import('./admin/admin.component').then(m => m.AdminComponent),
canActivate: [(route, state) => inject(AuthGuard).canActivate(route, state)]
},
{
path: '**',
loadComponent: () => import('./not-found/not-found.component').then(m => m.NotFoundComponent)
}
];Route Guards with inject() Function:
typescript
import { Injectable, inject } from '@angular/core';
import { Router, CanActivateFn } from '@angular/router';
import { AuthService } from './auth.service';
// Functional guard (modern approach)
export const authGuard: CanActivateFn = (route, state) => {
const authService = inject(AuthService);
const router = inject(Router);
if (authService.isAuthenticated()) {
return true;
}
// Redirect to login
return router.createUrlTree(['/login'], {
queryParams: { returnUrl: state.url }
});
};
// Class-based guard (traditional approach)
@Injectable({
providedIn: 'root'
})
export class AuthGuard {
private authService = inject(AuthService);
private router = inject(Router);
canActivate(route: any, state: any): boolean {
if (this.authService.isAuthenticated()) {
return true;
}
this.router.navigate(['/login'], {
queryParams: { returnUrl: state.url }
});
return false;
}
}Router with Route Parameters:
typescript
import { Component, OnInit, inject } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { switchMap } from 'rxjs/operators';
import { UserService, User } from '../services/user.service';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-user-detail',
standalone: true,
imports: [CommonModule],
template: `
<div class="user-detail">
@if (user) {
<h2>{{ user.name }}</h2>
<p>Email: {{ user.email }}</p>
<button (click)="goBack()">Back</button>
<button (click)="editUser()">Edit</button>
} @else {
<p>Loading user...</p>
}
</div>
`
})
export class UserDetailComponent implements OnInit {
private route = inject(ActivatedRoute);
private router = inject(Router);
private userService = inject(UserService);
user?: User;
ngOnInit() {
this.route.paramMap.pipe(
switchMap(params => {
const id = Number(params.get('id'));
return this.userService.getUserById(id);
})
).subscribe(user => {
this.user = user;
});
}
goBack() {
this.router.navigate(['/users']);
}
editUser() {
this.router.navigate(['/users', this.user?.id, 'edit']);
}
}Angular路由器支持视图间导航以及功能模块的懒加载。
带懒加载的现代化路由配置:
typescript
import { Routes } from '@angular/router';
export const routes: Routes = [
{
path: '',
redirectTo: '/home',
pathMatch: 'full'
},
{
path: 'home',
loadComponent: () => import('./home/home.component').then(m => m.HomeComponent)
},
{
path: 'users',
loadComponent: () => import('./users/user-list.component').then(m => m.UserListComponent)
},
{
path: 'users/:id',
loadComponent: () => import('./users/user-detail.component').then(m => m.UserDetailComponent)
},
{
path: 'admin',
loadComponent: () => import('./admin/admin.component').then(m => m.AdminComponent),
canActivate: [(route, state) => inject(AuthGuard).canActivate(route, state)]
},
{
path: '**',
loadComponent: () => import('./not-found/not-found.component').then(m => m.NotFoundComponent)
}
];使用inject()函数的路由守卫:
typescript
import { Injectable, inject } from '@angular/core';
import { Router, CanActivateFn } from '@angular/router';
import { AuthService } from './auth.service';
// 函数式守卫(现代化方案)
export const authGuard: CanActivateFn = (route, state) => {
const authService = inject(AuthService);
const router = inject(Router);
if (authService.isAuthenticated()) {
return true;
}
// 重定向到登录页
return router.createUrlTree(['/login'], {
queryParams: { returnUrl: state.url }
});
};
// 类式守卫(传统方案)
@Injectable({
providedIn: 'root'
})
export class AuthGuard {
private authService = inject(AuthService);
private router = inject(Router);
canActivate(route: any, state: any): boolean {
if (this.authService.isAuthenticated()) {
return true;
}
this.router.navigate(['/login'], {
queryParams: { returnUrl: state.url }
});
return false;
}
}带路由参数的路由器:
typescript
import { Component, OnInit, inject } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { switchMap } from 'rxjs/operators';
import { UserService, User } from '../services/user.service';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-user-detail',
standalone: true,
imports: [CommonModule],
template: `
<div class="user-detail">
@if (user) {
<h2>{{ user.name }}</h2>
<p>Email: {{ user.email }}</p>
<button (click)="goBack()">Back</button>
<button (click)="editUser()">Edit</button>
} @else {
<p>Loading user...</p>
}
</div>
`
})
export class UserDetailComponent implements OnInit {
private route = inject(ActivatedRoute);
private router = inject(Router);
private userService = inject(UserService);
user?: User;
ngOnInit() {
this.route.paramMap.pipe(
switchMap(params => {
const id = Number(params.get('id'));
return this.userService.getUserById(id);
})
).subscribe(user => {
this.user = user;
});
}
goBack() {
this.router.navigate(['/users']);
}
editUser() {
this.router.navigate(['/users', this.user?.id, 'edit']);
}
}Reactive Forms
Reactive Forms
Reactive forms provide a model-driven approach to handling form inputs with built-in validation.
Form with Validation:
typescript
import { Component, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ReactiveFormsModule, FormBuilder, Validators } from '@angular/forms';
@Component({
selector: 'app-user-form',
standalone: true,
imports: [CommonModule, ReactiveFormsModule],
template: `
<form [formGroup]="userForm" (ngSubmit)="onSubmit()" class="user-form">
<div class="form-group">
<label for="name">Name:</label>
<input
id="name"
type="text"
formControlName="name"
[class.error]="name.invalid && name.touched"
>
@if (name.invalid && name.touched) {
<div class="error-message">
@if (name.errors?.['required']) {
<span>Name is required</span>
}
@if (name.errors?.['minlength']) {
<span>Name must be at least 3 characters</span>
}
</div>
}
</div>
<div class="form-group">
<label for="email">Email:</label>
<input
id="email"
type="email"
formControlName="email"
[class.error]="email.invalid && email.touched"
>
@if (email.invalid && email.touched) {
<div class="error-message">
@if (email.errors?.['required']) {
<span>Email is required</span>
}
@if (email.errors?.['email']) {
<span>Invalid email format</span>
}
</div>
}
</div>
<div class="form-group">
<label for="age">Age:</label>
<input
id="age"
type="number"
formControlName="age"
[class.error]="age.invalid && age.touched"
>
@if (age.invalid && age.touched) {
<div class="error-message">
@if (age.errors?.['min']) {
<span>Age must be at least 18</span>
}
@if (age.errors?.['max']) {
<span>Age must be less than 100</span>
}
</div>
}
</div>
<button type="submit" [disabled]="userForm.invalid">Submit</button>
<button type="button" (click)="resetForm()">Reset</button>
</form>
`
})
export class UserFormComponent {
private fb = inject(FormBuilder);
userForm = this.fb.group({
name: ['', [Validators.required, Validators.minLength(3)]],
email: ['', [Validators.required, Validators.email]],
age: [null, [Validators.min(18), Validators.max(100)]]
});
// Convenience getters
get name() { return this.userForm.get('name')!; }
get email() { return this.userForm.get('email')!; }
get age() { return this.userForm.get('age')!; }
onSubmit() {
if (this.userForm.valid) {
console.log('Form submitted:', this.userForm.value);
// Handle form submission
}
}
resetForm() {
this.userForm.reset();
}
}Custom Validators:
typescript
import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';
export class CustomValidators {
static passwordMatch(passwordField: string, confirmField: string): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
const password = control.get(passwordField);
const confirm = control.get(confirmField);
if (!password || !confirm) {
return null;
}
return password.value === confirm.value ? null : { passwordMismatch: true };
};
}
static noWhitespace(): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
const value = control.value as string;
if (!value) return null;
const hasWhitespace = value.trim().length === 0;
return hasWhitespace ? { whitespace: true } : null;
};
}
static strongPassword(): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
const value = control.value as string;
if (!value) return null;
const hasNumber = /\d/.test(value);
const hasUpper = /[A-Z]/.test(value);
const hasLower = /[a-z]/.test(value);
const hasSpecial = /[!@#$%^&*(),.?":{}|<>]/.test(value);
const isLongEnough = value.length >= 8;
const valid = hasNumber && hasUpper && hasLower && hasSpecial && isLongEnough;
return valid ? null : {
weakPassword: {
hasNumber,
hasUpper,
hasLower,
hasSpecial,
isLongEnough
}
};
};
}
}响应式表单提供了一种模型驱动的方式来处理表单输入,并内置验证功能。
带验证的表单:
typescript
import { Component, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ReactiveFormsModule, FormBuilder, Validators } from '@angular/forms';
@Component({
selector: 'app-user-form',
standalone: true,
imports: [CommonModule, ReactiveFormsModule],
template: `
<form [formGroup]="userForm" (ngSubmit)="onSubmit()" class="user-form">
<div class="form-group">
<label for="name">Name:</label>
<input
id="name"
type="text"
formControlName="name"
[class.error]="name.invalid && name.touched"
>
@if (name.invalid && name.touched) {
<div class="error-message">
@if (name.errors?.['required']) {
<span>Name is required</span>
}
@if (name.errors?.['minlength']) {
<span>Name must be at least 3 characters</span>
}
</div>
}
</div>
<div class="form-group">
<label for="email">Email:</label>
<input
id="email"
type="email"
formControlName="email"
[class.error]="email.invalid && email.touched"
>
@if (email.invalid && email.touched) {
<div class="error-message">
@if (email.errors?.['required']) {
<span>Email is required</span>
}
@if (email.errors?.['email']) {
<span>Invalid email format</span>
}
</div>
}
</div>
<div class="form-group">
<label for="age">Age:</label>
<input
id="age"
type="number"
formControlName="age"
[class.error]="age.invalid && age.touched"
>
@if (age.invalid && age.touched) {
<div class="error-message">
@if (age.errors?.['min']) {
<span>Age must be at least 18</span>
}
@if (age.errors?.['max']) {
<span>Age must be less than 100</span>
}
</div>
}
</div>
<button type="submit" [disabled]="userForm.invalid">Submit</button>
<button type="button" (click)="resetForm()">Reset</button>
</form>
`
})
export class UserFormComponent {
private fb = inject(FormBuilder);
userForm = this.fb.group({
name: ['', [Validators.required, Validators.minLength(3)]],
email: ['', [Validators.required, Validators.email]],
age: [null, [Validators.min(18), Validators.max(100)]]
});
// 便捷获取器
get name() { return this.userForm.get('name')!; }
get email() { return this.userForm.get('email')!; }
get age() { return this.userForm.get('age')!; }
onSubmit() {
if (this.userForm.valid) {
console.log('Form submitted:', this.userForm.value);
// 处理表单提交
}
}
resetForm() {
this.userForm.reset();
}
}自定义验证器:
typescript
import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';
export class CustomValidators {
static passwordMatch(passwordField: string, confirmField: string): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
const password = control.get(passwordField);
const confirm = control.get(confirmField);
if (!password || !confirm) {
return null;
}
return password.value === confirm.value ? null : { passwordMismatch: true };
};
}
static noWhitespace(): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
const value = control.value as string;
if (!value) return null;
const hasWhitespace = value.trim().length === 0;
return hasWhitespace ? { whitespace: true } : null;
};
}
static strongPassword(): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
const value = control.value as string;
if (!value) return null;
const hasNumber = /\d/.test(value);
const hasUpper = /[A-Z]/.test(value);
const hasLower = /[a-z]/.test(value);
const hasSpecial = /[!@#$%^&*(),.?":{}|<>]/.test(value);
const isLongEnough = value.length >= 8;
const valid = hasNumber && hasUpper && hasLower && hasSpecial && isLongEnough;
return valid ? null : {
weakPassword: {
hasNumber,
hasUpper,
hasLower,
hasSpecial,
isLongEnough
}
};
};
}
}Directives
Directives
Directives allow you to attach behavior to elements in the DOM.
Structural Directive:
typescript
import { Directive, Input, TemplateRef, ViewContainerRef, inject } from '@angular/core';
@Directive({
selector: '[appRepeat]',
standalone: true
})
export class RepeatDirective {
private templateRef = inject(TemplateRef<any>);
private viewContainer = inject(ViewContainerRef);
@Input() set appRepeat(times: number) {
this.viewContainer.clear();
for (let i = 0; i < times; i++) {
this.viewContainer.createEmbeddedView(this.templateRef, {
$implicit: i,
index: i
});
}
}
}
// Usage:
// <div *appRepeat="5; let i = index">Item {{ i }}</div>Attribute Directive:
typescript
import { Directive, ElementRef, HostListener, Input, inject } from '@angular/core';
@Directive({
selector: '[appHighlight]',
standalone: true
})
export class HighlightDirective {
private el = inject(ElementRef);
@Input() appHighlight = 'yellow';
@Input() defaultColor = 'transparent';
@HostListener('mouseenter') onMouseEnter() {
this.highlight(this.appHighlight);
}
@HostListener('mouseleave') onMouseLeave() {
this.highlight(this.defaultColor);
}
private highlight(color: string) {
this.el.nativeElement.style.backgroundColor = color;
}
}
// Usage:
// <p appHighlight="lightblue" defaultColor="white">Hover me!</p>指令可让你为DOM元素附加行为。
结构型指令:
typescript
import { Directive, Input, TemplateRef, ViewContainerRef, inject } from '@angular/core';
@Directive({
selector: '[appRepeat]',
standalone: true
})
export class RepeatDirective {
private templateRef = inject(TemplateRef<any>);
private viewContainer = inject(ViewContainerRef);
@Input() set appRepeat(times: number) {
this.viewContainer.clear();
for (let i = 0; i < times; i++) {
this.viewContainer.createEmbeddedView(this.templateRef, {
$implicit: i,
index: i
});
}
}
}
// 使用方式:
// <div *appRepeat="5; let i = index">Item {{ i }}</div>属性型指令:
typescript
import { Directive, ElementRef, HostListener, Input, inject } from '@angular/core';
@Directive({
selector: '[appHighlight]',
standalone: true
})
export class HighlightDirective {
private el = inject(ElementRef);
@Input() appHighlight = 'yellow';
@Input() defaultColor = 'transparent';
@HostListener('mouseenter') onMouseEnter() {
this.highlight(this.appHighlight);
}
@HostListener('mouseleave') onMouseLeave() {
this.highlight(this.defaultColor);
}
private highlight(color: string) {
this.el.nativeElement.style.backgroundColor = color;
}
}
// 使用方式:
// <p appHighlight="lightblue" defaultColor="white">Hover me!</p>Pipes
Pipes
Pipes transform displayed values within templates.
Custom Pipe:
typescript
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'truncate',
standalone: true
})
export class TruncatePipe implements PipeTransform {
transform(value: string, limit = 50, ellipsis = '...'): string {
if (!value) return '';
if (value.length <= limit) return value;
return value.substring(0, limit) + ellipsis;
}
}
// Usage:
// {{ longText | truncate:100:'...' }}Async Pipe with Observables:
typescript
import { Component, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Observable, interval, map } from 'rxjs';
@Component({
selector: 'app-clock',
standalone: true,
imports: [CommonModule],
template: `
<div class="clock">
<h2>Current Time</h2>
<p>{{ time$ | async | date:'medium' }}</p>
</div>
`
})
export class ClockComponent {
time$: Observable<Date> = interval(1000).pipe(
map(() => new Date())
);
}管道可在模板中转换显示的值。
自定义管道:
typescript
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'truncate',
standalone: true
})
export class TruncatePipe implements PipeTransform {
transform(value: string, limit = 50, ellipsis = '...'): string {
if (!value) return '';
if (value.length <= limit) return value;
return value.substring(0, limit) + ellipsis;
}
}
// 使用方式:
// {{ longText | truncate:100:'...' }}结合Observables的Async管道:
typescript
import { Component, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Observable, interval, map } from 'rxjs';
@Component({
selector: 'app-clock',
standalone: true,
imports: [CommonModule],
template: `
<div class="clock">
<h2>Current Time</h2>
<p>{{ time$ | async | date:'medium' }}</p>
</div>
`
})
export class ClockComponent {
time$: Observable<Date> = interval(1000).pipe(
map(() => new Date())
);
}RxJS Integration
RxJS Integration
Angular extensively uses RxJS for reactive programming patterns.
Observable Patterns:
typescript
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, BehaviorSubject, Subject, combineLatest } from 'rxjs';
import { map, filter, debounceTime, distinctUntilChanged, switchMap, catchError, retry } from 'rxjs/operators';
export interface Product {
id: number;
name: string;
price: number;
category: string;
}
@Injectable({
providedIn: 'root'
})
export class ProductService {
private http = inject(HttpClient);
private apiUrl = 'https://api.example.com/products';
// BehaviorSubject for state management
private productsSubject = new BehaviorSubject<Product[]>([]);
products$ = this.productsSubject.asObservable();
// Subject for search queries
private searchSubject = new Subject<string>();
constructor() {
this.initializeSearch();
}
private initializeSearch() {
this.searchSubject.pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap(query => this.searchProducts(query))
).subscribe(products => {
this.productsSubject.next(products);
});
}
search(query: string) {
this.searchSubject.next(query);
}
private searchProducts(query: string): Observable<Product[]> {
return this.http.get<Product[]>(`${this.apiUrl}?q=${query}`).pipe(
retry(3),
catchError(error => {
console.error('Search failed:', error);
return [];
})
);
}
getProductsByCategory(category: string): Observable<Product[]> {
return this.products$.pipe(
map(products => products.filter(p => p.category === category))
);
}
getExpensiveProducts(minPrice: number): Observable<Product[]> {
return this.products$.pipe(
map(products => products.filter(p => p.price >= minPrice))
);
}
}Combining Multiple Observables:
typescript
import { Component, OnInit, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { combineLatest, map } from 'rxjs';
import { ProductService } from './product.service';
import { UserService } from './user.service';
@Component({
selector: 'app-dashboard',
standalone: true,
imports: [CommonModule],
template: `
<div class="dashboard">
@if (dashboardData$ | async; as data) {
<h2>Welcome, {{ data.user.name }}</h2>
<p>Products: {{ data.productCount }}</p>
<p>Total Value: {{ data.totalValue | currency }}</p>
}
</div>
`
})
export class DashboardComponent implements OnInit {
private productService = inject(ProductService);
private userService = inject(UserService);
dashboardData$ = combineLatest([
this.userService.getCurrentUser(),
this.productService.products$
]).pipe(
map(([user, products]) => ({
user,
productCount: products.length,
totalValue: products.reduce((sum, p) => sum + p.price, 0)
}))
);
ngOnInit() {
// Data streams are automatically combined
}
}Angular广泛使用RxJS实现响应式编程模式。
Observable模式:
typescript
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, BehaviorSubject, Subject, combineLatest } from 'rxjs';
import { map, filter, debounceTime, distinctUntilChanged, switchMap, catchError, retry } from 'rxjs/operators';
export interface Product {
id: number;
name: string;
price: number;
category: string;
}
@Injectable({
providedIn: 'root'
})
export class ProductService {
private http = inject(HttpClient);
private apiUrl = 'https://api.example.com/products';
// 用于状态管理的BehaviorSubject
private productsSubject = new BehaviorSubject<Product[]>([]);
products$ = this.productsSubject.asObservable();
// 用于搜索查询的Subject
private searchSubject = new Subject<string>();
constructor() {
this.initializeSearch();
}
private initializeSearch() {
this.searchSubject.pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap(query => this.searchProducts(query))
).subscribe(products => {
this.productsSubject.next(products);
});
}
search(query: string) {
this.searchSubject.next(query);
}
private searchProducts(query: string): Observable<Product[]> {
return this.http.get<Product[]>(`${this.apiUrl}?q=${query}`).pipe(
retry(3),
catchError(error => {
console.error('Search failed:', error);
return [];
})
);
}
getProductsByCategory(category: string): Observable<Product[]> {
return this.products$.pipe(
map(products => products.filter(p => p.category === category))
);
}
getExpensiveProducts(minPrice: number): Observable<Product[]> {
return this.products$.pipe(
map(products => products.filter(p => p.price >= minPrice))
);
}
}合并多个Observables:
typescript
import { Component, OnInit, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { combineLatest, map } from 'rxjs';
import { ProductService } from './product.service';
import { UserService } from './user.service';
@Component({
selector: 'app-dashboard',
standalone: true,
imports: [CommonModule],
template: `
<div class="dashboard">
@if (dashboardData$ | async; as data) {
<h2>Welcome, {{ data.user.name }}</h2>
<p>Products: {{ data.productCount }}</p>
<p>Total Value: {{ data.totalValue | currency }}</p>
}
</div>
`
})
export class DashboardComponent implements OnInit {
private productService = inject(ProductService);
private userService = inject(UserService);
dashboardData$ = combineLatest([
this.userService.getCurrentUser(),
this.productService.products$
]).pipe(
map(([user, products]) => ({
user,
productCount: products.length,
totalValue: products.reduce((sum, p) => sum + p.price, 0)
}))
);
ngOnInit() {
// 数据流会自动合并
}
}Modern Angular Patterns
现代化Angular模式
Standalone Components
Standalone Components
Standalone components eliminate the need for NgModules in most cases.
Standalone Component Application:
typescript
// main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { provideRouter } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';
import { AppComponent } from './app/app.component';
import { routes } from './app/app.routes';
bootstrapApplication(AppComponent, {
providers: [
provideRouter(routes),
provideHttpClient()
]
}).catch(err => console.error(err));App Component:
typescript
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule, RouterOutlet],
template: `
<header>
<h1>My Angular App</h1>
</header>
<main>
<router-outlet></router-outlet>
</main>
<footer>
<p>© 2024 My App</p>
</footer>
`,
styles: [`
header {
background: #1976d2;
color: white;
padding: 20px;
}
main {
min-height: 80vh;
padding: 20px;
}
footer {
background: #f5f5f5;
padding: 20px;
text-align: center;
}
`]
})
export class AppComponent {}独立组件在大多数情况下无需使用NgModule。
独立组件应用:
typescript
// main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { provideRouter } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';
import { AppComponent } from './app/app.component';
import { routes } from './app/app.routes';
bootstrapApplication(AppComponent, {
providers: [
provideRouter(routes),
provideHttpClient()
]
}).catch(err => console.error(err));应用组件:
typescript
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule, RouterOutlet],
template: `
<header>
<h1>My Angular App</h1>
</header>
<main>
<router-outlet></router-outlet>
</main>
<footer>
<p>© 2024 My App</p>
</footer>
`,
styles: [`
header {
background: #1976d2;
color: white;
padding: 20px;
}
main {
min-height: 80vh;
padding: 20px;
}
footer {
background: #f5f5f5;
padding: 20px;
text-align: center;
}
`]
})
export class AppComponent {}Control Flow Syntax
Control Flow Syntax
Modern Angular uses new control flow syntax with , , and .
@if@for@switchtypescript
import { Component, signal } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-control-flow-demo',
standalone: true,
imports: [CommonModule],
template: `
<div class="demo">
<!-- @if directive -->
@if (isLoggedIn()) {
<p>Welcome back!</p>
<button (click)="logout()">Logout</button>
} @else {
<p>Please log in</p>
<button (click)="login()">Login</button>
}
<!-- @for directive -->
<h3>Items:</h3>
@for (item of items(); track item.id) {
<div class="item">
<span>{{ item.name }}</span>
@if ($index === 0) {
<span class="badge">First</span>
}
</div>
} @empty {
<p>No items available</p>
}
<!-- @switch directive -->
<h3>Status: {{ status() }}</h3>
@switch (status()) {
@case ('loading') {
<p>Loading data...</p>
}
@case ('success') {
<p>Data loaded successfully!</p>
}
@case ('error') {
<p>Error loading data</p>
}
@default {
<p>Unknown status</p>
}
}
</div>
`
})
export class ControlFlowDemoComponent {
isLoggedIn = signal(false);
items = signal([
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' },
{ id: 3, name: 'Item 3' }
]);
status = signal<'loading' | 'success' | 'error' | 'idle'>('idle');
login() {
this.isLoggedIn.set(true);
}
logout() {
this.isLoggedIn.set(false);
}
}现代化Angular使用新的控制流语法,包括、和。
@if@for@switchtypescript
import { Component, signal } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-control-flow-demo',
standalone: true,
imports: [CommonModule],
template: `
<div class="demo">
<!-- @if 指令 -->
@if (isLoggedIn()) {
<p>Welcome back!</p>
<button (click)="logout()">Logout</button>
} @else {
<p>Please log in</p>
<button (click)="login()">Login</button>
}
<!-- @for 指令 -->
<h3>Items:</h3>
@for (item of items(); track item.id) {
<div class="item">
<span>{{ item.name }}</span>
@if ($index === 0) {
<span class="badge">First</span>
}
</div>
} @empty {
<p>No items available</p>
}
<!-- @switch 指令 -->
<h3>Status: {{ status() }}</h3>
@switch (status()) {
@case ('loading') {
<p>Loading data...</p>
}
@case ('success') {
<p>Data loaded successfully!</p>
}
@case ('error') {
<p>Error loading data</p>
}
@default {
<p>Unknown status</p>
}
}
</div>
`
})
export class ControlFlowDemoComponent {
isLoggedIn = signal(false);
items = signal([
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' },
{ id: 3, name: 'Item 3' }
]);
status = signal<'loading' | 'success' | 'error' | 'idle'>('idle');
login() {
this.isLoggedIn.set(true);
}
logout() {
this.isLoggedIn.set(false);
}
}Input and Output with Signals
Input and Output with Signals
Modern Angular supports signal-based inputs and outputs.
typescript
import { Component, input, output, model } from '@angular/core';
@Component({
selector: 'app-user-card',
standalone: true,
template: `
<div class="card">
<h3>{{ name() }}</h3>
<p>{{ email() }}</p>
<p>Active: {{ isActive() }}</p>
<button (click)="handleClick()">Select</button>
<button (click)="toggleActive()">Toggle Active</button>
</div>
`
})
export class UserCardComponent {
// Signal-based input (read-only)
name = input.required<string>();
email = input<string>('');
// Two-way binding with model()
isActive = model(false);
// Signal-based output
userSelected = output<string>();
handleClick() {
this.userSelected.emit(this.name());
}
toggleActive() {
this.isActive.update(active => !active);
}
}
// Parent component usage:
// <app-user-card
// [name]="userName"
// [email]="userEmail"
// [(isActive)]="userActive"
// (userSelected)="onUserSelected($event)"
// />现代化Angular支持基于Signals的输入和输出。
typescript
import { Component, input, output, model } from '@angular/core';
@Component({
selector: 'app-user-card',
standalone: true,
template: `
<div class="card">
<h3>{{ name() }}</h3>
<p>{{ email() }}</p>
<p>Active: {{ isActive() }}</p>
<button (click)="handleClick()">Select</button>
<button (click)="toggleActive()">Toggle Active</button>
</div>
`
})
export class UserCardComponent {
// 基于Signals的输入(只读)
name = input.required<string>();
email = input<string>('');
// 使用model()实现双向绑定
isActive = model(false);
// 基于Signals的输出
userSelected = output<string>();
handleClick() {
this.userSelected.emit(this.name());
}
toggleActive() {
this.isActive.update(active => !active);
}
}
// 父组件使用方式:
// <app-user-card
// [name]="userName"
// [email]="userEmail"
// [(isActive)]="userActive"
// (userSelected)="onUserSelected($event)"
// />Best Practices from Context7 Research
Context7研究总结的最佳实践
1. Use Standalone Components
1. 使用独立组件
Prefer standalone components over NgModule-based components for better tree-shaking and simpler architecture.
typescript
// Good: Standalone component
@Component({
selector: 'app-feature',
standalone: true,
imports: [CommonModule, FormsModule],
template: `...`
})
export class FeatureComponent {}
// Avoid: NgModule-based (legacy pattern)
@NgModule({
declarations: [FeatureComponent],
imports: [CommonModule, FormsModule]
})
export class FeatureModule {}优先使用独立组件而非基于NgModule的组件,以获得更好的摇树优化和更简洁的架构。
typescript
// 推荐:独立组件
@Component({
selector: 'app-feature',
standalone: true,
imports: [CommonModule, FormsModule],
template: `...`
})
export class FeatureComponent {}
// 避免:基于NgModule的组件(旧模式)
@NgModule({
declarations: [FeatureComponent],
imports: [CommonModule, FormsModule]
})
export class FeatureModule {}2. Use inject() Function
2. 使用inject()函数
Prefer the function over constructor injection for cleaner code.
inject()typescript
// Good: inject() function
export class MyComponent {
private http = inject(HttpClient);
private router = inject(Router);
}
// Avoid: Constructor injection (still valid but more verbose)
export class MyComponent {
constructor(
private http: HttpClient,
private router: Router
) {}
}优先使用函数而非构造函数注入,以获得更简洁的代码。
inject()typescript
// 推荐:inject()函数
export class MyComponent {
private http = inject(HttpClient);
private router = inject(Router);
}
// 避免:构造函数注入(仍有效但更冗长)
export class MyComponent {
constructor(
private http: HttpClient,
private router: Router
) {}
}3. Leverage Signals for State
3. 利用Signals管理状态
Use Signals for reactive state management instead of manually managing observables.
typescript
// Good: Signals
export class TodoService {
private todos = signal<Todo[]>([]);
completedCount = computed(() => this.todos().filter(t => t.completed).length);
}
// Avoid: Manual observable management
export class TodoService {
private todosSubject = new BehaviorSubject<Todo[]>([]);
todos$ = this.todosSubject.asObservable();
completedCount$ = this.todos$.pipe(
map(todos => todos.filter(t => t.completed).length)
);
}使用Signals进行响应式状态管理,而非手动管理observables。
typescript
// 推荐:Signals
export class TodoService {
private todos = signal<Todo[]>([]);
completedCount = computed(() => this.todos().filter(t => t.completed).length);
}
// 避免:手动管理observables
export class TodoService {
private todosSubject = new BehaviorSubject<Todo[]>([]);
todos$ = this.todosSubject.asObservable();
completedCount$ = this.todos$.pipe(
map(todos => todos.filter(t => t.completed).length)
);
}4. Implement Lazy Loading
4. 实现懒加载
Use lazy loading for better performance and faster initial load times.
typescript
// Good: Lazy loaded routes
export const routes: Routes = [
{
path: 'admin',
loadComponent: () => import('./admin/admin.component').then(m => m.AdminComponent)
}
];
// Avoid: Eager loading everything
import { AdminComponent } from './admin/admin.component';
export const routes: Routes = [
{ path: 'admin', component: AdminComponent }
];使用懒加载提升性能,加快初始加载速度。
typescript
// 推荐:懒加载路由
export const routes: Routes = [
{
path: 'admin',
loadComponent: () => import('./admin/admin.component').then(m => m.AdminComponent)
}
];
// 避免:全部预加载
import { AdminComponent } from './admin/admin.component';
export const routes: Routes = [
{ path: 'admin', component: AdminComponent }
];5. Use Reactive Forms
5. 使用响应式表单
Prefer reactive forms over template-driven forms for better testability and type safety.
typescript
// Good: Reactive forms
export class MyFormComponent {
form = this.fb.group({
name: ['', Validators.required],
email: ['', [Validators.required, Validators.email]]
});
}
// Avoid: Template-driven forms for complex scenarios
// <form #myForm="ngForm">
// <input name="name" ngModel required>
// </form>优先使用响应式表单而非模板驱动表单,以获得更好的可测试性和类型安全性。
typescript
// 推荐:响应式表单
export class MyFormComponent {
form = this.fb.group({
name: ['', Validators.required],
email: ['', [Validators.required, Validators.email]]
});
}
// 避免:复杂场景使用模板驱动表单
// <form #myForm="ngForm">
// <input name="name" ngModel required>
// </form>6. Unsubscribe from Observables
6. 取消Observable订阅
Always clean up subscriptions to prevent memory leaks.
typescript
// Good: Using takeUntilDestroyed (Angular 16+)
export class MyComponent {
private destroyed$ = inject(DestroyRef);
ngOnInit() {
this.dataService.getData()
.pipe(takeUntilDestroyed(this.destroyed$))
.subscribe(data => this.data = data);
}
}
// Alternative: Using async pipe (automatically unsubscribes)
export class MyComponent {
data$ = this.dataService.getData();
}始终清理订阅以防止内存泄漏。
typescript
// 推荐:使用takeUntilDestroyed(Angular 16+)
export class MyComponent {
private destroyed$ = inject(DestroyRef);
ngOnInit() {
this.dataService.getData()
.pipe(takeUntilDestroyed(this.destroyed$))
.subscribe(data => this.data = data);
}
}
// 替代方案:使用async管道(自动取消订阅)
export class MyComponent {
data$ = this.dataService.getData();
}7. Use OnPush Change Detection
7. 使用OnPush变更检测
Optimize performance with OnPush change detection strategy.
typescript
import { ChangeDetectionStrategy } from '@angular/core';
@Component({
selector: 'app-optimized',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
template: `{{ data() }}`
})
export class OptimizedComponent {
data = signal('initial value');
}使用OnPush变更检测策略优化性能。
typescript
import { ChangeDetectionStrategy } from '@angular/core';
@Component({
selector: 'app-optimized',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
template: `{{ data() }}`
})
export class OptimizedComponent {
data = signal('initial value');
}8. Implement Proper Error Handling
8. 实现完善的错误处理
Always handle errors in HTTP requests and observables.
typescript
export class DataService {
private http = inject(HttpClient);
getData(): Observable<Data[]> {
return this.http.get<Data[]>('/api/data').pipe(
retry(3),
catchError(error => {
console.error('Failed to fetch data:', error);
return of([]);
})
);
}
}始终处理HTTP请求和observables中的错误。
typescript
export class DataService {
private http = inject(HttpClient);
getData(): Observable<Data[]> {
return this.http.get<Data[]>('/api/data').pipe(
retry(3),
catchError(error => {
console.error('Failed to fetch data:', error);
return of([]);
})
);
}
}9. Use TrackBy with ngFor
9. 为ngFor使用TrackBy
Improve rendering performance with trackBy functions.
typescript
// Good: With trackBy
@Component({
template: `
@for (item of items; track item.id) {
<div>{{ item.name }}</div>
}
`
})
export class MyComponent {
items = [{ id: 1, name: 'Item 1' }];
}
// Old syntax with trackBy:
// *ngFor="let item of items; trackBy: trackById"使用trackBy函数提升渲染性能。
typescript
// 推荐:使用trackBy
@Component({
template: `
@for (item of items; track item.id) {
<div>{{ item.name }}</div>
}
`
})
export class MyComponent {
items = [{ id: 1, name: 'Item 1' }];
}
// 旧版语法的trackBy:
// *ngFor="let item of items; trackBy: trackById"10. Type Your Code
10. 为代码添加类型
Leverage TypeScript's type system for better IDE support and fewer runtime errors.
typescript
interface User {
id: number;
name: string;
email: string;
role: 'admin' | 'user' | 'guest';
}
export class UserService {
getUser(id: number): Observable<User> {
return this.http.get<User>(`/api/users/${id}`);
}
updateUser(id: number, updates: Partial<User>): Observable<User> {
return this.http.patch<User>(`/api/users/${id}`, updates);
}
}利用TypeScript的类型系统获得更好的IDE支持并减少运行时错误。
typescript
interface User {
id: number;
name: string;
email: string;
role: 'admin' | 'user' | 'guest';
}
export class UserService {
getUser(id: number): Observable<User> {
return this.http.get<User>(`/api/users/${id}`);
}
updateUser(id: number, updates: Partial<User>): Observable<User> {
return this.http.patch<User>(`/api/users/${id}`, updates);
}
}Performance Optimization
性能优化
Lazy Loading Modules
懒加载模块
typescript
export const routes: Routes = [
{
path: 'dashboard',
loadComponent: () => import('./dashboard/dashboard.component')
.then(m => m.DashboardComponent),
children: [
{
path: 'analytics',
loadComponent: () => import('./analytics/analytics.component')
.then(m => m.AnalyticsComponent)
}
]
}
];typescript
export const routes: Routes = [
{
path: 'dashboard',
loadComponent: () => import('./dashboard/dashboard.component')
.then(m => m.DashboardComponent),
children: [
{
path: 'analytics',
loadComponent: () => import('./analytics/analytics.component')
.then(m => m.AnalyticsComponent)
}
]
}
];Virtual Scrolling
虚拟滚动
typescript
import { Component } from '@angular/core';
import { ScrollingModule } from '@angular/cdk/scrolling';
@Component({
selector: 'app-virtual-scroll',
standalone: true,
imports: [ScrollingModule],
template: `
<cdk-virtual-scroll-viewport itemSize="50" class="viewport">
@for (item of items; track item) {
<div class="item">{{ item }}</div>
}
</cdk-virtual-scroll-viewport>
`,
styles: [`
.viewport {
height: 400px;
width: 100%;
}
.item {
height: 50px;
}
`]
})
export class VirtualScrollComponent {
items = Array.from({ length: 10000 }, (_, i) => `Item ${i + 1}`);
}typescript
import { Component } from '@angular/core';
import { ScrollingModule } from '@angular/cdk/scrolling';
@Component({
selector: 'app-virtual-scroll',
standalone: true,
imports: [ScrollingModule],
template: `
<cdk-virtual-scroll-viewport itemSize="50" class="viewport">
@for (item of items; track item) {
<div class="item">{{ item }}</div>
}
</cdk-virtual-scroll-viewport>
`,
styles: [`
.viewport {
height: 400px;
width: 100%;
}
.item {
height: 50px;
}
`]
})
export class VirtualScrollComponent {
items = Array.from({ length: 10000 }, (_, i) => `Item ${i + 1}`);
}Memoization with Signals
使用Signals进行记忆化
typescript
export class DataProcessorService {
private rawData = signal<number[]>([]);
// Computed signals automatically memoize results
processedData = computed(() => {
const data = this.rawData();
// Expensive computation only runs when rawData changes
return data.map(n => n * 2).filter(n => n > 10).sort((a, b) => a - b);
});
statistics = computed(() => {
const data = this.processedData();
return {
count: data.length,
sum: data.reduce((a, b) => a + b, 0),
average: data.length ? data.reduce((a, b) => a + b, 0) / data.length : 0
};
});
}typescript
export class DataProcessorService {
private rawData = signal<number[]>([]);
// 计算信号会自动记忆结果
processedData = computed(() => {
const data = this.rawData();
// 昂贵的计算仅在rawData变化时运行
return data.map(n => n * 2).filter(n => n > 10).sort((a, b) => a - b);
});
statistics = computed(() => {
const data = this.processedData();
return {
count: data.length,
sum: data.reduce((a, b) => a + b, 0),
average: data.length ? data.reduce((a, b) => a + b, 0) / data.length : 0
};
});
}Testing Angular Applications
测试Angular应用
Component Testing
组件测试
typescript
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { UserListComponent } from './user-list.component';
import { UserService } from './user.service';
import { of } from 'rxjs';
describe('UserListComponent', () => {
let component: UserListComponent;
let fixture: ComponentFixture<UserListComponent>;
let userService: jasmine.SpyObj<UserService>;
beforeEach(async () => {
const userServiceSpy = jasmine.createSpyObj('UserService', ['getUsers']);
await TestBed.configureTestingModule({
imports: [UserListComponent],
providers: [
{ provide: UserService, useValue: userServiceSpy }
]
}).compileComponents();
userService = TestBed.inject(UserService) as jasmine.SpyObj<UserService>;
fixture = TestBed.createComponent(UserListComponent);
component = fixture.componentInstance;
});
it('should create', () => {
expect(component).toBeTruthy();
});
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));
fixture.detectChanges();
expect(component.users.length).toBe(2);
expect(userService.getUsers).toHaveBeenCalled();
});
});typescript
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { UserListComponent } from './user-list.component';
import { UserService } from './user.service';
import { of } from 'rxjs';
describe('UserListComponent', () => {
let component: UserListComponent;
let fixture: ComponentFixture<UserListComponent>;
let userService: jasmine.SpyObj<UserService>;
beforeEach(async () => {
const userServiceSpy = jasmine.createSpyObj('UserService', ['getUsers']);
await TestBed.configureTestingModule({
imports: [UserListComponent],
providers: [
{ provide: UserService, useValue: userServiceSpy }
]
}).compileComponents();
userService = TestBed.inject(UserService) as jasmine.SpyObj<UserService>;
fixture = TestBed.createComponent(UserListComponent);
component = fixture.componentInstance;
});
it('should create', () => {
expect(component).toBeTruthy();
});
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));
fixture.detectChanges();
expect(component.users.length).toBe(2);
expect(userService.getUsers).toHaveBeenCalled();
});
});Service Testing
服务测试
typescript
import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { UserService } from './user.service';
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 fetch users', () => {
const mockUsers = [
{ id: 1, name: 'John', email: 'john@example.com' }
];
service.getUsers().subscribe(users => {
expect(users.length).toBe(1);
expect(users).toEqual(mockUsers);
});
const req = httpMock.expectOne('https://api.example.com/users');
expect(req.request.method).toBe('GET');
req.flush(mockUsers);
});
});typescript
import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { UserService } from './user.service';
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 fetch users', () => {
const mockUsers = [
{ id: 1, name: 'John', email: 'john@example.com' }
];
service.getUsers().subscribe(users => {
expect(users.length).toBe(1);
expect(users).toEqual(mockUsers);
});
const req = httpMock.expectOne('https://api.example.com/users');
expect(req.request.method).toBe('GET');
req.flush(mockUsers);
});
});Migration Guide
迁移指南
From NgModules to Standalone
从NgModules迁移到独立组件
typescript
// Before: NgModule-based
@NgModule({
declarations: [MyComponent],
imports: [CommonModule, FormsModule],
exports: [MyComponent]
})
export class MyModule {}
// After: Standalone
@Component({
selector: 'app-my-component',
standalone: true,
imports: [CommonModule, FormsModule],
template: `...`
})
export class MyComponent {}typescript
// 之前:基于NgModule
@NgModule({
declarations: [MyComponent],
imports: [CommonModule, FormsModule],
exports: [MyComponent]
})
export class MyModule {}
// 之后:独立组件
@Component({
selector: 'app-my-component',
standalone: true,
imports: [CommonModule, FormsModule],
template: `...`
})
export class MyComponent {}From Constructor to inject()
从构造函数注入迁移到inject()
typescript
// Before: Constructor injection
export class MyService {
constructor(
private http: HttpClient,
private router: Router,
private auth: AuthService
) {}
}
// After: inject() function
export class MyService {
private http = inject(HttpClient);
private router = inject(Router);
private auth = inject(AuthService);
}typescript
// 之前:构造函数注入
export class MyService {
constructor(
private http: HttpClient,
private router: Router,
private auth: AuthService
) {}
}
// 之后:inject()函数
export class MyService {
private http = inject(HttpClient);
private router = inject(Router);
private auth = inject(AuthService);
}From BehaviorSubject to Signals
从BehaviorSubject迁移到Signals
typescript
// Before: BehaviorSubject
export class StateService {
private countSubject = new BehaviorSubject<number>(0);
count$ = this.countSubject.asObservable();
increment() {
this.countSubject.next(this.countSubject.value + 1);
}
}
// After: Signals
export class StateService {
count = signal(0);
increment() {
this.count.update(value => value + 1);
}
}typescript
// 之前:BehaviorSubject
export class StateService {
private countSubject = new BehaviorSubject<number>(0);
count$ = this.countSubject.asObservable();
increment() {
this.countSubject.next(this.countSubject.value + 1);
}
}
// 之后:Signals
export class StateService {
count = signal(0);
increment() {
this.count.update(value => value + 1);
}
}Common Patterns
常见模式
Master-Detail Pattern
主从模式
typescript
// List component
@Component({
selector: 'app-product-list',
standalone: true,
imports: [CommonModule],
template: `
<div class="product-list">
@for (product of products(); track product.id) {
<div
class="product-item"
[class.selected]="selectedId() === product.id"
(click)="selectProduct(product.id)"
>
{{ product.name }} - {{ product.price | currency }}
</div>
}
</div>
`
})
export class ProductListComponent {
products = input.required<Product[]>();
selectedId = model<number | null>(null);
selectProduct(id: number) {
this.selectedId.set(id);
}
}
// Parent component
@Component({
selector: 'app-product-master-detail',
standalone: true,
imports: [ProductListComponent, ProductDetailComponent],
template: `
<div class="master-detail">
<app-product-list
[products]="products()"
[(selectedId)]="selectedProductId"
/>
@if (selectedProduct(); as product) {
<app-product-detail [product]="product" />
}
</div>
`
})
export class ProductMasterDetailComponent {
products = signal<Product[]>([]);
selectedProductId = signal<number | null>(null);
selectedProduct = computed(() => {
const id = this.selectedProductId();
return this.products().find(p => p.id === id);
});
}typescript
// 列表组件
@Component({
selector: 'app-product-list',
standalone: true,
imports: [CommonModule],
template: `
<div class="product-list">
@for (product of products(); track product.id) {
<div
class="product-item"
[class.selected]="selectedId() === product.id"
(click)="selectProduct(product.id)"
>
{{ product.name }} - {{ product.price | currency }}
</div>
}
</div>
`
})
export class ProductListComponent {
products = input.required<Product[]>();
selectedId = model<number | null>(null);
selectProduct(id: number) {
this.selectedId.set(id);
}
}
// 父组件
@Component({
selector: 'app-product-master-detail',
standalone: true,
imports: [ProductListComponent, ProductDetailComponent],
template: `
<div class="master-detail">
<app-product-list
[products]="products()"
[(selectedId)]="selectedProductId"
/>
@if (selectedProduct(); as product) {
<app-product-detail [product]="product" />
}
</div>
`
})
export class ProductMasterDetailComponent {
products = signal<Product[]>([]);
selectedProductId = signal<number | null>(null);
selectedProduct = computed(() => {
const id = this.selectedProductId();
return this.products().find(p => p.id === id);
});
}Smart/Presentational Pattern
智能/展示组件模式
typescript
// Presentational component (dumb)
@Component({
selector: 'app-user-card-presentational',
standalone: true,
imports: [CommonModule],
template: `
<div class="user-card">
<h3>{{ user().name }}</h3>
<p>{{ user().email }}</p>
<button (click)="edit.emit(user())">Edit</button>
<button (click)="delete.emit(user().id)">Delete</button>
</div>
`
})
export class UserCardPresentationalComponent {
user = input.required<User>();
edit = output<User>();
delete = output<number>();
}
// Smart component (container)
@Component({
selector: 'app-user-list-container',
standalone: true,
imports: [CommonModule, UserCardPresentationalComponent],
template: `
@for (user of users$ | async; track user.id) {
<app-user-card-presentational
[user]="user"
(edit)="handleEdit($event)"
(delete)="handleDelete($event)"
/>
}
`
})
export class UserListContainerComponent {
private userService = inject(UserService);
users$ = this.userService.getUsers();
handleEdit(user: User) {
// Business logic
this.userService.updateUser(user.id, user).subscribe();
}
handleDelete(id: number) {
// Business logic
this.userService.deleteUser(id).subscribe();
}
}typescript
// 展示组件(无状态)
@Component({
selector: 'app-user-card-presentational',
standalone: true,
imports: [CommonModule],
template: `
<div class="user-card">
<h3>{{ user().name }}</h3>
<p>{{ user().email }}</p>
<button (click)="edit.emit(user())">Edit</button>
<button (click)="delete.emit(user().id)">Delete</button>
</div>
`
})
export class UserCardPresentationalComponent {
user = input.required<User>();
edit = output<User>();
delete = output<number>();
}
// 智能组件(容器)
@Component({
selector: 'app-user-list-container',
standalone: true,
imports: [CommonModule, UserCardPresentationalComponent],
template: `
@for (user of users$ | async; track user.id) {
<app-user-card-presentational
[user]="user"
(edit)="handleEdit($event)"
(delete)="handleDelete($event)"
/>
}
`
})
export class UserListContainerComponent {
private userService = inject(UserService);
users$ = this.userService.getUsers();
handleEdit(user: User) {
// 业务逻辑
this.userService.updateUser(user.id, user).subscribe();
}
handleDelete(id: number) {
// 业务逻辑
this.userService.deleteUser(id).subscribe();
}
}Context7 Integration Summary
Context7集成总结
This skill incorporates best practices from the official Angular documentation (Context7 Trust Score: 8.9), including:
- Standalone Components: Modern approach eliminating NgModules
- inject() Function: Cleaner dependency injection
- Signals: Fine-grained reactive state management
- Control Flow Syntax: @if, @for, @switch directives
- Lazy Loading: Performance optimization patterns
- Reactive Forms: Type-safe form handling
- RxJS Patterns: Observable composition and operators
- Modern Routing: Functional guards and resolvers
- Change Detection: OnPush strategy for performance
- Testing: Component and service testing patterns
All examples follow the latest Angular best practices and patterns recommended in the official documentation, ensuring production-ready, maintainable, and performant Angular applications.
本技能整合了Angular官方文档中的最佳实践(Context7信任评分:8.9),包括:
- 独立组件:无需NgModules的现代化方案
- inject()函数:更简洁的依赖注入方式
- Signals:细粒度响应式状态管理
- 控制流语法:@if、@for、@switch指令
- 懒加载:性能优化模式
- 响应式表单:类型安全的表单处理
- RxJS模式:Observable组合和操作符
- 现代化路由:函数式守卫和解析器
- 变更检测:OnPush策略提升性能
- 测试:组件和服务测试模式
所有示例均遵循Angular官方文档推荐的最新最佳实践和模式,可用于构建生产就绪、可维护且高性能的Angular应用。