深入 Angular 编译器内核:ngtsccore包、NgCompiler与增量编译机制全解析
【免费下载链接】angularDeliver web apps with confidence 🚀项目地址: https://gitcode.com/GitHub_Trending/an/angular
导读
packages/compiler-cli/src/ngtsc/core/是 Angular 官方仓库中定位编译器"心脏"的目录——无论是命令行工具ngc、Angular CLI 内部使用的NgtscProgram,还是 Bazel 规则ts_library的实验性集成NgTscPlugin,最终都汇聚到这里的NgCompiler。本文以该目录的 README 为主线,结合 core/src/compiler.ts、core/src/host.ts 与 ngtsc/program.ts 等源码,为你讲清三条核心主线:Angular 编译如何嵌入 TypeScript 编译流程、NgCompiler如何通过CompilationTicket管理增量编译生命周期,以及NgCompilerOptions背后分层设计的选项体系。读完你将能够理解并二次开发任何以 TypeScript 编译器为宿主、内嵌 Angular 编译能力的集成方案。
Angular 编译的本质:装饰器到静态定义字段的翻译
Angular 编译的核心任务,是把 TypeScript 源码中的 Angular 装饰器(@Component、@Directive、@NgModule、@Injectable、@Pipe等)翻译为 Ivy 编译器可识别的静态定义字段(static definition fields)。
编译发生在整个 TypeScript 编译过程之中(构建期,build time):
- TypeScript 代码被进行类型检查(type-checking);
- 再被降级(downlevel)为 JavaScript 代码;
- 沿途产生 TypeScript 自己的诊断信息,同时也会产出Angular 特有的诊断信息(如模板类型错误、非法装饰器配置等)。
换句话说,Angular 的 AOT 编译并不是一套独立的工具链,而是"寄生"在 TypeScript 编译器工作流中的一个扩展层。core包提供的正是让第三方编译器实现者把 Angular 编译"装配"进 TypeScript 编译流程的 API。
从 ts.CompilerHost 到 NgCompiler:融入 Angular 的编译流程
标准 TypeScript 编译流程
任何使用 TypeScript 编译器 API 的程序都遵循同一套多步骤过程:
- 创建
ts.CompilerHost; - 用该 Host 外加一组"根文件"(root files)创建
ts.Program; - 用
ts.Program收集各类诊断(diagnostics); - 最终调用
ts.Program的emit,产出 JavaScript 代码。
融入 Angular 后的八步流程
集成 Angular 编译的编译器依然遵循类似流程,只是中间多了几步。README 中的对照流程如下:
- 创建
ts.CompilerHost; - 将该 Host 包装进一个
NgCompilerHost,后者会向编译中添加 Angular 特有的文件; - 基于
NgCompilerHost及其扩充后的根文件集合创建ts.Program; - 创建一个
CompilationTicket(可选地携带上一次编译运行的状态); - 使用该
CompilationTicket创建NgCompiler; - 照常从
ts.Program收集诊断,同时也可以从NgCompiler收集诊断; - 在
emit之前调用NgCompiler.prepareEmit,拿到需要喂给ts.Program.emit的 Angular transformers; - 携带上述 transformers 调用
ts.Program.emit,产出带 Angular 扩展的 JavaScript。
在仓库中的实际落地:NgtscProgram
这套抽象不只是文档中的"设计蓝图",Angular CLI 实际消费的NgtscProgram就在 ngtsc/program.ts 中实现。可以看到它与文档描述的步骤一一对应:
- 在 program.ts 中通过
NgCompilerHost.wrap(delegateHost, rootNames, options, reuseProgram ?? null)完成第 2 步的 Host 包装; - 在 program.ts 中依据"是否存在上一次编译程序"选择创建 fresh ticket 还是 incremental ticket(第 4 步);
- 在 program.ts 中将异步资源加载委托给
this.compiler.analyzeAsync(); - 在 program.ts 的 emit 阶段,通过
this.compiler.prepareEmit()取出 transformers 再执行真正的 TS 发射。
NgCompiler 与增量编译:两种信息、两种模式
NgCompiler被源码注释称为 "The heart of the Angular Ivy compiler"(Ivy 编译器的心脏),定义于 core/src/compiler.ts。它是一个惰性编译器——在调用任一输出方法(如getDiagnostics)之前,不会执行任何实质编译工作。
Angular 编译器支持增量编译:借助上一次编译的信息来加速下一次编译。编译过程中,编译器会产出两大类信息:
| 信息类别 | 含义 | 例子 |
|---|---|---|
| 本地信息(local information) | 与单个文件/组件绑定 | 组件与指令的元数据 |
| 全局信息(global information) | 跨文件汇总的结果 | 物化后的 NgModule 作用域(reified NgModule scopes) |
围绕这两类信息,增量编译以两种方式管理:
- 常规代码变更:新建一个
NgCompiler,可以有选择地从旧实例继承本地信息,并且只在底层 TypeScript 文件真正发生变化的地方重算;而全局信息在每次编译时总是从头重新计算。 - 资源类变更(例如组件资源变化):整个
NgCompiler可以被整体复用,原地更新以吸收变更影响,无需重算其他任何信息。
注意两种模式的核心区别在于:是否需要新NgCompiler实例,还是可以复用旧实例。为了不让这种实现复杂度泄漏给调用方、避免调用方去精细管理NgCompiler的生命周期,整个过程被抽象为CompilationTicket:调用方先拿到一个 ticket(取决于本次变更的性质),再用 ticket 取回NgCompiler实例;在创建 ticket 时,编译器内部自行决定"复用旧实例"还是"新建实例"。
CompilationTicket 三兄弟与 fromTicket 分发
CompilationTicket是一个可辨识联合(discriminated union),源码位于 core/src/compiler.ts。其判别字段是枚举CompilationTicketKind,共有三种成员:
export enum CompilationTicketKind { Fresh, // 全新编译 IncrementalTypeScript, // 增量:TypeScript 源码变更 IncrementalResource, // 增量:仅组件资源变更 } export type CompilationTicket = | FreshCompilationTicket | IncrementalTypeScriptCompilationTicket | IncrementalResourceCompilationTicket;三种 ticket 的语义与构成:
FreshCompilationTicket:从零开始一次编译。携带完整的NgCompilerOptions、ts.Program、增量构建策略、程序驱动(ProgramDriver)等,不引用任何旧状态。IncrementalTypeScriptCompilationTicket:处理 TypeScript 代码变化。除了新ts.Program外,还携带旧程序对应的IncrementalCompilation状态。IncrementalResourceCompilationTicket:只包含被修改的组件资源文件集合与旧的NgCompiler实例本身——这正是"整体复用编译器"模式的载体。
源码提供了四个配套的工厂函数(均在 core/src/compiler.ts 中):
freshCompilationTicket(...):以无任何旧状态的方式创建全新编译的 ticket;incrementalFromCompilerTicket(...):根据旧NgCompiler实例 + 新ts.Program尽可能高效地构造 ticket;如果旧程序找不到对应的IncrementalState,会回退为 fresh ticket;incrementalFromStateTicket(...):从旧的ts.Program、IncrementalState与新的ts.Program直接构造 ticket;resourceChangeTicket(...):构造仅处理资源变更的IncrementalResourceCompilationTicket。
ticket 的解包逻辑集中在静态方法NgCompiler.fromTicket(core/src/compiler.ts):对Fresh与IncrementalTypeScript两种分支,它都new一个NgCompiler(区别只在于传入IncrementalCompilation.fresh(...)还是复用的incrementalCompilation);而对IncrementalResource分支,它直接在旧编译器上调用compiler.updateWithChangedResources(ticket.modifiedResourceFiles, ticket.perfRecorder)并返回同一个实例。
ngtsc/program.ts 恰好演示了消费方视角:当没有旧程序时调用freshCompilationTicket,否则调用incrementalFromCompilerTicket(oldProgram.compiler, ...)。
异步编译:analyzeAsync 与资源加载
在某些编译环境(例如 Angular CLI 中由 webpack 驱动的编译),部分编译输入只能异步产生。典型例子:styleUrls指向 SASS 文件时,编译 SASS 需要派生一个子 webpack 编译(child webpack compilation)才能完成。
为此 Angular 提供了异步加载此类资源的接口(对应ResourceHost语义)。使用该接口时,在NgCompiler创建之后需额外执行一个异步步骤——调用NgCompiler.analyzeAsync()并await其返回的Promise。该操作完成后,所有资源均已加载完毕,此后NgCompiler的其余 API 就可以同步使用。实现位于 core/src/compiler.ts,它会遍历 trait compiler 收集analyzeAsync(sf)产生的 Promise 并统一等待。与之对应的NgtscProgram.loadNgStructureAsync()也直接透传给NgCompiler(见 program.ts)。
包装 ts.CompilerHost:合成文件从哪来
Angular 编译会根据配置生成一批合成文件(synthetic files,即原本并非输入、也不存在于磁盘上的文件)。典型的包括:
- 请求 flat module 时生成的flat module index 文件(如
index.js/index.d.ts); - 支撑模板类型检查代码的
__ngtypecheck__.ts文件(README 中写作__ng_typecheck__.ts)。
这些文件并不存在于磁盘,但对ts.Program而言必须表现得像真实文件一样。解决方式是对外包装ts.CompilerHost(对ts.Program来说 Host 是对外界的抽象):用一个能在需要时"凭空提供"这些合成文件的实现去包装它。这正是NgCompilerHost的核心职责,实现于 core/src/host.ts。
DelegatingCompilerHost:全量委托,杜绝漏委托
core/src/host.ts 中的DelegatingCompilerHost是NgCompilerHost的基类,注释道出了它存在的动机:TypeScript 不断向ts.CompilerHost添加新的可选方法,委托型 Host 漏实现或漏转发这些可选方法不会触发类型错误,却会在运行时产生隐蔽故障。因此这里用类型体操保证"漏委托 = 编译期报错":如果未来ts.CompilerHost新增了未被转发的方法,该类会直接产生类型错误。getSourceFile与fileExists两个方法被刻意排除在委托之外,因为它们由NgCompilerHost亲自实现。
NgCompilerHost.wrap 的装配逻辑
静态工厂NgCompilerHost.wrap(delegate, inputFiles, options, oldProgram)(core/src/host.ts)把整个包装流程串起来:
- 计算根目录
rootDirs; - 注册每文件 shim 生成器
TypeCheckShimGenerator(用于模板类型检查文件); - 若配置了
flatModuleOutFile,则通过FlatIndexGenerator注册顶层 shim 生成器,并寻找 flat module 入口;找不到合法入口时产生错误码CONFIG_FLAT_MODULE_NO_INDEX的诊断; - 用
ShimAdapter统一管理 shim 的按需生成与缓存(maybeGenerate),并支持携带oldProgram做 shim 的增量复用; - 用
ShimReferenceTagger在用户文件被读取时打上引用标记(tag),以便类型检查程序能识别 shim 引用; - 组装并返回
NgCompilerHost。
构造出的 Host 暴露了ignoreForEmit(不应输出为 JS 的文件集合,多为ngtypecheck类 shim)、shimExtensionPrefixes、constructionDiagnostics(Host 构建期间产生的诊断)等接口;在getSourceFile中会优先询问 shim adapter 是否需要生成 shim,否则回落给被委托的 Host 并执行引用标记(host.ts);fileExists则同时回答"真实存在或任一 shim 生成器认识它"(host.ts)。
API 定义:按用途分层的选项体系
core包内散布着跨编译器使用的独立 API 定义,通过 api/index.ts 统一导出adapter、interfaces、options、public_options四组模块。
NgCompilerOptions:统一 TypeScript 与 Angular 的选项
最值得关注的接口是NgCompilerOptions,定义于 core/api/src/options.ts。它把 Angular 支持的各类编译选项与 TypeScript 自身的ts.CompilerOptions合并为一个整体:
export interface NgCompilerOptions extends ts.CompilerOptions, LegacyNgcOptions, // View Engine 遗留、仍被 Ivy 兼容消费的选项 BazelAndG3Options, // 面向 Bazel / monorepo 构建的选项 DiagnosticOptions, // 诊断类别配置 TypeCheckingOptions, // 模板类型检查及其严格度 TestOnlyOptions, // 仅测试使用的内部选项 I18nOptions, // i18n 支持 TargetOptions, // 目标相关选项 InternalOptions, // 编译器内部选项 MiscOptions { // 杂项 // 为兼容 ts.CompilerOptions 的索引签名而放宽 [prop: string]: any; }它可赋值给ts.CompilerOptions,同时被 transformers/api.ts 中的旧式CompilerOptions类型所实现(该类型还叠加了genDir、basePath、skipMetadataEmit、annotationsAs、enableResourceInlining等一代ngc遗留字段)。正如 README 指出的,各类选项按用途与支持层级被拆分进彼此独立的接口。
各类选项族逐层拆解
LegacyNgcOptions(View Engine 遗留,Ivy 向后兼容消费),见 core/api/src/public_options.ts:
flatModuleOutFile:生成指定名字的 flat module index 与对应的.metadata.json,适用于以 flat 形式打包的库(如@angular/core);要求files中只有一个.ts入口(或显式给定libraryIndex),否则报错;flatModuleId:导入 flat module 时使用的模块 id,仅在同时给出flatModuleOutFile时生效;strictInjectionParameters:构造参数注入类型无法确定时,默认产生告警,置true则升级为错误;preserveWhitespaces:是否在编译模板时移除空白文本节点,Angular 6 起默认false;allowEmptyCodegenFiles:已废弃,不再使用。
TypeCheckingOptions(Angular 模板类型检查严格度),见 core/api/src/public_options.ts:
typeCheckHostBindings:是否开启 host binding 的类型检查;strictTemplates:总开关,置true时隐含开启下方所有模板严格度开关(除非单独关闭),默认true;strictInputTypes:是否校验绑定表达式对指令/组件input字段的赋值类型,默认false;strictInputAccessModifiers:是否拦截对readonly/private/protectedinput 的绑定赋值,默认false,且依赖strictInputTypes开启;strictNullInputTypes:开启后对可能求值为null/undefined的绑定做严格空值检查(需 TSstrictNullChecks),默认false;strictAttributeTypes:是否校验被指令消费的文本属性(如<input matInput disabled>),默认false;strictSafeNavigationTypes:空安全导航a?.b的返回类型是否取严格类型而非any,默认false;strictDomLocalRefTypes:#ref局部模板引用变量是否按createElement结果推断而非any,默认false;strictOutputEventTypes:指令输出上$event是否按EventEmitter/Subject泛型推断,默认false;strictDomEventTypes:DOM 事件上$event是否按HTMLElementEventMap推断,默认false;strictContextGenerics:泛型组件在模板上下文类型中是否保留真实泛型参数(默认置any),默认false;strictLiteralTypes:模板中的对象/数组字面量使用推断类型还是any,默认false、strictTemplates开启后生效;strictStandalone:置true后禁止非 standalone 声明并使其成为构建错误。
DiagnosticOptions(诊断类别控制),见 core/api/src/public_options.ts,配合枚举DiagnosticCategoryLabel(warning/error/suppress,同文件):
extendedDiagnostics.defaultCategory:未在checks中覆盖的可配置诊断的默认类别,默认warning;extendedDiagnostics.checks:按扩展模板诊断名称到类别的映射,可逐项把某类诊断提级为错误或降级为忽略。
I18nOptions(国际化),见 core/api/src/public_options.ts:i18nInLocale(导入翻译的语言环境)、i18nOutFormat(导出格式xlf/xlf2/xmb)、i18nOutFile、i18nOutLocale、enableI18nLegacyMessageIdFormat(是否用旧的$localize消息 id,默认true)、i18nUseExternalIds。
BazelAndG3Options(monorepo / Bazel),见 core/api/src/public_options.ts:generateDeepReexports(生成 NgModule 对其指令/管道的私有再导出以支撑深路径导入)、onlyPublishPublicTypingsForNgModules(.d.ts中只列出公开导出的类型)、annotateForClosureCompiler、onlyExplicitDeferDependencyImports、generateExtraImportsInLocalMode、_experimentalAllowEmitDeclarationOnly、legacyOptionalChaining、enableTemplateSourceLocations。
TestOnlyOptions 与 InternalOptions(内部),见 core/api/src/options.ts:前者包括_useHostForImportGeneration、_enableTemplateTypeChecker、_compilePoisonedComponents、tracePerformance;后者包括supportTestBed(默认true,供 CLI 使用)、supportJitMode(默认true)、externalRuntimeStyles、_angularCoreVersion、_enableHmr、_enableSelectorless、_isAngularCoreCompilation。注意这些下划线开头选项均为@internal,不面向普通用户。
从 adapter 到 host:解耦出可插拔的适配层
NgCompilerAdapter(定义于 core/api/src/adapter.ts)是NgCompilerHost中被NgCompiler真正依赖的那部分ts.CompilerHost实现的子集,所以消费方既可以直接使用现成的NgCompilerHost,也可以自己实现NgCompilerAdapter。它要求提供entryPoint、constructionDiagnostics、ignoreForEmit、unifiedModulesHost、rootDirs等字段,并对文件来源做两类判别:
isShim(sf):文件是否是 Angular 加入编译的 shim(如ngfactory、ngtypecheck),用于把类型检查限定在用户文件上;isResource(sf):文件是否是资源文件(语言服务会把资源作为根文件加入项目时需要)。
配套的ExtendedTsCompilerHost(interfaces.ts)在ts.CompilerHost之上叠加了可选的ResourceHost(resourceNameToFileName、readResource、getModifiedResourceFiles、transformResource)与UnifiedModulesHost(fileNameToModuleName,用于 Bazel 这类对模块命名空间有统一视图的构建系统)。transformResource目前仅支持style类型资源(见ResourceHostContext的type: 'style'),其结果是携带content的TransformResourceResult——这正是 CLI 在 HMR 场景中为每个 style 生成确定性标识(order字段)所依赖的接口。
总结与进一步探索
core包把"Angular 编译如何融入 TypeScript 编译器"这一复杂命题,收敛成了三件清晰可用的产物:NgCompilerHost(向ts.Program呈现合成文件)、CompilationTicket/NgCompiler(管理从全新到增量再到资源热更新的完整生命周期)、NgCompilerOptions及细分选项接口(把 TS 与 Angular 的配置统一并分层)。README 中描述的每一处设计,都能在上层NgtscProgram的调用链里找到落点。
建议按如下顺序继续深读源码:
- 生命周期总览:core/src/compiler.ts(ticket 三兄弟、工厂函数、
NgCompiler.fromTicket); - 异步与发射:core/src/compiler.ts 与 program.ts;
- Host 包装细节:core/src/host.ts;
- 选项体系:core/api/src/public_options.ts 与 core/api/src/options.ts;
- 集成入口与测试:program.ts 与 test/compiler_spec.ts。
其中 compiler_spec.ts 覆盖了从全新编译到资源变更复用等关键行为的回归验证,是观察NgCompiler生命周期语义最直观的窗口。
【免费下载链接】angularDeliver web apps with confidence 🚀项目地址: https://gitcode.com/GitHub_Trending/an/angular
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考