首页
/ Angular Standalone 迁移实战:用 `ng generate @angular/core:standalone` 将现有 Angular 项目渐进式改造成 Standalone 架构

Angular Standalone 迁移实战:用 `ng generate @angular/core:standalone` 将现有 Angular 项目渐进式改造成 Standalone 架构

2026-09-07 16:08:23作者:滕妙奇

Standalone components / directives / pipes 是 Angular 为了简化应用搭建方式而引入的新一代作者模式,它显著降低了 NgModule 在代码中的占比。Angular 官方在核心仓库中提供了一款名为 standalone-migration 的 ng generate schematic(别名为 standalone),可帮助已有项目在不引入破坏性变更的前提下,增量地把组件、指令与管道转换为 standalone,最终删除冗余的 NgModule 并切换到基于 bootstrapApplication 的引导方式。本文以官方迁移文档为主干,结合 packages/core/schematics/ng-generate/standalone-migration 目录下的真实实现,完整讲解三种迁移模式、执行顺序、前置条件、选项参数与常见坑,读完后你可以直接在你的项目上安全地走完整个 standalone 迁移流程。

迁移工具概述:schematic 能帮你做什么

Angular 仓库的 collection.json 中注册了该迁移入口:

  • 条目名:standalone-migration
  • 别名:standalone
  • 入口 factory:./bundles/standalone-migration.cjs#migrate
  • schema:./ng-generate/standalone-migration/schema.json

因此在任意 Angular CLI 项目中,最简单的运行方式就是:

ng generate @angular/core:standalone

即等价于文档给出的 ng generate @angular/core:standalone 完整形式。该 schematic 的目标是"尽可能多地自动改写",但正如 standalone-migration/index.ts 中实现所体现的:迁移完成后工具会打印 "Automated migration step has finished" 并明确提示——必须人工验证应用仍然可以编译且行为符合预期。迁移过程会涉及大量机械改写,部分边缘情况仍需项目作者手工修复,因此官方建议把迁移视为"多轮执行 + 每轮后手工校验"的流程,而不是一次性操作。

迁移前置条件:Before updating

在运行 schematic 前,请确保你的项目满足以下三项要求(官方文档列出的硬性前提):

  1. Angular 版本不低于 15.2.0——standalone API 与对应迁移工具从这个版本开始可用;
  2. 项目当前没有任何编译错误——schematic 依赖 Angular 编译器对代码做静态分析,编译不通过会直接影响分析正确性;
  3. 工作区处于干净的 Git 分支且所有工作已保存——迁移会批量改写甚至删除文件(例如删除整个 NgModule 文件),必须保证可随时回滚。

Schematic 选项:mode 与 path

迁移工具的选项由 schema.json 定义,共两个:

选项 详情
mode 要执行的迁移操作,可选 convert-to-standaloneprune-ng-modulesstandalone-bootstrap,默认值为 convert-to-standalone。运行时不传会以交互式列表让用户选择。
path 相对项目根目录的待迁移路径,默认 ./(整个项目)。可以用它分目录、分阶段地增量迁移大项目。

mode 对应的三种取值与官方提供的交互提示文案为:

  • convert-to-standalone —— "Convert all components, directives and pipes to standalone"
  • prune-ng-modules —— "Remove unnecessary NgModule classes"
  • standalone-bootstrap —— "Bootstrap the application using standalone APIs"

例如只迁移 src/app/features 目录下的代码,可以执行:

ng generate @angular/core:standalone --mode=convert-to-standalone --path=src/app/features

迁移的执行步骤:三步走 + 逐步校验

官方流程建议按以下顺序运行,每运行完一步都要确认代码能正常编译、应用行为符合预期,再进入下一步:

  1. 运行 ng generate @angular/core:standalone,选择 Convert all components, directives and pipes to standalone(把组件、指令、管道转为 standalone);
  2. 再次运行 ng generate @angular/core:standalone,选择 Remove unnecessary NgModule classes(删除冗余的 NgModule);
  3. 第三次运行 ng generate @angular/core:standalone,选择 Bootstrap the application using standalone APIs(把应用入口切换到 standalone 引导);
  4. 运行项目的 lint 与格式化检查,修复问题后提交代码。

需要注意两点:

  • 迁移工具自动生成的新代码很可能不符合你项目现有的代码格式化风格,也可能会触发 lint 报错(例如 import 顺序、引号风格等),这属于预期现象,应在最后统一手工整理;
  • 对于大型项目,利用 path 选项按子目录逐个迁移会显著降低每轮的手工校验成本;子模块迁移过程中可以阶段性 commit,每步独立验证、独立提交。

值得一提的是,虽然用户层面拆成了三步,但从 index.ts 的实现可以观察到:当执行 standalone-bootstrap 模式时,工具在完成引导改写后会立即以 prune-ng-modules 模式递归地再跑一遍,从而自动删除刚被替换掉的根 NgModule。原因在源码注释里写明:无法在同一遍内部直接执行模块清理,因为两阶段会产生相互冲突的 AST 节点变更,必须先把第一步改动落到磁盘。

模式一:Convert declarations to standalone

这是迁移的第一步。此模式会扫描所有组件、指令与管道:

  • 移除它们装饰器上的 standalone: false
  • 把其原先依赖的指令、管道自动分析出来,加入新的 imports 数组。

特别注意: 本步骤会跳过会 bootstrap 组件的 NgModule 及其声明的组件。原因是这类模块很可能是通过 bootstrapModule 引导的根模块,与 standalone 体系的 bootstrapApplication 不兼容,需要留待第三步"Switch to standalone bootstrapping API"统一处理。源码层面这一逻辑对应 to-standalone.ts 中的 findNgModuleClassesToMigratefilterNonBootstrappedDeclarations 组合:先收集每个 NgModule 的全部 declarations,再过滤掉"由该模块 bootstrap"的那部分,只有剩余部分才进入迁移集合。

迁移前:

// shared.module.ts
@NgModule({
  imports: [CommonModule],
  declarations: [Greeter],
  exports: [Greeter],
})
export class SharedModule {}
// greeter.ts
@Component({
  selector: 'greeter',
  template: '<div *ngIf="showGreeting">Hello</div>',
  standalone: false,
})
export class Greeter {
  showGreeting = true;
}

迁移后:

// shared.module.ts
@NgModule({
  imports: [CommonModule, Greeter],
  exports: [Greeter],
})
export class SharedModule {}
// greeter.ts
@Component({
  selector: 'greeter',
  template: '<div *ngIf="showGreeting">Hello</div>',
  imports: [NgIf],
})
export class Greeter {
  showGreeting = true;
}

模板中使用的 *ngIf 在 Angular 内部对应的类是 NgIf,schematic 会把它们解析为 imports 依赖并自动补上 import 语句。从 util.tsknownInternalAliasRemapper 还可以看到一个实现细节:当模板中用到 *ngFor 时,Angular 模板类型检查器找到的底层类是 NgForOf,工具会专门把生成的 import 符号重映射为更短的常用别名 NgFor,让产物更贴近开发者手写习惯:

export function knownInternalAliasRemapper(imports: PotentialImport[]) {
  return imports.map((current) =>
    current.moduleSpecifier === '@angular/common' && current.symbolName === 'NgForOf'
      ? {...current, symbolName: 'NgFor'}
      : current,
  );
}

为了让模板分析准确,迁移程序在创建编译程序时显式开启了模板类型检查器(_enableTemplateTypeChecker: true),并设置 compileNonExportedClasses: true(连未导出的类也一并迁移)、skipLibCheck 相关开关以加速分析,具体见 index.ts

模式二:Remove unnecessary NgModules

当所有 declarations 都转成 standalone 后,大量纯粹用于"汇总声明并导出"的 NgModule 就失去了存在的意义。此步骤会:

  • 识别出这类模块并直接删除整个模块文件
  • 同时删除代码中尽可能多的对它的引用(含 import、re-export 等)。

如果某个引用实在无法由工具安全删除,工具会在原位置留下如下注释,提示开发者手工清理:

/* TODO(standalone-migration): clean up removed NgModule reference manually */

该注释的确切文本格式在 prune-modules.tsstandalone-bootstrap.ts 中都有对应实现。

官方定义了一个模块"可以安全删除"的判定条件,满足以下全部条件才会被删除:

  • 没有 declarations
  • 没有 providers
  • 没有 bootstrap 组件;
  • imports 中没有引用 ModuleWithProviders 符号或"本身不可删除"的模块;
  • 类中没有成员(空的构造函数会被忽略)。

迁移前:

// importer.module.ts
@NgModule({
  imports: [FooComponent, BarPipe],
  exports: [FooComponent, BarPipe],
})
export class ImporterModule {}

迁移后:

// importer.module.ts
// Does not exist!

如果一个模块只是被另一个仍然保留的模块引用,但引用发生在不可自动修改的位置,就会变成文档展示的带 TODO 形式;模块内部不可移动的 providers 逻辑也会被原样保留下来。

引用删除能力的背后,是 util.ts 中的 ReferenceResolver 工具类:它基于 TypeScript Language Service 在整个项目中查找某个被删类的全部引用位置(findReferencesInProject),并且默认通过正则 node_modules|\.ngtypecheck\.ts 排除库代码与类型检查生成文件,兼顾准确性与性能;单文件场景还会临时让 language service 只能读取目标文件,把大规模项目的引用查找提速一个数量级。随后 index.ts 会把"待删除文件"从改动列表中排除,最终真正 tree.delete 掉模块文件本身。

模式三:Switch to standalone bootstrapping API

这是迁移的收尾步骤,负责把应用入口从 platformBrowser().bootstrapModule(AppModule) 切换到 standalone 体系的 bootstrapApplication(App, {providers: [...]})。综合官方文档与 standalone-bootstrap.ts 的实现,这一模式要安全地完成以下一连串改动:

  1. 生成替换 bootstrapModule 调用的 bootstrapApplication 调用;
  2. 把被引导模块的 declarations(即第一步中被刻意跳过的根组件)转为 standalone;
  3. 把根模块的 providers 原样复制到 bootstrapApplicationproviders 选项;
  4. 把根模块 imports 数组中那些仍需保留的"非 standalone 类"模块,用 importProvidersFrom(...) 包裹后放进 providers
  5. 调整被复制代码中的动态 import 路径,保证拷到新位置后依然正确;
  6. 检测到存在 standalone 等价物的 API 时自动改写,例如 RouterModule.forRoot(...) 会转为 provideRouter(...)
  7. 删除根 NgModule。

对于根模块中 providers / imports 引用了"类声明之外"的代码(如模块级常量、非导出类、注入令牌、接口等),迁移工具会尽量把可导出的部分改为从新位置 import,不可导出的则整体复制过去,避免破坏现有类型。

迁移前:

// ./app/app.module.ts
import {NgModule} from '@angular/core';
import {App} from './app';

@NgModule({
  declarations: [App],
  bootstrap: [App],
})
export class AppModule {}
// ./app/app.ts
@Component({
  selector: 'app',
  template: 'hello',
  standalone: false,
})
export class App {}
// ./main.ts
import {platformBrowser} from '@angular/platform-browser';
import {AppModule} from './app/app.module';

platformBrowser()
  .bootstrapModule(AppModule)
  .catch((e) => console.error(e));

迁移后:

// ./app/app.module.ts
// Does not exist!
// ./app/app.ts
@Component({
  selector: 'app',
  template: 'hello',
})
export class App {}
// ./main.ts
import {bootstrapApplication} from '@angular/platform-browser';
import {App} from './app';

bootstrapApplication(App).catch((e) => console.error(e));

当根模块携带路由、动画等配置时,产物会体现出"module imports → providers 函数调用"的改写模式。工具内部在检测到 RouterModule.forRoot 时会自动补充 provideRouter 的 import(见 standalone-bootstrap.ts),最终入口看起来大致如下:

bootstrapApplication(AppComponent, {
  providers: [
    importProvidersFrom(SharedModule),
    {provide: token, useValue: {foo: true, bar: {baz: false}}},
    provideAnimations(),
    provideRouter(
      [
        {
          path: 'shop',
          loadComponent: () => import('./shop/shop.component').then((m) => m.ShopComponent),
        },
      ],
      withEnabledBlockingInitialNavigation(),
    ),
  ],
}).catch((e) => console.error(e));

迁移的底层执行机制

结合 index.ts 全貌,可以梳理出该 schematic 的执行骨架,这对理解"为什么会有某些前置条件 / 限制"很有帮助:

  • 以 tsconfig 为迁移范围:工具通过 getProjectTsConfigPaths 收集项目所有 build 与 test 的 tsconfig,逐个建立 Angular 编译程序(createProgram),因此不在任何 tsconfig 覆盖范围内的文件不会被迁移
  • 以路径过滤迁移目标:对每个编译程序,只保留 fileNamepathToMigrate 开头的源文件;若 path.. 开头或在项目外、指向的文件不是目录,会直接抛出 SchematicsException(见 index.ts);
  • 依赖模板类型检查器:模式一依赖 program.compiler.getTemplateTypeChecker() 解析模板中引用的指令/管道(见 to-standalone.ts),这解释了为何编译错误会阻塞迁移;
  • 统一变更追踪:所有改写经由 ChangeTracker 收集为 ChangesByFile,再统一 apply 到 schematic 的 Tree 上,避免多阶段分析间的相互干扰。

仓库中还带有完整的端到端测试 standalone_migration_spec.ts,覆盖三种模式的大量正反例,可以作为了解迁移工具行为边界的补充阅读。

常见问题:为什么 schematic 可能无法正常工作

官方文档列举了几类高频障碍,结合上述实现机制可以更好理解成因:

  • 编译错误——项目存在编译错误时,Angular 编译器无法正确建立类型信息,模板类型检查与分析都会失真,必须先修复;
  • 文件未被 tsconfig 覆盖——schematic 通过分析项目的 tsconfig.json(build 与 test 路径)来决定迁移哪些文件,未被任何 tsconfig 捕获的文件会被直接排除;
  • 无法静态分析的代码——schematic 依靠静态分析理解代码并定位改动点。任何在构建期无法被静态分析的类元数据(例如动态生成装饰器、eval 式写法)所在类都可能被跳过,需要手工迁移。

已知限制:哪些场景必须手工处理

官方明确承认,鉴于迁移工具的体积与复杂度,存在两类它无法可靠处理的场景:

  • 单元测试中的 imports 可能不完整:由于单测代码不会经过 AoT(Ahead-of-Time)编译,模板依赖分析在其中天然缺失,工具为测试组件补的 imports 可能并不完全正确,需要人工核对 TestBed 配置;
  • 无法识别 Angular API 的自定义包装:工具只识别对 Angular API 的直接调用。比如你封装了自定义的 customConfigureTestModule 来包装 TestBed.configureTestingModule,那么通过它声明的组件可能无法被识别与迁移。判断逻辑可参考 util.ts:它只对 configureTestingModule(且首参为对象字面量)或 Catalyst 的 setupModule 这类直接调用生效。

迁移完成后的可选跟进

当三步迁移跑完、应用已整体转为 standalone 后,官方建议再做以下收尾工作:

  • 查找并手工清理剩余的 NgModule declarations:步骤二无法自动删除所有模块,残留的声明(尤其是测试、特殊场景中的引用)需要手工移除;
  • 运行单元测试:按上文限制章节,重点检查测试文件中 TestBed 的 imports 是否正确,修复失败的用例;
  • 运行代码格式化:schematic 生成的代码通常不符合项目原有格式规范;
  • 运行 linter 并修复新增告警:部分 linter 支持 --fix 参数,可以自动解决部分格式类告警。

完成上述工作并提交后,你的项目就正式进入了 standalone 架构:组件、指令与管道自带 imports,应用入口使用 bootstrapApplicationNgModule 只会在真正需要(例如懒加载 bundle 聚合、遗留三方库适配)的地方出现,未来无论是惰性加载独立路由、使用新的控制流还是 signal-based 输入等新特性,都会顺畅得多。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.13 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
529
593
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
916
1.83 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.58 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.35 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
515
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388