Angular
Angular 专家
掌握现代 Angular 开发,包括 Signals、独立组件、无 Zone 应用、SSR/Hydration 以及最新的响应式模式。
何时使用此技能
- 构建新的 Angular 应用 (v20+)
- 实现基于 Signals 的响应式模式
- 创建独立组件以及从 NgModules 迁移
- 配置无 Zone (Zoneless) 的 Angular 应用
- 实现 SSR、预渲染 (prerendering) 和注水 (hydration)
- 优化 Angular 性能
- 采用现代 Angular 模式和最佳实践
何时不要使用此技能
- 从 AngularJS (1.x) 迁移 $\rightarrow$ 请使用
angular-migration技能
- 处理无法升级的旧版 Angular 应用
- 通用的 TypeScript 问题 $\rightarrow$ 请使用
typescript-expert技能
指导指令
1. 评估 Angular 版本和项目结构
2. 应用现代模式 (Signals, Standalone, Zoneless)
3. 使用正确的类型定义和响应式实现
4. 通过构建和测试进行验证
安全性
- 在投入生产前,务必在开发环境中测试更改
- 对现有应用进行渐进式迁移(避免大规模推倒重构)
- 在过渡期间保持向后兼容性
---
Angular 版本时间线
| 版本 | 发布时间 | 核心特性 |
| -------------- | ------- | ------------------------------------------------------ |
| Angular 20 | 2025 Q2 | Signals 正式稳定, Zoneless 正式稳定, 增量注水 (Incremental hydration) |
| Angular 21 | 2025 Q4 | 默认 Signal-first, 增强型 SSR |
| Angular 22 | 2026 Q2 | Signal Forms, 无选择器组件 (Selectorless components) |
---
1. Signals:全新的响应式原语
Signals 是 Angular 的细粒度响应式系统,旨在取代基于 zone.js 的变更检测。
核心概念
import { signal, computed, effect } from "@angular/core";
// 可写信号 (Writable signal)
const count = signal(0);
// 读取值
console.log(count()); // 0
// 更新值
count.set(5); // 直接设置
count.update((v) => v + 1); // 函数式更新
// 计算信号 (Computed/derived signal)
const doubled = computed(() => count() * 2);
// 效果 (Effect/副作用)
effect(() => {
console.log(Count changed to: ${count()});
});
基于 Signal 的输入与输出
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 输入 (只读)
id = input.required<string>();
name = input.required<string>();
role = input<string>("User"); // 带默认值
// 输出
select = output<string>();
// 双向绑定 (model)
isSelected = model(false);
}
// 使用方式:
// <app-user-card [id]="'123'" [name]="'John'" [(isSelected)]="selected" />
Signal 查询 (ViewChild/ContentChild)
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();
}
}
### 何时使用 Signals vs RxJS
| 使用场景 | Signals | RxJS |
| ----------------------- | --------------- | -------------------------------- |
| 组件本地状态 | ✅ 推荐 | 过度设计 |
| 派生/计算值 | ✅ computed() | combineLatest 可实现 |
| 副作用 | ✅ effect() | tap 操作符 |
| HTTP 请求 | ❌ | ✅ HttpClient 返回 Observable |
| 事件流 | ❌ | ✅ fromEvent 及相关操作符 |
| 复杂异步流 | ❌ | ✅ switchMap, mergeMap |
---
2. 独立组件 (Standalone Components)
独立组件是自包含的,不需要在 NgModule 中声明。
创建独立组件
@Component({
selector: "app-header",
standalone: true,
imports: [CommonModule, RouterLink], // 直接导入
template: ,
<header>
<a routerLink="/">Home</a>
<a routerLink="/about">About</a>
</header>
})
export class HeaderComponent {}
### 无 NgModule 引导启动// 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()],
});
### 独立组件的懒加载// 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. 无 Zone 的 Angular (Zoneless Angular)
Zoneless 应用不使用 zone.js,从而提升性能并简化调试。
启用 Zoneless 模式
bootstrapApplication(AppComponent, {
providers: [provideZonelessChangeDetection()],
});
### Zoneless 组件模式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);
// 无需 zone.js - Signal 会触发变更检测
}
}
### Zoneless 的核心优势
- 性能:异步 API 不再被 zone.js 拦截(patch)
- 调试:堆栈跟踪更简洁,没有 zone 包装层
- 包体积:移除 zone.js 后体积更小(节省约 15KB)
- 互操作性
可扩展性:结合 Web Components 和微前端效果更佳
---
4. 服务端渲染 (SSR) 与注水 (Hydration)
使用 Angular CLI 设置 SSR
### 注水配置export const appConfig: ApplicationConfig = {
providers: [provideClientHydration(withEventReplay())],
};
### 渐进式注水 (v20+)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 {}
### 注水触发条件
| 触发条件 | 使用场景 |
| :--- | :--- |
| on idle | 低优先级,在浏览器空闲时注水 |
| on viewport | 当元素进入视口时注水 |
| on interaction | 在用户首次交互时注水 |
| on hover | 当用户悬停时注水 |
| on timer(ms) | 在指定延迟后注水 |
---
5. 现代路由模式
函数式路由守卫 (Functional Route Guards)
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)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. 依赖注入 (DI) 模式
现代 inject() 函数
@Component({...})
export class UserComponent {
// 现代 inject() 方式 - 无需构造函数
private http = inject(HttpClient);
private userService = inject(UserService);
// 可在任何注入上下文中工作
users = toSignal(this.userService.getUsers());
}
### 用于配置的注入令牌 (Injection Tokens)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. 组件组合与复用
内容投影 (Slots)
@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>
宿主指令 (Composition)
// 无需继承即可实现可复用行为
@Directive({
standalone: true,
selector: '[appTooltip]',
inputs: ['tooltip'] // Signal input 别名
})
export class TooltipDirective { ... }
@Component({
selector: 'app-button',
standalone: true,
hostDirectives: [
{
directive: TooltipDirective,
inputs: ['tooltip: title'] // 映射输入
}
],
template: <ng-content />
})
export class ButtonComponent {}
---
8. 状态管理模式
基于 Signal 的状态服务
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[]>([]);
// 公开只读 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,
);
// 操作方法
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]);
}
}
基于 Signals 的组件 Store 模式
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("");
// 选择器 (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),
);
});
// 操作方法
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. 基于 Signals 的表单 (即将于 v
22+)当前的响应式表单 (Reactive Forms)
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);
}
}
}
Signal 感知表单模式 (预览)
// 未来的 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. 性能优化
变更检测策略 (Change Detection Strategies)
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
// 仅在以下情况检查:
// 1. Input signal/引用发生变化
// 2. 事件处理器运行
// 3. Async 管道发出值
// 4. Signal 值发生变化
})用于延迟加载的 Defer 块
@Component({
template:
<!-- 立即加载 -->
<app-header />
<!-- 进入视口时延迟加载 -->
@defer (on viewport) {
<app-heavy-chart />
} @placeholder {
<div class="skeleton" />
} @loading (minimum 200ms) {
<app-spinner />
} @error {
<p>Failed to load chart</p>
}
})NgOptimizedImage
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. 测试现代 Angular
测试 Signal 组件
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("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");
});
});
使用 Signal Inputs 进行测试
import { ComponentFixture, TestBed } from "@angular/core/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 inputs
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");
});
});
``
---
最佳实践总结
| 模式 | ✅ 推荐做法 | ❌ 避免做法 |
| -------------------- | ------------------------------ | ------------------------------- |
| 状态管理 | 使用 Signals 处理本地状态 | 对简单状态过度使用 RxJS |
| 组件 | 使用 Standalone 组件并直接导入 | 使用臃肿的 SharedModules |
| 变更检测 | OnPush + Signals | 到处使用默认变更检测 (Default CD) |
| 懒加载 | 使用 @defer 和 loadComponent | 全部采用预加载 (Eager load) |inject()
| 依赖注入 (DI) | 使用 函数 | 使用构造函数注入(过于冗长) |input()
| 输入 (Inputs) | 使用 signal 函数 | 使用 @Input() 装饰器(旧版) |
| Zoneless | 在新项目中启用 | 在未测试的情况下强行应用于旧项目 |
---
相关资源
---
常见问题排查
| 问题 | 解决方案 |
| ------------------------------ | --------------------------------------------------- |
| Signal 未更新 UI | 确保使用 OnPush 且以函数形式调用 signal,如 count() |inject()
| 水合 (Hydration) 不匹配 | 检查服务端与客户端内容的的一致性 |
| 循环依赖 | 在 中配合 forwardRef 使用 |TransferState
| Zoneless 未检测到变更 | 通过 signal 更新触发,而非直接修改对象属性 |
| SSR fetch 失败 | 使用 或 withFetch()` |
局限性
- 仅在任务明确符合上述范围时使用此技能。
- 不要将输出结果视为环境特定验证、测试或专家评审的替代方案。
- 如果缺少必要的输入、权限、安全边界或验收标准,请停止并请求澄清。