angular

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Angular 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
    angular-migration
    skill
  • Working with legacy Angular apps that cannot upgrade
  • General TypeScript issues → use
    typescript-expert
    skill
  • 从AngularJS(1.x)迁移 → 使用
    angular-migration
    技能
  • 处理无法升级的遗留Angular应用
  • 通用TypeScript问题 → 使用
    typescript-expert
    技能

Instructions

操作说明

  1. Assess the Angular version and project structure
  2. Apply modern patterns (Signals, Standalone, Zoneless)
  3. Implement with proper typing and reactivity
  4. Validate with build and tests
  1. 评估Angular版本和项目结构
  2. 应用现代模式(Signals、Standalone、Zoneless)
  3. 结合正确的类型定义和响应式特性实现功能
  4. 通过构建和测试验证成果

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版本时间线

VersionReleaseKey Features
Angular 20Q2 2025Signals stable, Zoneless stable, Incremental hydration
Angular 21Q4 2025Signals-first default, Enhanced SSR
Angular 22Q2 2026Signal Forms, Selectorless components

版本发布时间核心特性
Angular 202025年第二季度Signals稳定版、Zoneless稳定版、增量水合
Angular 212025年第四季度默认Signals优先、增强型SSR
Angular 222026年第二季度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 CaseSignalsRxJS
Local component state✅ PreferredOverkill
Derived/computed values
computed()
combineLatest
works
Side effects
effect()
tap
operator
HTTP requests✅ HttpClient returns Observable
Event streams
fromEvent
, operators
Complex async flows
switchMap
,
mergeMap

场景SignalsRxJS
组件本地状态✅ 优先使用大材小用
派生/计算值
computed()
combineLatest
可实现
副作用
effect()
tap
操作符
HTTP请求✅ HttpClient返回Observable
事件流
fromEvent
及操作符
复杂异步流
switchMap
,
mergeMap

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/ssr
bash
ng add @angular/ssr

Hydration 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

水合触发条件

TriggerWhen to Use
on idle
Low-priority, hydrate when browser idle
on viewport
Hydrate when element enters viewport
on interaction
Hydrate on first user interaction
on hover
Hydrate when user hovers
on timer(ms)
Hydrate after specified delay

触发方式适用场景
on idle
低优先级内容,浏览器空闲时水合
on viewport
元素进入视口时水合
on interaction
用户首次交互时水合
on hover
用户悬停时水合
on timer(ms)
指定延迟后水合

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
StateUse Signals for local stateOveruse RxJS for simple state
ComponentsStandalone with direct importsBloated SharedModules
Change DetectionOnPush + SignalsDefault CD everywhere
Lazy Loading
@defer
and
loadComponent
Eager load everything
DI
inject()
function
Constructor injection (verbose)
Inputs
input()
signal function
@Input()
decorator (legacy)
ZonelessEnable for new projectsForce on legacy without testing

模式✅ 推荐做法❌ 不推荐做法
状态管理本地状态使用Signals简单状态过度使用RxJS
组件设计独立组件+直接导入依赖臃肿的SharedModules
变更检测OnPush + Signals全局使用默认变更检测
懒加载
@defer
loadComponent
全部内容预加载
依赖注入
inject()
函数
冗长的构造函数注入
输入绑定
input()
Signal函数
传统
@Input()
装饰器
Zoneless新项目启用未经测试就强制在遗留项目中使用

Resources

参考资源

Common Troubleshooting

常见问题排查

IssueSolution
Signal not updating UIEnsure
OnPush
+ call signal as function
count()
Hydration mismatchCheck server/client content consistency
Circular dependencyUse
inject()
with
forwardRef
Zoneless not detecting changesTrigger via signal updates, not mutations
SSR fetch failsUse
TransferState
or
withFetch()
问题解决方案
Signal未更新UI确保使用
OnPush
策略,且以函数形式调用Signal如
count()
水合不匹配检查服务端与客户端内容一致性
循环依赖结合
inject()
forwardRef
使用
Zoneless模式下变更未被检测通过Signal更新触发,而非直接修改对象
SSR请求失败使用
TransferState
withFetch()