Angular 17 新特性深度解析:独立组件 + 信号系统实战
概览
- 独立组件(Standalone Components):不再依赖 NgModule,应用可直接通过组件与功能提供者组织
- 信号系统(Signals):以可追踪的状态源替代传统变更检测触发方式,提供
signal/computed/effect、input/output/model等能力 - 工程收益:更简洁的项目结构、更强的类型推断、可预测的状态更新与更小的包体
独立组件(Standalone)
- 核心点:组件、指令与管道可声明为
standalone: true,直接在imports中引用;应用入口使用bootstrapApplication
main.ts:
ts
import { bootstrapApplication } from '@angular/platform-browser'
import { provideRouter } from '@angular/router'
import { AppComponent } from './app/app.component'
import { routes } from './app/app.routes'
bootstrapApplication(AppComponent, {
providers: [provideRouter(routes)]
})
app.component.ts:
ts
import { Component } from '@angular/core'
import { RouterLink } from '@angular/router'
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterLink],
template: `
<h1>Angular 17 Standalone + Signals</h1>
<a routerLink="/counter">Counter</a>
`
})
export class AppComponent {}
app.routes.ts:
ts
import { Routes } from '@angular/router'
import { CounterComponent } from './counter.component'
export const routes: Routes = [
{ path: 'counter', loadComponent: () => import('./counter.component').then(m => m.CounterComponent) },
{ path: '', pathMatch: 'full', redirectTo: 'counter' }
]
- 特性:路由支持
loadComponent懒加载组件;无需 NgModule 汇总
依赖注入与提供者
- 提供者与 DI 更简化:在组件或入口通过
providers注入;在任意位置使用inject()获取服务
counter.service.ts:
ts
import { Injectable, signal, computed } from '@angular/core'
@Injectable({ providedIn: 'root' })
export class CounterService {
readonly count = signal(0)
readonly double = computed(() => this.count() * 2)
inc() { this.count.update(n => n + 1) }
reset() { this.count.set(0) }
}
counter.component.ts:
ts
import { Component, effect, inject } from '@angular/core'
import { CounterService } from './counter.service'
@Component({
standalone: true,
selector: 'app-counter',
template: `
<button (click)="svc.inc()">Count: {{ svc.count() }}</button>
<p>Double: {{ svc.double() }}</p>
`
})
export class CounterComponent {
svc = inject(CounterService)
constructor() {
effect(() => {
console.log('count changed:', this.svc.count())
})
}
}
信号系统(Signals)基础
signal<T>(initial):可读/可写状态源,读取用调用signal(),更新用set/updatecomputed(fn):派生信号,依赖源变化时重新计算effect(fn):副作用,追踪依赖并在变化时执行,支持清理逻辑
信号 API:
ts
import { signal, computed, effect } from '@angular/core'
const count = signal(0)
const double = computed(() => count() * 2)
const stop = effect(onCleanup => {
// 使用依赖
double()
// 清理
onCleanup(() => console.log('cleanup'))
})
count.set(1)
stop()
输入/输出与模型信号(组件边界)
input():声明输入为信号,自动追踪父组件传值变化output():事件输出为信号的写端,保持类型安全model():双向绑定的模型信号,对应[(model)]或[(value)]
child.component.ts:
ts
import { Component, input, output, model } from '@angular/core'
@Component({
selector: 'app-child',
standalone: true,
template: `
<h3>{{ title() }}</h3>
<button (click)="changed.emit(count())">Emit</button>
<button (click)="count.update(n => n + 1)">+1</button>
`
})
export class ChildComponent {
readonly title = input<string>('默认标题')
readonly changed = output<number>()
readonly count = model<number>(0)
}
父组件:
ts
@Component({ standalone: true, template: `
<app-child [title]="name" [(count)]="value" (changed)="onChanged($event)"></app-child>
` })
export class ParentComponent {
name = 'Demo'
value = 0
onChanged(v:number){ console.log(v) }
}
与 RxJS 互操作
- 在
@angular/core/rxjs-interop中提供toSignal/toObservable
ts
import { toSignal, toObservable } from '@angular/core/rxjs-interop'
import { interval, map } from 'rxjs'
const tick$ = interval(1000).pipe(map(n => n))
const tick = toSignal(tick$, { initialValue: 0 })
const tick$2 = toObservable(signal(42))
- 用途:与现有 RxJS 管道/服务融合,逐步迁移到 Signals 推模式
路由与懒加载(无 NgModule)
- 使用
provideRouter+loadComponent,组件直接懒加载 - 可结合
canActivate、canMatch守卫函数,返回布尔或可观察对象
ts
export const routes: Routes = [
{ path: 'dashboard', loadComponent: () => import('./dashboard.component').then(m => m.DashboardComponent), canActivate: [() => true] }
]
表单与模板中的信号
- 模板中访问信号值使用调用形式:
{``{ signal() }} - 与表单结合:模型信号
model()配合表单控件的[(ngModel)]或 Reactive Forms 的valueChanges互转
性能与变更检测
- 信号驱动的更新可在无 Zone 的模式下工作(实验:
provideExperimentalZoneless),减少全局变更检测开销 - 局部更新:信号依赖图精确定位需要刷新的组件树
迁移与落地建议
- 渐进迁移:先改入口到
bootstrapApplication与provideRouter,再将核心页面改为standalone - 服务层迁移:将共享状态改为
signal与computed,结合effect做副作用 - 组件边界:使用
input/output/model信号统一输入输出协议,替换@Input/@Output旧写法 - 与 RxJS 融合:对复杂数据流保留 RxJS,逐步在 UI 层用
toSignal
常见坑与修复
- 在模板中忘记以调用形式读取信号:应使用
count()而非count - effect 未清理导致内存泄漏:在组件销毁时自动清理,但自定义停止时要调用
stop - 混用
@Input()与input():建议统一使用信号版,减少同步问题 - 懒加载组件未声明
standalone: true:会导致路由加载失败
测试与诊断
- 组件测试:使用
TestBed.configureTestingModule({ imports: [Component] })挂载独立组件 - 信号测试:直接调用
set/update并断言模板渲染或副作用触发 - 性能诊断:Profiler + 变更检测开销对比,记录信号驱动的局部更新收益
总结
- Angular 17 的独立组件与信号系统让应用结构更轻、更快、更可维护
- 通过
bootstrapApplication、standalone、signal/computed/effect与input/output/model,可构建更现代的前端架构 - 与 RxJS 和路由懒加载配合,逐步完成从 NgModule/Zone 时代到推模式状态管理的迁移
Angular 17 新特性深度解析:独立组件 + 信号系统实战
概览
- 独立组件(Standalone Components):不再依赖 NgModule,应用可直接通过组件与功能提供者组织
- 信号系统(Signals):以可追踪的状态源替代传统变更检测触发方式,提供
signal/computed/effect、input/output/model等能力 - 工程收益:更简洁的项目结构、更强的类型推断、可预测的状态更新与更小的包体
独立组件(Standalone)
- 核心点:组件、指令与管道可声明为
standalone: true,直接在imports中引用;应用入口使用bootstrapApplication
main.ts:
ts
import { bootstrapApplication } from '@angular/platform-browser'
import { provideRouter } from '@angular/router'
import { AppComponent } from './app/app.component'
import { routes } from './app/app.routes'
bootstrapApplication(AppComponent, {
providers: [provideRouter(routes)]
})
app.component.ts:
ts
import { Component } from '@angular/core'
import { RouterLink } from '@angular/router'
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterLink],
template: `
<h1>Angular 17 Standalone + Signals</h1>
<a routerLink="/counter">Counter</a>
`
})
export class AppComponent {}
app.routes.ts:
ts
import { Routes } from '@angular/router'
import { CounterComponent } from './counter.component'
export const routes: Routes = [
{ path: 'counter', loadComponent: () => import('./counter.component').then(m => m.CounterComponent) },
{ path: '', pathMatch: 'full', redirectTo: 'counter' }
]
- 特性:路由支持
loadComponent懒加载组件;无需 NgModule 汇总
依赖注入与提供者
- 提供者与 DI 更简化:在组件或入口通过
providers注入;在任意位置使用inject()获取服务
counter.service.ts:
ts
import { Injectable, signal, computed } from '@angular/core'
@Injectable({ providedIn: 'root' })
export class CounterService {
readonly count = signal(0)
readonly double = computed(() => this.count() * 2)
inc() { this.count.update(n => n + 1) }
reset() { this.count.set(0) }
}
counter.component.ts:
ts
import { Component, effect, inject } from '@angular/core'
import { CounterService } from './counter.service'
@Component({
standalone: true,
selector: 'app-counter',
template: `
<button (click)="svc.inc()">Count: {{ svc.count() }}</button>
<p>Double: {{ svc.double() }}</p>
`
})
export class CounterComponent {
svc = inject(CounterService)
constructor() {
effect(() => {
console.log('count changed:', this.svc.count())
})
}
}
信号系统(Signals)基础
signal<T>(initial):可读/可写状态源,读取用调用signal(),更新用set/updatecomputed(fn):派生信号,依赖源变化时重新计算effect(fn):副作用,追踪依赖并在变化时执行,支持清理逻辑
信号 API:
ts
import { signal, computed, effect } from '@angular/core'
const count = signal(0)
const double = computed(() => count() * 2)
const stop = effect(onCleanup => {
// 使用依赖
double()
// 清理
onCleanup(() => console.log('cleanup'))
})
count.set(1)
stop()
输入/输出与模型信号(组件边界)
input():声明输入为信号,自动追踪父组件传值变化output():事件输出为信号的写端,保持类型安全model():双向绑定的模型信号,对应[(model)]或[(value)]
child.component.ts:
ts
import { Component, input, output, model } from '@angular/core'
@Component({
selector: 'app-child',
standalone: true,
template: `
<h3>{{ title() }}</h3>
<button (click)="changed.emit(count())">Emit</button>
<button (click)="count.update(n => n + 1)">+1</button>
`
})
export class ChildComponent {
readonly title = input<string>('默认标题')
readonly changed = output<number>()
readonly count = model<number>(0)
}
父组件:
ts
@Component({ standalone: true, template: `
<app-child [title]="name" [(count)]="value" (changed)="onChanged($event)"></app-child>
` })
export class ParentComponent {
name = 'Demo'
value = 0
onChanged(v:number){ console.log(v) }
}
与 RxJS 互操作
- 在
@angular/core/rxjs-interop中提供toSignal/toObservable
ts
import { toSignal, toObservable } from '@angular/core/rxjs-interop'
import { interval, map } from 'rxjs'
const tick$ = interval(1000).pipe(map(n => n))
const tick = toSignal(tick$, { initialValue: 0 })
const tick$2 = toObservable(signal(42))
- 用途:与现有 RxJS 管道/服务融合,逐步迁移到 Signals 推模式
路由与懒加载(无 NgModule)
- 使用
provideRouter+loadComponent,组件直接懒加载 - 可结合
canActivate、canMatch守卫函数,返回布尔或可观察对象
ts
export const routes: Routes = [
{ path: 'dashboard', loadComponent: () => import('./dashboard.component').then(m => m.DashboardComponent), canActivate: [() => true] }
]
表单与模板中的信号
- 模板中访问信号值使用调用形式:
{``{ signal() }} - 与表单结合:模型信号
model()配合表单控件的[(ngModel)]或 Reactive Forms 的valueChanges互转
性能与变更检测
- 信号驱动的更新可在无 Zone 的模式下工作(实验:
provideExperimentalZoneless),减少全局变更检测开销 - 局部更新:信号依赖图精确定位需要刷新的组件树
迁移与落地建议
- 渐进迁移:先改入口到
bootstrapApplication与provideRouter,再将核心页面改为standalone - 服务层迁移:将共享状态改为
signal与computed,结合effect做副作用 - 组件边界:使用
input/output/model信号统一输入输出协议,替换@Input/@Output旧写法 - 与 RxJS 融合:对复杂数据流保留 RxJS,逐步在 UI 层用
toSignal
常见坑与修复
- 在模板中忘记以调用形式读取信号:应使用
count()而非count - effect 未清理导致内存泄漏:在组件销毁时自动清理,但自定义停止时要调用
stop - 混用
@Input()与input():建议统一使用信号版,减少同步问题 - 懒加载组件未声明
standalone: true:会导致路由加载失败
测试与诊断
- 组件测试:使用
TestBed.configureTestingModule({ imports: [Component] })挂载独立组件 - 信号测试:直接调用
set/update并断言模板渲染或副作用触发 - 性能诊断:Profiler + 变更检测开销对比,记录信号驱动的局部更新收益
总结
- Angular 17 的独立组件与信号系统让应用结构更轻、更快、更可维护
- 通过
bootstrapApplication、standalone、signal/computed/effect与input/output/model,可构建更现代的前端架构 - 与 RxJS 和路由懒加载配合,逐步完成从 NgModule/Zone 时代到推模式状态管理的迁移