Angular 迁移全攻略:从 NgModule 到 Standalone 与 Signal API 的 14 条官方自动化升级路线
Angular 持续迭代的过程中引入了 Standalone 组件、内置控制流语法、基于 Signal 的输入输出与查询 API 等新一代范式,而官方将对应的一整套"自动化迁移机制"集中收录在本文档站点目录(adev/src/content/reference/migrations/overview.md)中。本文以该迁移总览为主体,逐条拆解其收录的全部 14 条官方迁移(migration)指令:它们都以 @angular/core 集合提供的 schematics 形式存在,允许你在不产生破坏性变更的前提下,对既有项目进行增量式升级。读完本文,你将掌握每条迁移的命令、可配置项、边界条件与推荐执行顺序,并能对照仓库源码确认这些迁移的实现位置,从而为旧项目规划出一条从 NgModule 平滑走向现代 Angular 的完整升级路线图。
迁移总览:14 条官方路线图
在迁移前先把握全局。下表汇总了 overview 中 14 张迁移卡片的对应 schematics 命令与核心目标,各命令均由 ng generate @angular/core:<schematic> 的形式调用:
| 迁移方向 | 命令 | 解决的核心问题 |
|---|---|---|
| Standalone 组件 | ng generate @angular/core:standalone |
将组件/指令/管道转为 standalone,移除多余 NgModule,改用独立引导 API |
| 内置控制流语法 | ng generate @angular/core:control-flow |
用 @if/@for/@switch 替代 *ngIf/*ngFor/*ngSwitch |
inject() 函数 |
ng generate @angular/core:inject |
将构造函数注入改为函数式注入 |
| 路由懒加载 | ng generate @angular/core:route-lazy-loading |
把急加载组件路由转为 loadComponent 懒加载 |
Signal input() |
ng generate @angular/core:signal-input-migration |
将 @Input 字段迁移到 signal input API |
Signal output() |
ng generate @angular/core:output-migration |
将 @Output/EventEmitter 迁移到 output() 函数 |
| Signal 查询 | ng generate @angular/core:signal-queries-migration |
将 @ViewChild/@ContentChild 等迁移到 signal queries |
| 清理无用导入 | ng generate @angular/core:cleanup-unused-imports |
清理组件 imports 数组中未被模板使用的符号 |
| 自闭合标签 | ng generate @angular/core:self-closing-tag |
模板改写为自闭合标签 |
| NgClass → class 绑定 | ng generate @angular/core:ngclass-to-class |
用 [class]/[class.x] 绑定替代 NgClass |
| NgStyle → style 绑定 | ng generate @angular/core:ngstyle-to-style |
用 [style] 绑定替代 NgStyle |
| RouterTestingModule 迁移 | ng generate @angular/core:router-testing-module-migration |
测试中改用 RouterModule 与 provideLocationMocks() |
| CommonModule → 独立导入 | ng generate @angular/core:common-to-standalone |
用最小化的指令/管道导入替代整包 CommonModule |
这些 schematics 都注册在仓库的 @angular/core 迁移集合中,可在 packages/core/schematics/collection.json 中逐条确认其定义与别名(例如 standalone、control-flow、inject 等)。对应每个迁移的实现源码位于 packages/core/schematics/ng-generate 与 packages/core/schematics/migrations 目录,源码结构基本与上表一一对应。
迁移执行前的通用前置条件
多数迁移文档都强调以下共同前提(详见 standalone.md 的 "Before updating" 一节,cleanup-unused-imports.md 等亦有类似说明):
- 项目版本满足要求:例如 standalone 迁移要求 Angular 15.2.0 及以上;内置控制流语法自 Angular v17 起可用;signal inputs/queries 与
output()函数被认为在 v19 达到生产就绪。请结合自己项目的版本确认迁移可用性。 - 项目必须无编译错误:迁移依赖对代码的静态分析,编译报错会导致 Angular 无法正确识别待迁移代码。
- 工作在干净的分支上且所有改动已保存:迁移会改写大量文件,干净的 Git 工作区是出错后可回退的前提。
此外,从 overview 各卡片与迁移文档可以提炼出三条贯穿始终的工程纪律:
- 分步执行、逐步验证:每运行一步迁移后,都应重新构建并运行测试,确认无回归后再进入下一步;
- 保留人工复核环节:自动迁移无法覆盖所有边界情况,迁移产物可能不符合你项目的格式化规范,需配合 lint/format 工具收尾;
- 利用
--path做增量迁移:几乎所有迁移都支持path选项,让你可以先对子目录试点,再推广到全项目。
第一梯队:Standalone 与打包体积优化
迁移到 Standalone:三步走流程
Standalone 组件允许组件、指令、管道直接在自身声明依赖,而无需经由 NgModule,从而大幅简化 Angular 应用的作者体验。官方提供了 standalone schematic(详见 standalone.md)自动化完成大部分改造,其迁移过程由三个模式(mode)构成,必须按以下顺序依次执行,并在每一步之间验证构建:
ng generate @angular/core:standalone
模式一:Convert declarations to standalone —— 将所有组件、指令和管道转换为 standalone:移除 standalone: false,并把依赖补充进各自的 imports 数组。迁移会跳过引导(bootstrap)组件的 NgModule(因为它们通常还是 bootstrapModule 风格的根模块),这些会在模式三中被一并处理。一个典型例子:转换前 Greeter 组件声明了 standalone: false 并使用 *ngIf,而共享模块 SharedModule 通过 imports: [CommonModule] 间接提供 NgIf;转换后 Greeter 变为 standalone 并在 imports: [NgIf] 中直接导入所需指令。
模式二:Remove unnecessary NgModules —— 在全部声明转换为 standalone 后,删除可以安全移除的 NgModule。迁移判定一个模块可安全删除的条件是:无 declarations、无 providers、无 bootstrap 组件、无引用 ModuleWithProviders 或不可删除模块的 imports,且无类成员(空构造函数会被忽略)。若某处引用无法自动删除,迁移会留下如下 TODO 注释供人工清理:
/* TODO(standalone-migration): clean up removed NgModule reference manually */
模式三:Switch to standalone bootstrapping API —— 将 bootstrapModule 用法改写为基于 standalone 的 bootstrapApplication,移除根组件上的 standalone: false 并删除根 NgModule;若根模块带 providers 或 imports,迁移会尽量把这些配置拷贝进新的引导调用中。转换前 main.ts 里的 platformBrowser().bootstrapModule(AppModule) 会被改写为:
import {bootstrapApplication} from '@angular/platform-browser';
import {App} from './app';
bootstrapApplication(App).catch((e) => console.error(e));
完成以上三步后(详见原文档 "After the migration" 部分),建议继续:手动清理剩余的 NgModule 声明、跑一遍单元测试并修复失败用例、执行格式化器与 linter(部分 linter 支持 --fix 自动修复)。
常见失败原因与限制(见 standalone.md 的 "Common problems" 与 "Limitations"):编译错误会让迁移无法正确分析;schematic 只分析 tsconfig.json 捕获到的文件;无法静态分析元数据的类会被跳过。此外,单测代码不经 AoT 编译,为测试组件补的 imports 可能不完整;schematic 只识别对 Angular API 的直接调用,无法识别诸如自定义封装 TestBed.configureTestingModule 的包装函数。
移除整包 CommonModule:拆分为最小导入
该迁移帮助项目将组件中对 CommonModule 的整包导入替换为模板实际用到的最小指令与管道集合(如 NgIf、NgFor、AsyncPipe、JsonPipe 等),详见 common-to-standalone.md:
ng generate @angular/core:common-to-standalone
例如模板中同时使用了 *ngIf、async 管道与 json 管道时,迁移会把 imports: [CommonModule] 改写为 imports: [AsyncPipe, JsonPipe, NgIf] 并移除 CommonModule 导入。该迁移同样支持 --path 选项增量执行(默认作用于项目根 ./)。这一收窄导入的做法既配合了控制流语法的推行,也是降低打包体积、提升 tree-shaking 效果的重要一步。
路由懒加载迁移:按需拆包
route-lazy-loading.md 描述的迁移将急加载的组件路由转为懒加载,使构建产物被拆分成更小的 chunk,从而减少首屏加载的 JS 体积:
ng generate @angular/core:route-lazy-loading
迁移会扫描以下方式定义的路由并执行转换:RouterModule.forRoot / RouterModule.forChild、Router.resetConfig、provideRouter,以及类型为 Routes 或 Route[] 的变量。对于 standalone 且急加载的组件,迁移将其改写为 loadComponent:
// 转换前
RouterModule.forRoot([{path: 'home', component: Home}]),
// 转换后
RouterModule.forRoot([{path: 'home', loadComponent: () => import('./home').then((m) => m.Home)}]),
若路由中某些组件仍声明在 NgModule 里(非 standalone),迁移会输出这些组件的清单及所在文件位置,提示你先用 standalone 迁移(见上文)将其转换后再次运行本迁移。该迁移支持 --path src/app/sub-component 限定迁移范围,path 值为项目内的相对路径。
模板写法现代化三件套
三条轻量级模板迁移聚焦于减少运行时指令与冗余导入:
自闭合标签(self-closing-tags.md):Angular 模板自 v16 起支持自闭合标签,运行 ng generate @angular/core:self-closing-tag 可将 <hello-world></hello-world> 转换为 <hello-world />。
NgClass → class 绑定(ngclass-to-class.md):运行 ng generate @angular/core:ngclass-to-class 仅迁移被判定为安全的用法,例如将 <div [ngClass]="{admin: isAdmin, dense: density === 'high'}"></div> 改写为 <div [class]="{admin: isAdmin, dense: density === 'high'}"></div>。默认情况下,对象字面量键含空格分隔类名的用法不会被迁移;开启 --migrate-space-separated-key 后,迁移会为每个键单独生成一条绑定,例如 {'class1 class2': condition} 会被拆成两条 [class.class1] 与 [class.class2] 绑定。
NgStyle → style 绑定(ngstyle-to-style.md):运行 ng generate @angular/core:ngstyle-to-style 将 [ngStyle] 迁移为 [style] 绑定。默认只迁移内联对象字面量;开启 --best-effort-mode 后,绑定到对象引用的写法(如 [ngStyle]="styleObject")也会被迁移——但若该对象后续被原地修改,这可能不安全,因此该选项默认关闭。
控制流语法迁移:告别 CommonModule 的模板糖
自 Angular v17 起,内置控制流语法 @if、@for、@switch 被编译进模板本身,不再需要为了 *ngIf、*ngFor、*ngSwitch 而导入 CommonModule(详见 control-flow.md):
ng generate @angular/core:control-flow
该迁移会改写应用中所有既有模板代码。值得注意的是它带来的一个破坏性差异:在 @for 的 track 表达式中,若被跟踪的属性变化而对象引用未变(就地修改),Angular 只会更新视图绑定(含组件输入),而不会销毁重建元素;与之相对,*ngFor 在同场景下若 trackBy 返回了不同的值,则会执行元素的重新挂载(destroy + recreate)。迁移后若应用的渲染行为依赖旧的销毁重建语义,需要据此人工审视相关逻辑。
第二梯队:Signal 化改造(input/output/queries)
从 Angular v17/v19 起逐步引入并定为生产就绪的 Signal 化组件 API,让开发者不再依赖装饰器,即可获得更精确的类型与更细粒度的变更检测能力。
迁移到 Signal input()
signal inputs 在 v19 被认定生产就绪(signal-inputs.md)。命令行迁移方式:
ng generate @angular/core:signal-input-migration
迁移会做两件事:① 把 @Input() 类成员改写为 input() 等价物;② 同步更新对已迁移输入的所有引用——包括模板、host bindings 与 TypeScript 代码。例如 @Input() name: string | undefined = undefined; 会变为 readonly name = input<string>();,模板中 {{ name ?? '' }} 相应变为 {{ name() ?? '' }},方法内对 this.name 的判空访问也需要改为先取信号值再判空。
配置选项:
--path:默认更新整个 Angular CLI 工作区,可限定到子目录;--best-effort-mode:默认会跳过无法安全迁移的输入;开启后尽量迁移所有输入,即使可能破坏构建;--insert-todos:为被跳过的输入添加带有原因说明的 TODO 注释(例如"应用代码写入了该输入,导致无法迁移");--analysis-dir:@Input()迁移需要更新所有受影响引用,因此默认会分析整个工作区(与--path无关);在大项目中可用该选项把分析范围收窄到子目录,但目录外的引用会被静默跳过,可能破坏构建。
迁移到 output() 函数
output API 于 v17.3 引入、v19 生产就绪,它模仿 input() API 形态但并非基于 Signal(outputs.md)。执行:
ng generate @angular/core:output-migration
迁移完成三件事:① @Output() 类成员改写为 output() 等价物;② 更新文件顶层的 TypeScript 模块导入;③ 将不推荐的 event.next() 调用改为 event.emit(),并删除 event.complete() 调用。例如 @Output() someChange = new EventEmitter<string>(); 变为 readonly someChange = output<string>();。
有一类例外不会被迁移:当事件与 pipe() 方法组合使用时,迁移会跳过该代码(例如类中同时存在 this.close.complete() 与 this.close.pipe() 的写法)。配置项与 signal input 迁移一致,均支持 --path(未指定时会提示你输入路径并默认处理整个工作区)与 --analysis-dir,用法如:
ng generate @angular/core:output-migration --path src/app/sub-folder
迁移到 Signal queries
signal queries(viewChild/contentChild 等)同样于 v19 被认定生产就绪(signal-queries.md)。执行:
ng generate @angular/core:signal-queries-migration
迁移把 @ViewChild()、@ViewChildren、@ContentChild、@ContentChildren 类成员改写为对应的 signal 版本,并同步更新模板、host bindings 与 TypeScript 中对这些查询的所有引用。例如 @ContentChild('someRef') ref: ElementRef | undefined = undefined; 变为 readonly ref = contentChild<ElementRef>('someRef');,模板中相应改为调用 someRef()。配置选项(--path、--best-effort-mode、--insert-todos、--analysis-dir)的语义与 signal input 迁移完全一致,被跳过的查询同样会附带如"应用代码写入了该查询"之类的 TODO 说明。
在 VSCode 中一键执行 Signal 迁移
signal input 与 signal queries 两条迁移除命令行外,还以 VSCode 代码重构动作(code refactor action) 的形式提供(详见两篇文档的 "VSCode extension" 一节)。安装最新版 Angular Language Service 扩展后,将光标点在某个 @Input 字段(或组件/指令上),稍候点击出现的黄色灯泡按钮,即可在菜单中选择对应的 signal 迁移;signal queries 的触发对象则是 @ViewChild、@ViewChildren、@ContentChild、@ContentChildren 字段。下面的截图展示了这一交互形态(图片位于 adev/src/assets/images/migrations/signal-inputs-vscode.png 与 adev/src/assets/images/migrations/signal-queries-vscode.png)。
Screenshot of the VSCode extension and clicking on an @Input field
Screenshot of the VSCode extension and clicking on an @ViewChild field
第三梯队:依赖注入与代码卫生
迁移到 inject() 函数
inject() 相比构造函数注入能提供更精确的类型,且与标准装饰器有更好的兼容性(inject-function.md)。执行:
ng generate @angular/core:inject
迁移会把构造函数注入改写为字段初始化式的 inject() 调用,例如:
// 转换前
constructor(
private service: MyService,
@Inject(DI_TOKEN) @Optional() readonly token: string,
) {}
// 转换后
private service = inject(MyService);
readonly token = inject(DI_TOKEN, {optional: true});
该迁移提供了四个精细配置项以定制输出:
path:指定待迁移的项目子路径,传.或留空则迁移整个目录;migrateAbstractClasses:Angular 并不校验抽象类参数的注入性,因此迁移默认跳过抽象类以避免引入破坏;开启后可以迁移它们,但可能需要人工修复部分编译错误;backwardsCompatibleConstructors:默认迁移会尽量精简代码,包括删除构造函数参数乃至整个空构造函数。但当带 Angular 装饰器的类之间发生继承时,这可能引发编译错误。开启该选项后,迁移会额外生成一个向后兼容的构造函数签名(如constructor(...args: unknown[]);与空的constructor() {}并存),代价是代码量增加;nonNullableOptional:带@Optional的参数注入失败时 Angular 会返回null,因此其真实类型应含| null;但装饰器无法影响类型,既有代码的类型往往并不正确。inject()修正了这一点,却可能暴露出新的编译错误。开启该选项后,迁移会在inject()调用后追加非空断言!以匹配旧类型——但已声明为可空类型的参数不会追加断言,因为依赖它的代码通常已正确处理空值。
清理无用导入
自 v19 起,Angular 会报告组件的 imports 数组中含有未在模板中使用到的符号(cleanup-unused-imports.md)。运行下述 schematic 即可清理项目中全部无用导入:
ng generate @angular/core:cleanup-unused-imports
例如当组件模板仅渲染 'Hello' 却导入了 UnusedDirective 时,迁移会移除该导入并让 imports 数组留空,保持组件声明最小化。
测试侧的 RouterTestingModule 迁移
在测试代码中,RouterTestingModule 正逐步被淘汰(router-testing-module-migration.md)。该迁移将测试中的 RouterTestingModule 用量改写为 RouterModule;当测试从 @angular/common/testing 导入 SpyLocation 并使用了其 urlChanges 属性时,迁移还会自动补充 provideLocationMocks() 以保持原有行为:
ng generate @angular/core:router-testing-module-migration
示例 1(保留路由选项):RouterTestingModule.withRoutes(routes, {initialNavigation: 'enabledBlocking'}) 会被改写为 RouterModule.forRoot(routes, {initialNavigation: 'enabledBlocking'})。
示例 2(补上 location mocks):当测试通过 TestBed.inject(SpyLocation) 注入并断言 spy.urlChanges 时,迁移会在 TestBed.configureTestingModule 的 providers 中加入 provideLocationMocks(),确保注入 SpyLocation 后其模拟定位能力依然可用。该迁移同样支持 --path(相对项目根的路径,默认 ./)做增量迁移。
推荐的端到端升级路径
结合 overview 对全部迁移的介绍以及各文档强调的依赖关系,对于尚停留在 NgModule + 装饰器风格的老项目,一条低风险、可逐步验证的推荐升级顺序如下:
- 先做行为等价、低风险的模板卫生改造:自闭合标签、
NgClass → class、NgStyle → style、清理无用导入——这些迁移几乎不改变运行时语义,适合热身并提前发现编译问题; - 再推行 Standalone 化的三步迁移:从"convert declarations"到"remove NgModules"再到"bootstrapApplication",这是后续几乎所有优化的前置条件;
- 随后做打包优化:在组件已 standalone 的前提下,运行路由懒加载迁移拆分产物;同时用
common-to-standalone收敛CommonModule的整包导入; - 然后开启控制流语法迁移:替换
*ngIf/*ngFor/*ngSwitch,注意@for的 track 与视图复用语义差异; - Signal 化改造作为收尾:按 signal input、signal output、signal queries、
inject()的顺序推进,配合各迁移的--analysis-dir与 VSCode 重构动作在大项目中分片完成; - 最后清理测试与收尾:运行
router-testing-module-migration修正测试依赖,再执行 lint、格式化与完整测试套件。
整个过程中请始终遵循文档强调的纪律:每一步迁移之间都验证构建与运行、为无法自动处理的边界保留人工复核、并把每个 schematics 的具体行为与限制对照 packages/core/schematics/collection.json 以及 packages/core/schematics/ng-generate、packages/core/schematics/migrations 目录下的实现源码来确认。这样逐级推进,你的项目就能在不推翻重写的前提下,平滑完成从旧范式到现代 Angular 的全部升级。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00