angular
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseAngular Expert
Angular专家
Master modern Angular development with Signals, Standalone Components, Zoneless applications, SSR/Hydration, and the latest reactive patterns.
掌握基于Signals、Standalone Components、Zoneless应用、SSR/Hydration以及最新响应式模式的现代Angular开发。
When to Use This Skill
何时使用此技能
- Building new Angular applications (v20+)
- Implementing Signals-based reactive patterns
- Creating Standalone Components and migrating from NgModules
- Configuring Zoneless Angular applications
- Implementing SSR, prerendering, and hydration
- Optimizing Angular performance
- Adopting modern Angular patterns and best practices
- 构建新的Angular应用(v20+)
- 实现基于Signals的响应式模式
- 创建Standalone Components并从NgModules迁移
- 配置Zoneless Angular应用
- 实现SSR、预渲染和水合(Hydration)
- 优化Angular性能
- 采用现代Angular模式与最佳实践
Do Not Use This Skill When
何时不使用此技能
- Migrating from AngularJS (1.x) → use skill
angular-migration - Working with legacy Angular apps that cannot upgrade
- General TypeScript issues → use skill
typescript-expert
- 从AngularJS(1.x)迁移 → 使用技能
angular-migration - 处理无法升级的遗留Angular应用
- 通用TypeScript问题 → 使用技能
typescript-expert
Instructions
操作说明
- Assess the Angular version and project structure
- Apply modern patterns (Signals, Standalone, Zoneless)
- Implement with proper typing and reactivity
- Validate with build and tests
- 评估Angular版本和项目结构
- 应用现代模式(Signals、Standalone、Zoneless)
- 结合正确的类型定义和响应式特性实现功能
- 通过构建和测试验证成果
Safety
注意事项
- Always test changes in development before production
- Gradual migration for existing apps (don't big-bang refactor)
- Keep backward compatibility during transitions
- 生产环境前务必在开发环境测试变更
- 现有应用逐步迁移(避免大规模重构)
- 过渡期间保持向后兼容性
Angular Version Timeline
Angular版本时间线
| Version | Release | Key Features |
|---|---|---|
| Angular 20 | Q2 2025 | Signals stable, Zoneless stable, Incremental hydration |
| Angular 21 | Q4 2025 | Signals-first default, Enhanced SSR |
| Angular 22 | Q2 2026 | Signal Forms, Selectorless components |
| 版本 | 发布时间 | 核心特性 |
|---|---|---|
| Angular 20 | 2025年第二季度 | Signals稳定版、Zoneless稳定版、增量水合 |
| Angular 21 | 2025年第四季度 | 默认Signals优先、增强型SSR |
| Angular 22 | 2026年第二季度 | Signal Forms、无选择器组件 |
1. Signals: The New Reactive Primitive
1. Signals:新一代响应式原语
Signals are Angular's fine-grained reactivity system, replacing zone.js-based change detection.
Signals是Angular的细粒度响应式系统,替代基于zone.js的变更检测。
Core Concepts
核心概念
typescript
import { signal, computed, effect } from "@angular/core";
// Writable signal
const count = signal(0);
// Read value
console.log(count()); // 0
// Update value
count.set(5); // Direct set
count.update((v) => v + 1); // Functional update
// Computed (derived) signal
const doubled = computed(() => count() * 2);
// Effect (side effects)
effect(() => {
console.log(`Count changed to: ${count()}`);
});typescript
import { signal, computed, effect } from "@angular/core";
// 可写Signal
const count = signal(0);
// 读取值
console.log(count()); // 0
// 更新值
count.set(5); // 直接设置
count.update((v) => v + 1); // 函数式更新
// 计算(派生)Signal
const doubled = computed(() => count() * 2);
// 副作用(Effect)
effect(() => {
console.log(`Count已变更为: ${count()}`);
});Signal-Based Inputs and Outputs
基于Signal的输入与输出
typescript
import { Component, input, output, model } from "@angular/core";
@Component({
selector: "app-user-card",
standalone: true,
template: `
<div class="card">
<h3>{{ name() }}</h3>
<span>{{ role() }}</span>
<button (click)="select.emit(id())">Select</button>
</div>
`,
})
export class UserCardComponent {
// Signal inputs (read-only)
id = input.required<string>();
name = input.required<string>();
role = input<string>("User"); // With default
// Output
select = output<string>();
// Two-way binding (model)
isSelected = model(false);
}
// Usage:
// <app-user-card [id]="'123'" [name]="'John'" [(isSelected)]="selected" />typescript
import { Component, input, output, model } from "@angular/core";
@Component({
selector: "app-user-card",
standalone: true,
template: `
<div class="card">
<h3>{{ name() }}</h3>
<span>{{ role() }}</span>
<button (click)="select.emit(id())">选择</button>
</div>
`,
})
export class UserCardComponent {
// Signal输入(只读)
id = input.required<string>();
name = input.required<string>();
role = input<string>("普通用户"); // 带默认值
// 输出
select = output<string>();
// 双向绑定(model)
isSelected = model(false);
}
// 使用方式:
// <app-user-card [id]="'123'" [name]="'John'" [(isSelected)]="selected" />Signal Queries (ViewChild/ContentChild)
Signal查询(ViewChild/ContentChild)
typescript
import {
Component,
viewChild,
viewChildren,
contentChild,
} from "@angular/core";
@Component({
selector: "app-container",
standalone: true,
template: `
<input #searchInput />
<app-item *ngFor="let item of items()" />
`,
})
export class ContainerComponent {
// Signal-based queries
searchInput = viewChild<ElementRef>("searchInput");
items = viewChildren(ItemComponent);
projectedContent = contentChild(HeaderDirective);
focusSearch() {
this.searchInput()?.nativeElement.focus();
}
}typescript
import {
Component,
viewChild,
viewChildren,
contentChild,
} from "@angular/core";
@Component({
selector: "app-container",
standalone: true,
template: `
<input #searchInput />
<app-item *ngFor="let item of items()" />
`,
})
export class ContainerComponent {
// 基于Signal的查询
searchInput = viewChild<ElementRef>("searchInput");
items = viewChildren(ItemComponent);
projectedContent = contentChild(HeaderDirective);
focusSearch() {
this.searchInput()?.nativeElement.focus();
}
}When to Use Signals vs RxJS
Signals与RxJS的适用场景对比
| Use Case | Signals | RxJS |
|---|---|---|
| Local component state | ✅ Preferred | Overkill |
| Derived/computed values | ✅ | |
| Side effects | ✅ | |
| HTTP requests | ❌ | ✅ HttpClient returns Observable |
| Event streams | ❌ | ✅ |
| Complex async flows | ❌ | ✅ |
| 场景 | Signals | RxJS |
|---|---|---|
| 组件本地状态 | ✅ 优先使用 | 大材小用 |
| 派生/计算值 | ✅ | |
| 副作用 | ✅ | |
| HTTP请求 | ❌ | ✅ HttpClient返回Observable |
| 事件流 | ❌ | ✅ |
| 复杂异步流 | ❌ | ✅ |
2. Standalone Components
2. Standalone Components(独立组件)
Standalone components are self-contained and don't require NgModule declarations.
独立组件是自包含的,无需NgModule声明。
Creating Standalone Components
创建独立组件
typescript
import { Component } from "@angular/core";
import { CommonModule } from "@angular/common";
import { RouterLink } from "@angular/router";
@Component({
selector: "app-header",
standalone: true,
imports: [CommonModule, RouterLink], // Direct imports
template: `
<header>
<a routerLink="/">Home</a>
<a routerLink="/about">About</a>
</header>
`,
})
export class HeaderComponent {}typescript
import { Component } from "@angular/core";
import { CommonModule } from "@angular/common";
import { RouterLink } from "@angular/router";
@Component({
selector: "app-header",
standalone: true,
imports: [CommonModule, RouterLink], // 直接导入依赖
template: `
<header>
<a routerLink="/">首页</a>
<a routerLink="/about">关于我们</a>
</header>
`,
})
export class HeaderComponent {}Bootstrapping Without NgModule
无需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()],
});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()],
});Lazy Loading Standalone Components
独立组件的懒加载
typescript
// app.routes.ts
import { Routes } from "@angular/router";
export const routes: Routes = [
{
path: "dashboard",
loadComponent: () =>
import("./dashboard/dashboard.component").then(
(m) => m.DashboardComponent,
),
},
{
path: "admin",
loadChildren: () =>
import("./admin/admin.routes").then((m) => m.ADMIN_ROUTES),
},
];typescript
// app.routes.ts
import { Routes } from "@angular/router";
export const routes: Routes = [
{
path: "dashboard",
loadComponent: () =>
import("./dashboard/dashboard.component").then(
(m) => m.DashboardComponent,
),
},
{
path: "admin",
loadChildren: () =>
import("./admin/admin.routes").then((m) => m.ADMIN_ROUTES),
},
];3. Zoneless Angular
3. Zoneless Angular
Zoneless applications don't use zone.js, improving performance and debugging.
Zoneless应用不使用zone.js,提升性能与调试体验。
Enabling Zoneless Mode
启用Zoneless模式
typescript
// main.ts
import { bootstrapApplication } from "@angular/platform-browser";
import { provideZonelessChangeDetection } from "@angular/core";
import { AppComponent } from "./app/app.component";
bootstrapApplication(AppComponent, {
providers: [provideZonelessChangeDetection()],
});typescript
// main.ts
import { bootstrapApplication } from "@angular/platform-browser";
import { provideZonelessChangeDetection } from "@angular/core";
import { AppComponent } from "./app/app.component";
bootstrapApplication(AppComponent, {
providers: [provideZonelessChangeDetection()],
});Zoneless Component Patterns
Zoneless组件模式
typescript
import { Component, signal, ChangeDetectionStrategy } from "@angular/core";
@Component({
selector: "app-counter",
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div>Count: {{ count() }}</div>
<button (click)="increment()">+</button>
`,
})
export class CounterComponent {
count = signal(0);
increment() {
this.count.update((v) => v + 1);
// No zone.js needed - Signal triggers change detection
}
}typescript
import { Component, signal, ChangeDetectionStrategy } from "@angular/core";
@Component({
selector: "app-counter",
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div>计数: {{ count() }}</div>
<button (click)="increment()">+</button>
`,
})
export class CounterComponent {
count = signal(0);
increment() {
this.count.update((v) => v + 1);
// 无需zone.js - Signal会自动触发变更检测
}
}Key Zoneless Benefits
Zoneless核心优势
- Performance: No zone.js patches on async APIs
- Debugging: Clean stack traces without zone wrappers
- Bundle size: Smaller without zone.js (~15KB savings)
- Interoperability: Better with Web Components and micro-frontends
- 性能:无需zone.js对异步API打补丁
- 调试:无zone.js包装的干净堆栈跟踪
- 包体积:移除zone.js后减少约15KB
- 互操作性:与Web Components和微前端更好兼容
4. Server-Side Rendering & Hydration
4. 服务端渲染(SSR)与水合(Hydration)
SSR Setup with Angular CLI
使用Angular CLI搭建SSR
bash
ng add @angular/ssrbash
ng add @angular/ssrHydration Configuration
水合配置
typescript
// app.config.ts
import { ApplicationConfig } from "@angular/core";
import {
provideClientHydration,
withEventReplay,
} from "@angular/platform-browser";
export const appConfig: ApplicationConfig = {
providers: [provideClientHydration(withEventReplay())],
};typescript
// app.config.ts
import { ApplicationConfig } from "@angular/core";
import {
provideClientHydration,
withEventReplay,
} from "@angular/platform-browser";
export const appConfig: ApplicationConfig = {
providers: [provideClientHydration(withEventReplay())],
};Incremental Hydration (v20+)
增量水合(v20+)
typescript
import { Component } from "@angular/core";
@Component({
selector: "app-page",
standalone: true,
template: `
<app-hero />
@defer (hydrate on viewport) {
<app-comments />
}
@defer (hydrate on interaction) {
<app-chat-widget />
}
`,
})
export class PageComponent {}typescript
import { Component } from "@angular/core";
@Component({
selector: "app-page",
standalone: true,
template: `
<app-hero />
@defer (hydrate on viewport) {
<app-comments />
}
@defer (hydrate on interaction) {
<app-chat-widget />
}
`,
})
export class PageComponent {}Hydration Triggers
水合触发条件
| Trigger | When to Use |
|---|---|
| Low-priority, hydrate when browser idle |
| Hydrate when element enters viewport |
| Hydrate on first user interaction |
| Hydrate when user hovers |
| Hydrate after specified delay |
| 触发方式 | 适用场景 |
|---|---|
| 低优先级内容,浏览器空闲时水合 |
| 元素进入视口时水合 |
| 用户首次交互时水合 |
| 用户悬停时水合 |
| 指定延迟后水合 |
5. Modern Routing Patterns
5. 现代路由模式
Functional Route Guards
函数式路由守卫
typescript
// auth.guard.ts
import { inject } from "@angular/core";
import { Router, CanActivateFn } from "@angular/router";
import { AuthService } from "./auth.service";
export const authGuard: CanActivateFn = (route, state) => {
const auth = inject(AuthService);
const router = inject(Router);
if (auth.isAuthenticated()) {
return true;
}
return router.createUrlTree(["/login"], {
queryParams: { returnUrl: state.url },
});
};
// Usage in routes
export const routes: Routes = [
{
path: "dashboard",
loadComponent: () => import("./dashboard.component"),
canActivate: [authGuard],
},
];typescript
// auth.guard.ts
import { inject } from "@angular/core";
import { Router, CanActivateFn } from "@angular/router";
import { AuthService } from "./auth.service";
export const authGuard: CanActivateFn = (route, state) => {
const auth = inject(AuthService);
const router = inject(Router);
if (auth.isAuthenticated()) {
return true;
}
return router.createUrlTree(["/login"], {
queryParams: { returnUrl: state.url },
});
};
// 在路由中使用
export const routes: Routes = [
{
path: "dashboard",
loadComponent: () => import("./dashboard.component"),
canActivate: [authGuard],
},
];Route-Level Data Resolvers
路由级数据解析器
typescript
import { inject } from '@angular/core';
import { ResolveFn } from '@angular/router';
import { UserService } from './user.service';
import { User } from './user.model';
export const userResolver: ResolveFn<User> = (route) => {
const userService = inject(UserService);
return userService.getUser(route.paramMap.get('id')!);
};
// In routes
{
path: 'user/:id',
loadComponent: () => import('./user.component'),
resolve: { user: userResolver }
}
// In component
export class UserComponent {
private route = inject(ActivatedRoute);
user = toSignal(this.route.data.pipe(map(d => d['user'])));
}typescript
import { inject } from '@angular/core';
import { ResolveFn } from '@angular/router';
import { UserService } from './user.service';
import { User } from './user.model';
export const userResolver: ResolveFn<User> = (route) => {
const userService = inject(UserService);
return userService.getUser(route.paramMap.get('id')!);
};
// 在路由中配置
{
path: 'user/:id',
loadComponent: () => import('./user.component'),
resolve: { user: userResolver }
}
// 在组件中使用
export class UserComponent {
private route = inject(ActivatedRoute);
user = toSignal(this.route.data.pipe(map(d => d['user'])));
}6. Dependency Injection Patterns
6. 依赖注入模式
Modern inject() Function
现代inject()函数
typescript
import { Component, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { UserService } from './user.service';
@Component({...})
export class UserComponent {
// Modern inject() - no constructor needed
private http = inject(HttpClient);
private userService = inject(UserService);
// Works in any injection context
users = toSignal(this.userService.getUsers());
}typescript
import { Component, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { UserService } from './user.service';
@Component({...})
export class UserComponent {
// 现代inject() - 无需构造函数
private http = inject(HttpClient);
private userService = inject(UserService);
// 在任何注入上下文中生效
users = toSignal(this.userService.getUsers());
}Injection Tokens for Configuration
用于配置的注入令牌
typescript
import { InjectionToken, inject } from "@angular/core";
// Define token
export const API_BASE_URL = new InjectionToken<string>("API_BASE_URL");
// Provide in config
bootstrapApplication(AppComponent, {
providers: [{ provide: API_BASE_URL, useValue: "https://api.example.com" }],
});
// Inject in service
@Injectable({ providedIn: "root" })
export class ApiService {
private baseUrl = inject(API_BASE_URL);
get(endpoint: string) {
return this.http.get(`${this.baseUrl}/${endpoint}`);
}
}typescript
import { InjectionToken, inject } from "@angular/core";
// 定义令牌
export const API_BASE_URL = new InjectionToken<string>("API_BASE_URL");
// 在配置中提供
bootstrapApplication(AppComponent, {
providers: [{ provide: API_BASE_URL, useValue: "https://api.example.com" }],
});
// 在服务中注入
@Injectable({ providedIn: "root" })
export class ApiService {
private baseUrl = inject(API_BASE_URL);
get(endpoint: string) {
return this.http.get(`${this.baseUrl}/${endpoint}`);
}
}7. Component Composition & Reusability
7. 组件组合与复用
Content Projection (Slots)
内容投影(插槽)
typescript
@Component({
selector: 'app-card',
template: `
<div class="card">
<div class="header">
<!-- Select by attribute -->
<ng-content select="[card-header]"></ng-content>
</div>
<div class="body">
<!-- Default slot -->
<ng-content></ng-content>
</div>
</div>
`
})
export class CardComponent {}
// Usage
<app-card>
<h3 card-header>Title</h3>
<p>Body content</p>
</app-card>typescript
@Component({
selector: 'app-card',
template: `
<div class="card">
<div class="header">
<!-- 通过属性选择投影内容 -->
<ng-content select="[card-header]"></ng-content>
</div>
<div class="body">
<!-- 默认插槽 -->
<ng-content></ng-content>
</div>
</div>
`
})
export class CardComponent {}
// 使用方式
<app-card>
<h3 card-header>标题</h3>
<p>正文内容</p>
</app-card>Host Directives (Composition)
宿主指令(组合式)
typescript
// Reusable behaviors without inheritance
@Directive({
standalone: true,
selector: '[appTooltip]',
inputs: ['tooltip'] // Signal input alias
})
export class TooltipDirective { ... }
@Component({
selector: 'app-button',
standalone: true,
hostDirectives: [
{
directive: TooltipDirective,
inputs: ['tooltip: title'] // Map input
}
],
template: `<ng-content />`
})
export class ButtonComponent {}typescript
// 可复用行为,无需继承
@Directive({
standalone: true,
selector: '[appTooltip]',
inputs: ['tooltip'] // Signal输入别名
})
export class TooltipDirective { ... }
@Component({
selector: 'app-button',
standalone: true,
hostDirectives: [
{
directive: TooltipDirective,
inputs: ['tooltip: title'] // 映射输入
}
],
template: `<ng-content />`
})
export class ButtonComponent {}8. State Management Patterns
8. 状态管理模式
Signal-Based State Service
基于Signal的状态服务
typescript
import { Injectable, signal, computed } from "@angular/core";
interface AppState {
user: User | null;
theme: "light" | "dark";
notifications: Notification[];
}
@Injectable({ providedIn: "root" })
export class StateService {
// Private writable signals
private _user = signal<User | null>(null);
private _theme = signal<"light" | "dark">("light");
private _notifications = signal<Notification[]>([]);
// Public read-only computed
readonly user = computed(() => this._user());
readonly theme = computed(() => this._theme());
readonly notifications = computed(() => this._notifications());
readonly unreadCount = computed(
() => this._notifications().filter((n) => !n.read).length,
);
// Actions
setUser(user: User | null) {
this._user.set(user);
}
toggleTheme() {
this._theme.update((t) => (t === "light" ? "dark" : "light"));
}
addNotification(notification: Notification) {
this._notifications.update((n) => [...n, notification]);
}
}typescript
import { Injectable, signal, computed } from "@angular/core";
interface AppState {
user: User | null;
theme: "light" | "dark";
notifications: Notification[];
}
@Injectable({ providedIn: "root" })
export class StateService {
// 私有可写Signal
private _user = signal<User | null>(null);
private _theme = signal<"light" | "dark">("light");
private _notifications = signal<Notification[]>([]);
// 公共只读计算Signal
readonly user = computed(() => this._user());
readonly theme = computed(() => this._theme());
readonly notifications = computed(() => this._notifications());
readonly unreadCount = computed(
() => this._notifications().filter((n) => !n.read).length,
);
// 操作方法
setUser(user: User | null) {
this._user.set(user);
}
toggleTheme() {
this._theme.update((t) => (t === "light" ? "dark" : "light"));
}
addNotification(notification: Notification) {
this._notifications.update((n) => [...n, notification]);
}
}Component Store Pattern with Signals
基于Signal的组件Store模式
typescript
import { Injectable, signal, computed, inject } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { toSignal } from "@angular/core/rxjs-interop";
@Injectable()
export class ProductStore {
private http = inject(HttpClient);
// State
private _products = signal<Product[]>([]);
private _loading = signal(false);
private _filter = signal("");
// Selectors
readonly products = computed(() => this._products());
readonly loading = computed(() => this._loading());
readonly filteredProducts = computed(() => {
const filter = this._filter().toLowerCase();
return this._products().filter((p) =>
p.name.toLowerCase().includes(filter),
);
});
// Actions
loadProducts() {
this._loading.set(true);
this.http.get<Product[]>("/api/products").subscribe({
next: (products) => {
this._products.set(products);
this._loading.set(false);
},
error: () => this._loading.set(false),
});
}
setFilter(filter: string) {
this._filter.set(filter);
}
}typescript
import { Injectable, signal, computed, inject } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { toSignal } from "@angular/core/rxjs-interop";
@Injectable()
export class ProductStore {
private http = inject(HttpClient);
// 状态
private _products = signal<Product[]>([]);
private _loading = signal(false);
private _filter = signal("");
// 选择器
readonly products = computed(() => this._products());
readonly loading = computed(() => this._loading());
readonly filteredProducts = computed(() => {
const filter = this._filter().toLowerCase();
return this._products().filter((p) =>
p.name.toLowerCase().includes(filter),
);
});
// 操作方法
loadProducts() {
this._loading.set(true);
this.http.get<Product[]>("/api/products").subscribe({
next: (products) => {
this._products.set(products);
this._loading.set(false);
},
error: () => this._loading.set(false),
});
}
setFilter(filter: string) {
this._filter.set(filter);
}
}9. Forms with Signals (Coming in v22+)
9. 基于Signal的表单(v22+即将推出)
Current Reactive Forms
当前响应式表单
typescript
import { Component, inject } from "@angular/core";
import { FormBuilder, Validators, ReactiveFormsModule } from "@angular/forms";
@Component({
selector: "app-user-form",
standalone: true,
imports: [ReactiveFormsModule],
template: `
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<input formControlName="name" placeholder="Name" />
<input formControlName="email" type="email" placeholder="Email" />
<button [disabled]="form.invalid">Submit</button>
</form>
`,
})
export class UserFormComponent {
private fb = inject(FormBuilder);
form = this.fb.group({
name: ["", Validators.required],
email: ["", [Validators.required, Validators.email]],
});
onSubmit() {
if (this.form.valid) {
console.log(this.form.value);
}
}
}typescript
import { Component, inject } from "@angular/core";
import { FormBuilder, Validators, ReactiveFormsModule } from "@angular/forms";
@Component({
selector: "app-user-form",
standalone: true,
imports: [ReactiveFormsModule],
template: `
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<input formControlName="name" placeholder="姓名" />
<input formControlName="email" type="email" placeholder="邮箱" />
<button [disabled]="form.invalid">提交</button>
</form>
`,
})
export class UserFormComponent {
private fb = inject(FormBuilder);
form = this.fb.group({
name: ["", Validators.required],
email: ["", [Validators.required, Validators.email]],
});
onSubmit() {
if (this.form.valid) {
console.log(this.form.value);
}
}
}Signal-Aware Form Patterns (Preview)
支持Signal的表单模式(预览)
typescript
// Future Signal Forms API (experimental)
import { Component, signal } from '@angular/core';
@Component({...})
export class SignalFormComponent {
name = signal('');
email = signal('');
// Computed validation
isValid = computed(() =>
this.name().length > 0 &&
this.email().includes('@')
);
submit() {
if (this.isValid()) {
console.log({ name: this.name(), email: this.email() });
}
}
}typescript
// 未来Signal Forms API(实验性)
import { Component, signal } from '@angular/core';
@Component({...})
export class SignalFormComponent {
name = signal('');
email = signal('');
// 计算式验证
isValid = computed(() =>
this.name().length > 0 &&
this.email().includes('@')
);
submit() {
if (this.isValid()) {
console.log({ name: this.name(), email: this.email() });
}
}
}10. Performance Optimization
10. 性能优化
Change Detection Strategies
变更检测策略
typescript
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
// Only checks when:
// 1. Input signal/reference changes
// 2. Event handler runs
// 3. Async pipe emits
// 4. Signal value changes
})typescript
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
// 仅在以下情况触发检测:
// 1. 输入Signal/引用变更
// 2. 事件处理器执行
// 3. Async管道发射新值
// 4. Signal值变更
})Defer Blocks for Lazy Loading
延迟加载的Defer块
typescript
@Component({
template: `
<!-- Immediate loading -->
<app-header />
<!-- Lazy load when visible -->
@defer (on viewport) {
<app-heavy-chart />
} @placeholder {
<div class="skeleton" />
} @loading (minimum 200ms) {
<app-spinner />
} @error {
<p>Failed to load chart</p>
}
`
})typescript
@Component({
template: `
<!-- 立即加载 -->
<app-header />
<!-- 可见时懒加载 -->
@defer (on viewport) {
<app-heavy-chart />
} @placeholder {
<div class="skeleton" />
} @loading (minimum 200ms) {
<app-spinner />
} @error {
<p>图表加载失败</p>
}
`
})NgOptimizedImage
NgOptimizedImage
typescript
import { NgOptimizedImage } from '@angular/common';
@Component({
imports: [NgOptimizedImage],
template: `
<img
ngSrc="hero.jpg"
width="800"
height="600"
priority
/>
<img
ngSrc="thumbnail.jpg"
width="200"
height="150"
loading="lazy"
placeholder="blur"
/>
`
})typescript
import { NgOptimizedImage } from '@angular/common';
@Component({
imports: [NgOptimizedImage],
template: `
<img
ngSrc="hero.jpg"
width="800"
height="600"
priority
/>
<img
ngSrc="thumbnail.jpg"
width="200"
height="150"
loading="lazy"
placeholder="blur"
/>
`
})11. Testing Modern Angular
11. 现代Angular测试
Testing Signal Components
测试Signal组件
typescript
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { CounterComponent } from "./counter.component";
describe("CounterComponent", () => {
let component: CounterComponent;
let fixture: ComponentFixture<CounterComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [CounterComponent], // Standalone import
}).compileComponents();
fixture = TestBed.createComponent(CounterComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it("should increment count", () => {
expect(component.count()).toBe(0);
component.increment();
expect(component.count()).toBe(1);
});
it("should update DOM on signal change", () => {
component.count.set(5);
fixture.detectChanges();
const el = fixture.nativeElement.querySelector(".count");
expect(el.textContent).toContain("5");
});
});typescript
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { CounterComponent } from "./counter.component";
describe("CounterComponent", () => {
let component: CounterComponent;
let fixture: ComponentFixture<CounterComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [CounterComponent], // 导入独立组件
}).compileComponents();
fixture = TestBed.createComponent(CounterComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it("应该增加计数", () => {
expect(component.count()).toBe(0);
component.increment();
expect(component.count()).toBe(1);
});
it("Signal变更时应更新DOM", () => {
component.count.set(5);
fixture.detectChanges();
const el = fixture.nativeElement.querySelector(".count");
expect(el.textContent).toContain("5");
});
});Testing with Signal Inputs
测试带Signal输入的组件
typescript
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { ComponentRef } from "@angular/core";
import { UserCardComponent } from "./user-card.component";
describe("UserCardComponent", () => {
let fixture: ComponentFixture<UserCardComponent>;
let componentRef: ComponentRef<UserCardComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [UserCardComponent],
}).compileComponents();
fixture = TestBed.createComponent(UserCardComponent);
componentRef = fixture.componentRef;
// Set signal inputs via setInput
componentRef.setInput("id", "123");
componentRef.setInput("name", "John Doe");
fixture.detectChanges();
});
it("should display user name", () => {
const el = fixture.nativeElement.querySelector("h3");
expect(el.textContent).toContain("John Doe");
});
});typescript
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { ComponentRef } from "@angular/core";
import { UserCardComponent } from "./user-card.component";
describe("UserCardComponent", () => {
let fixture: ComponentFixture<UserCardComponent>;
let componentRef: ComponentRef<UserCardComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [UserCardComponent],
}).compileComponents();
fixture = TestBed.createComponent(UserCardComponent);
componentRef = fixture.componentRef;
// 通过setInput设置Signal输入
componentRef.setInput("id", "123");
componentRef.setInput("name", "John Doe");
fixture.detectChanges();
});
it("应该显示用户名", () => {
const el = fixture.nativeElement.querySelector("h3");
expect(el.textContent).toContain("John Doe");
});
});Best Practices Summary
最佳实践总结
| Pattern | ✅ Do | ❌ Don't |
|---|---|---|
| State | Use Signals for local state | Overuse RxJS for simple state |
| Components | Standalone with direct imports | Bloated SharedModules |
| Change Detection | OnPush + Signals | Default CD everywhere |
| Lazy Loading | | Eager load everything |
| DI | | Constructor injection (verbose) |
| Inputs | | |
| Zoneless | Enable for new projects | Force on legacy without testing |
| 模式 | ✅ 推荐做法 | ❌ 不推荐做法 |
|---|---|---|
| 状态管理 | 本地状态使用Signals | 简单状态过度使用RxJS |
| 组件设计 | 独立组件+直接导入依赖 | 臃肿的SharedModules |
| 变更检测 | OnPush + Signals | 全局使用默认变更检测 |
| 懒加载 | | 全部内容预加载 |
| 依赖注入 | | 冗长的构造函数注入 |
| 输入绑定 | | 传统 |
| Zoneless | 新项目启用 | 未经测试就强制在遗留项目中使用 |
Resources
参考资源
Common Troubleshooting
常见问题排查
| Issue | Solution |
|---|---|
| Signal not updating UI | Ensure |
| Hydration mismatch | Check server/client content consistency |
| Circular dependency | Use |
| Zoneless not detecting changes | Trigger via signal updates, not mutations |
| SSR fetch fails | Use |
| 问题 | 解决方案 |
|---|---|
| Signal未更新UI | 确保使用 |
| 水合不匹配 | 检查服务端与客户端内容一致性 |
| 循环依赖 | 结合 |
| Zoneless模式下变更未被检测 | 通过Signal更新触发,而非直接修改对象 |
| SSR请求失败 | 使用 |