Angular
本文基于 Angular 22(2026-06-03 发布的最新稳定版,官方 24 个月支持窗口内 Active)讲解现代 Angular 的完整形态:standalone 组件 + 信号(signals)+ 可选 Zoneless 的新范式,以及沿用至今的依赖注入与 RxJS 生态。 核心信号 API 示例已用
@angular/core@22.1.4在 Node v20.20.1 实测;模板/路由等浏览器侧代码标注适用版本并给出可直接运行的最小工程。
全景:全家桶框架 vs 库
React/Vue 是「库 + 社区拼装」:状态、路由、请求、表单、测试各选其道,由你决定取舍。Angular 是完整框架:CLI、组件化、依赖注入、路由、HTTP、表单、响应式、测试工具全部内建且版本一致——换来的代价是概念多、学习曲线陡;换来的是大型团队/大型应用的统一约束。
| 维度 | Angular | React | Vue |
|---|---|---|---|
| 定位 | 全家桶框架 | 视图库 | 渐进式框架 |
| 响应式模型 | 信号 +(传统)Zone.js 脏检查 | 手动 setState / useSyncExternalStore | 响应式代理 / ref |
| 样式作用域 | 组件内 :host 与封装 | CSS-in-JS / CSS Modules | SFC scoped |
| 变更检测 | 框架调度(含 Zoneless) | 组件树重渲染 | 组件级响应追踪 |
| 内置 | CLI、DI、路由、HTTP、表单、i18n、测试 | 需自行拼装 | 官方 Router/Pinia/Vite |
| TypeScript | 一等公民(AngularJS 是 JS,Angular 2+ 必须 TS) | 推荐但可选 | 推荐但可选 |
一句话定位:选择 Angular 等于接受一整套「框架替你决策」的约束——这套约束在 Google 内部支撑了 Gmail、Google Drive 等超大型应用。
版本演进速览(每个大版本一次语义升级)
| 版本 | 时间 | 关键变化 |
|---|---|---|
| AngularJS 1.x | 2010– | 前端 MV* 先驱,与 2+ 完全重写、不兼容(已 EOL) |
| v2–v8 | 2016–2019 | TS + 组件化 + Zone.js + 依赖注入;稳定演进 |
| v9 | 2020 | Ivy 编译与渲染管线成为默认 |
| v14 | 2022 | Standalone 组件(Developer Preview);NgModule 开始退场 |
| v17 | 2023-11 | Standalone 成为默认;新控制流 @if/@for/@switch 稳定 |
| v19 | 2024-11 | 信号(signals)稳定:input/output/model/computed/effect |
| v18→v21 | 2024–2025 | Zoneless 从实验到新项目默认;v21 起 Zone.js 退出默认脚手架 |
| v22 | 2026-06 | 当前 Active;Signal Forms、Angular Aria 稳定 |
Angular 自 v22 起改为每 12 个月一个大版本(此前每 6 个月);每个大版本维护 24 个月(12 个月 Active + 12 个月 LTS),可跨一个 major 用 ng update 自动迁移。
现代 Angular 四支柱
组件化 + 依赖注入 + 服务层 + 响应式(信号与 RxJS),再加内建路由/表单/HTTP。历史上「模块(NgModule)」曾包裹组件并组织 DI,v14+ 起 standalone 组件自带依赖声明、不再需要 NgModule 外壳——新项目生成的 app.ts 完全没有 NgModule。
快速上手(Standalone 最小工程)
npx -y @angular/cli@22 new my-app --style=css --standalone # standalone 已是默认
cd my-app && ng serve # http://localhost:4200组件即「类 + 元数据 + 模板」:
// src/app/app.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-root', // 模板里用 <app-root> 引用
standalone: true, // v17+ 默认 true,可省略
template: `
<h1>{{ title }}</h1>
<button (click)="count.set(count() + 1)">count = {{ count() }}</button>
`,
})
export class AppComponent {
title = 'Hello Angular';
count = signal(0); // 信号字段(见「信号与变更检测」)
}应用引导(不需要 NgModule,不需要 bootstrapModule):
// src/main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent); // 可选第二个参数 provideAppConfig(...)要点:selector 名唯一;类字段直接可在模板插值中使用;事件 (click) 绑方法或表达式;方法内更新信号即触发重渲染——组件不需要手动 setState。
组件与模板
数据绑定的四种形态
| 语法 | 方向 | 示例 | 说明 |
|---|---|---|---|
{{ expr }} | 单向(数据→视图) | {{ user.name }} | 插值,也可用于方法返回值 |
[prop]="expr" | 单向 | [disabled]="!valid" | 属性绑定,与 DOM 属性而非 HTML 属性绑定 |
(event)="stmt" | 单向(视图→数据) | (click)="save()" | 事件绑定;$event 可取事件对象 |
[(model)]="x" | 双向 | [(ngModel)]="name" | 语法糖:[x] + (xChange);信号用 [(model)] 或 [(x)] |
事件传值:(input)="onInput($event.target.value)";模板局部变量 #input + (keyup)="onEnter(input.value)"。
控制流块(v17+,替代 *ngIf / *ngFor / *ngSwitch)
<!-- 条件 -->
@if (user; as u) { <!-- as 别名避免重复读取 -->
<p>Hello {{ u.name }}</p>
} @else if (guest) {
<p>游客</p>
} @else {
<p>未登录</p>
}
<!-- 列表 -->
@for (item of items; track item.id; let i = $index) {
<li>{{ i }}: {{ item.title }}</li>
} @empty { <!-- 空列表兜底,替代 *ngFor 的 ng-template 怪招 -->
<li>暂无数据</li>
}
<!-- 分支 -->
@switch (status) {
@case ('loading') { <span>加载中…</span> }
@default { <span>{{ status }}</span> }
}track(对应 *ngFor 的 :key)给每行稳定标识,必须提供——否则列表增删会引发整段 DOM 重建;@for 还自带 $index/$first/$last/$even 等上下文变量。
组件间通信:input / output / model(v19+ 信号 API 稳定)
// child.ts
import { Component, input, output, model } from '@angular/core';
@Component({
selector: 'app-counter',
standalone: true,
template: `
<button (click)="count.update(v => v + 1)">+1</button>
<button (click)="reset.emit()">重置</button>
`,
})
export class CounterComponent {
count = input(0); // 父传子:只读信号 input
min = input(0, { alias: 'minVal' }); // 别名:父用 [minVal]
countChange = output<number>(); // 子传父:事件发射器(信号化封装)
reset = output();
value = model(0); // 双向:父可 [(value)]
}父组件:
<app-counter [count]="base" (countChange)="onCount($event)" (reset)="resetAll()" [(value)]="n" />input() 返回值是只读信号:子组件读 this.count() 即可拿到最新值;output() 在模板里用 (事件名) 订阅;model() 是可双向写回的信号——内部 model.set(...) 会自动向外发 valueChange。
生命周期与副作用
| 钩子 | 时机 | 常见用途 |
|---|---|---|
constructor | 实例创建(无输入) | 注入服务 |
ngOnInit | 首次输入绑定完成 | 首次取数据 |
ngOnChanges | 每次输入变化 | 依赖输入的派生逻辑(新代码优先用 computed) |
ngAfterViewInit | 视图就绪 | 操作第三方 DOM 库 |
ngOnDestroy | 销毁前 | 取消订阅、清理定时器 |
信号化写法优先:输入派生值用 computed(() => this.x() + 1),模板自动更新;effect(() => ...) 在依赖变化时执行副作用(类似 watchEffect),默认在变更检测期间运行,不要在 effect 里写信号(会死循环/触发额外检测)。
@defer:按需加载的块
@defer (on viewport) { <!-- 滚动进入视口才加载 -->
<app-heavy-chart />
} @placeholder { <p>图表加载中…</p> }
@loading (minimum 500ms) { <p>仍在加载…</p> }on viewport / on idle / on timer(2s) / on interaction 等触发条件;块内组件自动被代码分割成独立 chunk——Angular 内建懒加载到模板级。
信号与变更检测
为什么需要信号
传统 Angular 用 Zone.js 给浏览器的宏任务、微任务、DOM 事件、XHR 打补丁:任何异步发生都通知 Angular「可能变了」,于是从根组件向下遍历整棵组件树做脏检查(每次点击都全树跑一遍)。配合 OnPush 只能粗粒度跳过子树,本质是「全局广播」。
信号(signal) 把依赖关系变成细粒度图:computed 只知道自己读了哪些信号;信号变化时只通知直接依赖者,视图更新只重算受影响的部分——这同时也让 Zoneless(不依赖 Zone.js 打补丁)成为可能:变化由信号/事件显式通知框架,而非全局拦截。
信号核心 API(Node 实测输出)
// 编译运行:node(@angular/core@22.1.4,信号 API 与平台无关,Node 可直接跑)
const { signal, computed, untracked } = require('@angular/core')
const count = signal(0)
count.set(2) // 赋值:set(新值)
count.update(v => v + 1) // 基于旧值更新:update(fn)
const arr = signal([1, 2, 3])
arr.update(a => [...a, 4]) // 不可变更新:返回新数组(见「踩坑」mutate)
console.log(arr()) // [1, 2, 3, 4]
const doubled = computed(() => count() * 2) // 派生值,读时惰性计算
console.log(count(), doubled()) // 3 6
count.set(10)
console.log(count(), doubled()) // 10 20:依赖变化后自动重算
let recompute = 0
const source = signal(1)
const c2 = computed(() => { recompute++; return source() * 2 })
console.log(c2(), recompute) // 2 1:首次读取才算
c2() // 缓存命中
source.set(2) // 依赖变了,但还没人读
console.log(recompute) // 1:惰性——不读就不算
console.log(c2(), recompute) // 4 2:读取时重算一次
source.set(2) // 同值(默认 === 相等)
c2()
console.log(recompute) // 2:同值不通知,不触发重算
const flag = signal('a')
let evals = 0
const mix = computed(() => { evals++; return count() + untracked(() => flag().length) })
console.log(mix(), evals) // 11 1:10 + 'a'.length = 1
flag.set('abcdef') // flag 未被追踪……
console.log(mix(), evals) // 11 1:mix 不因 flag 失效(缓存旧值)三条行为即信号的全部心智模型:
- 惰性:
computed首次被读才计算,之后缓存;依赖变脏只做标记,下一次读取才重算; - 依赖追踪:
computed求值时读取过的信号成为依赖;untracked(() => ...)读信号但不建立依赖; - 同值不通知:
set相同值(默认===判断,可用signal(0, { equal })自定义)不会使依赖失效——避免无谓重算。
模板中信号直接调用取值:{{ count() }};写 count.set(...) 或 update 后由框架调度更新。
变更检测策略
- Default:所有组件按需检查(信号驱动下只检查受影响组件);
ChangeDetectionStrategy.OnPush:仅当输入引用变化 / 组件内事件 / 信号变化时检查——现代 Angular 默认思想接近它;- Zoneless:v18 起
provideZonelessChangeDetection()实验性启用,v21+ 新项目默认开启、不再引入 Zone.js(迁移老项目时第三方非信号回调可能不再触发检测,见「踩坑」)。手动兜底:ChangeDetectorRef.markForCheck()。
与 RxJS 互通
import { toSignal, toObservable } from '@angular/core/rxjs-interop';
const user = toSignal(http.get<User>('/api/user')); // Observable → 信号(模板直接 user())
const obs$ = toObservable(someSignal); // 信号 → Observable(与 RxJS 流合并)依赖注入:框架的灵魂
依赖注入(DI)让服务「声明要什么就注入什么」,调用方与实现解耦;这是 Angular 与 React/Vue 最大的架构性差异。
服务与注入器树
// user.service.ts —— providedIn 决定注入器层级
import { Injectable } from '@angular/core';
@Injectable({ providedIn: 'root' }) // root:全局单例,随处可注入
export class UserService {
async fetch(id: number) { /* fetch 用户 */ }
}注入器构成层级树:根注入器 → 路由/懒加载注入器 → 组件级注入器。可见性规则:下层可注入上层提供的服务,反之不可。若某服务只想在组件及其子树内共享,写进组件 providers:
@Component({
standalone: true,
providers: [CartService], // 每个组件实例一份新实例
})
export class ProductCardComponent {}providedIn: 'root' 与组件 providers 的区别即「全局单例 vs 按组件作用域隔离」——状态共享/隔离的经典坑位(见「踩坑」)。
注入方式:构造器注入与 inject()
import { inject } from '@angular/core';
@Component({ ... })
export class ProductsComponent {
// 传统:构造函数参数注入
constructor(private user: UserService) {}
// 现代(函数式,可在 guard/interceptor 等任意 context 用):
private http = inject(HttpClient);
}同一次注入在构造器与 inject() 中不可混用(Angular 会报 NG0203),选一种即可;inject() 只能在注入上下文(构造、字段初始化、provideXxx 工厂)里调用。
提供者 token 的四种写法与 InjectionToken
import { InjectionToken, inject } from '@angular/core';
// 无类可依时用 InjectionToken 描述依赖
export const API_BASE = new InjectionToken<string>('API base URL');
// 同一个 token 可给出不同提供方式:
providers: [
UserService, // 等价 useClass:类即 token
{ provide: API_BASE, useValue: '/api/v2' }, // 常量值
{ provide: Logger, useClass: ConsoleLogger }, // 换实现
{ provide: BACKEND, useFactory: () => build(inject(Config)), deps: [] }, // 工厂
{ provide: UserService, useExisting: AccountService }, // 别名
]类在 @Injectable 时即注册;非类常量必须 InjectionToken。@Optional() 允许注入不存在时报 null;@Self()/@SkipSelf()/@Host() 限定查找层级。
RxJS 与异步编程
Angular 的异步世界由 Observable 流驱动:HTTP 返回流、表单值是流、路由参数是流、事件是流。RxJS 提供时间上的数组方法——map/filter/reduce 作用于「未来到达的每个值」。
核心操作(rxjs 7.8.2 Node 实测)
// 编译运行:node(rxjs 是纯库,无需浏览器)
const { of, interval, map, filter, take, combineLatest, BehaviorSubject } = require('rxjs')
// 变换 + 过滤:of(1..5) 依次发射
of(1, 2, 3, 4, 5).pipe(
map(x => x * 10),
filter(x => x >= 30),
).subscribe(x => console.log(x)) // 30 → 40 → 50(每个值一个回调)
// BehaviorSubject:有初始值的「当前值 + 变化流」;combineLatest 合并最新值
const a = new BehaviorSubject(0)
const b = new BehaviorSubject(10)
combineLatest([a, b]).subscribe(([x, y]) => console.log(x + y))
a.next(1) // → 11
b.next(20) // → 21
interval(1000).pipe(take(1)).subscribe(x => console.log(x)) // 1 秒后输出 0HttpClient:一次请求就是一个流
// 编译运行:Angular 组件内使用(浏览器环境)
import { HttpClient } from '@angular/common/http';
import { Component, inject } from '@angular/core';
import { AsyncPipe } from '@angular/common';
@Component({
standalone: true,
imports: [AsyncPipe],
template: `
<div *ngIf="users$ | async as users; else loading"> <!-- 传统写法:pipe + async -->
@for (user of users; track user.id) { <p>{{ user.name }}</p> }
</div>
<ng-template #loading><p>加载中…</p></ng-template>
`,
})
export class UsersComponent {
private http = inject(HttpClient);
// async 管道负责订阅与自动退订(组件销毁时)——手动 subscribe 必须记得退订
users$ = this.http.get<User[]>('/api/users');
}习惯差别:RxJS 里不 subscribe 就什么都没发生;async 管道 / toSignal 帮你管理订阅与生命周期,优先于在组件里手动 subscribe(手动订阅忘退订 = 内存泄漏)。
流式组合:用操作符编排时序
// 用户切换时自动请求其订单;只取最新一次切换的结果
private userSwitched = new Subject<number>();
orders$ = this.userSwitched.pipe(
debounceTime(300), // 防抖:停止切换 300ms 后
switchMap(id => this.http.get<Order[]>(`/api/users/${id}/orders`)), // 切换到最新请求,旧请求自动取消
catchError(() => of([])), // 错误也要给出流的值,否则 UI 静默
);RxJS 还是信号?
| 场景 | 推荐 |
|---|---|
| 组件内本地状态 / 派生值 / 输入输出 | 信号(简单、类型友好、无订阅管理) |
| HTTP 请求、表单值、路由参数、事件合并、防抖/竞态取消 | RxJS(异步编排能力是信号没有的) |
| 需要两者桥接 | toSignal / toObservable |
信号解决「状态如何渲染」,RxJS 解决「随时间而来的数据如何编排」——现代 Angular 的答案是两个都要,各司其职。
路由
// app.routes.ts —— 声明式路由表,懒加载是默认
import { Routes } from '@angular/router';
export const routes: Routes = [
{ path: '', pathMatch: 'full', redirectTo: '/products' },
{ path: 'products',
loadComponent: () => import('./products/products.component')
.then(m => m.ProductsComponent), // 路由级懒加载
canActivate: [() => inject(AuthService).loggedIn() || redirect('/login')], // 函数守卫
},
{ path: 'product/:id', component: ProductComponent },
{ path: '**', component: NotFoundComponent }, // 兜底
];// main.ts 或 app.config.ts
import { provideRouter } from '@angular/router';
bootstrapApplication(AppComponent, {
providers: [provideRouter(routes)],
});模板内用 <router-outlet /> 承载当前路由组件;routerLink="/products"、routerLinkActive="active" 导航与高亮;编程式导航 inject(Router).navigate(['/product', id]);路由参数 inject(ActivatedRoute).paramMap(值随导航更新的 Observable)。v19+ 支持 withComponentInputBinding() 让路由参数直接绑定到组件 input。
表单
Angular 有两条路线,现代推荐响应式表单(数据模型与模板分离、可测试性最好):
// 编译运行:Angular 表单模块(浏览器环境)
import { Component, inject } from '@angular/core';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
@Component({
standalone: true,
imports: [ReactiveFormsModule],
template: `
<form [formGroup]="form" (ngSubmit)="submit()">
<input formControlName="name" />
<p *ngIf="form.controls.name.touched && form.controls.name.invalid">姓名必填</p>
<button [disabled]="form.invalid">提交</button>
</form>
`,
})
export class ProfileComponent {
private fb = inject(FormBuilder);
form = this.fb.nonNullable.group({
name: ['', [Validators.required, Validators.minLength(2)]],
email: ['', [Validators.required, Validators.email]],
});
submit() {
if (this.form.invalid) return;
console.log(this.form.getRawValue()); // { name, email }(禁用字段也在 raw 中)
}
}响应式表单的模型即对象:FormGroup(对象)/ FormArray(数组)/ FormControl(叶子);校验器是纯函数,可组合、可异步;.valueChanges 是 Observable——表单验证联动、动态依赖都在流上解决。模板驱动表单([(ngModel)] + NgForm)适合简单表单,但校验逻辑散落在模板、难做单元测试。v22 起 Signal Forms(formControl()/formGroup() 信号式 API)稳定,进一步对齐信号范式。
工程化与 SSR
ng new app # 脚手架:standalone + zoneless 默认
ng serve # 开发(热更新)
ng build # 产物默认 outputPath dist/;ng deploy 见各平台
ng g component hero # 生成器:组件/服务/管道/指令/路由守卫
ng test # 单元测试(Karma/Jasmine 或配置 Vitest)
ng update @angular/core@22 # 跨 major 自动迁移- 测试:
TestBed建组件环境(可覆盖注入器、模拟服务),ComponentFixture驱动;Jasmine/Karma 是传统,Vitest +ng test --builder是社区主流; - SSR/水合:v17+
provideServerRendering;增量水合(v19 稳定)只对交互部分激活客户端;@defer与路由懒加载天然配合 SSR; - 依赖注入贯穿各层:HTTP 拦截器
provideHttpClient(withInterceptors([...]))、错误处理、功能守卫(functional guards)都走inject()函数式生态。
踩坑记录
signal.mutate()已移除(Angular 22 实测count.mutate is not a function):v20 起废弃——就地修改与信号「不可变值」语义冲突且难追踪。数组/对象更新一律signal.update(v => [...v, x])返回新引用;- computed 里写副作用、effect 里写信号:computed 必须纯(只读依赖、不写状态);effect 中写另一个信号会触发连锁调度,可能无限循环——写信号放事件回调,effect 只做日志/外部 API 调用;
- 忘记退订 Observable:手动
subscribe而不在ngOnDestroy退订 = 泄漏。用async管道 /takeUntilDestroyed()(v16+)/toSignal让框架代管;只有HttpClient请求会自动 complete 无需退订; - Zone.js → Zoneless 迁移后局部不刷新:第三方库用 setTimeout/addEventListener 内部更新(非信号、非 zone 感知)不再触发检测——用
ChangeDetectorRef.markForCheck()显式标记,或把这些回调包进ApplicationRef.tick()之前升级为信号源; providedIn: 'root'误用为「每页面新状态」:root 是全局单例,多组件共享会串状态;需要隔离就放进组件providers。反过来,懒加载路由里若在providers重复提供某 root 服务,会得到多个实例(路由注入器隔离)——判断清楚生命周期归属;- 新旧两套语法混用:
*ngIf/*ngFor/*ngSwitch(NgModule 时代指令)与新控制流@if/@for/@switch不要在同一模板混搭;新项目全部用@块,迁移工具ng generate @angular/core:control-flow可批量转换; - track 缺失或不可靠:
@for不写track(或 track 用对象自身而非 id)会导致增删时整个列表重建,且表单输入框在复用节点时会串值——始终 track 稳定唯一键; - input 信号以为是普通值:
input()是InputSignal,模板里也要user()调用——写成user.name会拿到 undefined;派生逻辑放computed,不要写ngOnChanges; - 构造器与
inject()混用:同组件里两种注入方式并用触发NG0203。选函数式inject()后保持一致; - 把 Angular 当 React 写:试图「每个组件自管状态、随手手动 setState」会撞上框架约束。Angular 的正确姿势是服务(DI)+ 信号/流——学习曲线的本质是范式差异,不是 API 差异。
术语速查
| 术语 | 一句话 |
|---|---|
| Standalone | 组件自带依赖声明、不依赖 NgModule 的形态(v14+ 预览,v17+ 默认) |
| NgModule | 旧的组织单元(声明/导入/导出/提供者),被 standalone 取代,老项目仍常见 |
| 模板 | HTML + 模板语法(插值/绑定/控制流块/事件) |
| 信号(signal) | 携带响应式值的容器:读取即建立依赖,写入触发更新 |
| computed | 由信号派生、惰性缓存的计算值 |
| effect | 依赖变化时执行的副作用回调(类似 watchEffect) |
| 变更检测 | 数据变化后框架更新 DOM 的机制 |
| Zone.js | 给异步打补丁以触发全局脏检查的库(Zoneless 后不再默认) |
| OnPush | 手动声明「仅在输入/事件/信号变化时检查」的策略 |
| 依赖注入(DI) | 声明所需依赖、由注入器树按层级提供实例的机制 |
| 注入器树 | 根 → 路由/懒加载 → 组件 的层级服务作用域 |
| InjectionToken | 为无法用类作 token 的依赖(常量/配置)定义的键 |
| RxJS / Observable | 惰性推流式异步数据源;操作符在 pipe 中变换 |
| Subject / BehaviorSubject | 多播事件源 / 带当前值的多播事件源 |
| async 管道 | 模板中订阅并自动退订 Observable 的管道 |
| 响应式表单 | 模型与模板分离、可校验可测试的表单体系 |
| 懒加载 | 路由 / @defer 组件按需加载的代码分割 |
| SSR / 水合 | 服务端渲染首屏后,客户端增量接管交互 |
状态与参考
- 状态:已收录(2026-09-02,由前端领域规划清单「Angular|模块化架构、依赖注入、RxJS 响应式编程」转正式)。
- 版本与实测:基于 Angular 22(2026-06-03 发布,当前 Active;版本线见 官方 Releases)。信号 API 示例以
@angular/core@22.1.4+ Node v20.20.1 实跑(输出如文中注释,并由此确认signal.mutate()已移除);RxJS 示例以rxjs@7.8.2实跑;模板/路由/表单示例为 Angular 22 标准用法,可复制进ng new生成的最小工程直接运行。 - 参考:Angular 官方文档(Guide / API / Update guide)、Signals 指南、RxJS 文档、Zoneless 演进。
- 阅读联动:与 React/Vue 的定位差异对照前端领域概览与 React/Vue 3;TypeScript 基础见语言笔记 TypeScript。
下一步
- [ ] 补充 Angular 官方 Guide 的分篇镜像(类似 Vue 3 栏目),把模板、DI、表单、路由逐步展开为独立子页
- [ ] 写一个「服务 + 信号 + HttpClient + 路由」的完整迷你应用示例并跑通测试
- [ ] 对照 React 补一篇三框架心智模型对比,落地选型决策表
写作规范请参阅领域概览。