ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Angular Ivy 编译器如何在 `.d.ts` 中存储元数据:局部编译与 Tree-Shaking 的权衡设计

Angular Ivy 编译器如何在 `.d.ts` 中存储元数据:局部编译与 Tree-Shaking 的权衡设计 Angular Ivy 编译器如何在.d.ts中存储元数据局部编译与 Tree-Shaking 的权衡设计【免费下载链接】angularDeliver web apps with confidence 项目地址: https://gitcode.com/GitHub_Trending/an/angularAngular Ivy 编译架构追求局部性locality使ngtsc无需全局信息即可单独编译某个组件或库但要生成可被 tree-shaking 正确裁剪的代码编译器仍必须了解被引用指令的选择器、输入输出等信息。为此Ivy 在库产物.d.ts文件中以静态类型成员如ɵdir、ɵmod编码元数据本文基于 packages/core/src/render3/STORING_METADATA_IN_D.TS.md 这份设计文档结合核心类型定义与ngtsc元数据读取实现完整还原这套机制的原理与真实形态。读完你将理解为什么.d.ts里会出现static ɵdir: i0.ɵɵDirectiveDeclaration...这类声明、各泛型参数代表什么、以及ngtsc在消费第三方库时如何把它们读回为结构化元数据。从metadata.json到 Ivy编译模型的两代变迁在 Ivy 之前的 View Engine 时代Angular 使用metadata.json文件保存 directive/component/pipe/ng-module 的元数据。ngc编译器随后基于全局分析从这些metadata.json生成.ngfactory.ts文件工厂文件再由运行时引导。这套模型有两个结构性特点元数据与实际代码分离存储.ts源码只含装饰器真正可执行的工厂逻辑由ngc在metadata.json之上全局推断后另行生成编译是全局性的为了生成NgModule工厂需要汇总该模块内所有声明、导入、导出的信息。Ivy 的设计哲学与之相反——追求 localityngtsc在编译某个组件、指令或库时原则上不应依赖超出当前文件之外的全局信息即可完成编译。这带来更快的增量编译、更好的库分发结构。设计文档原文如此表述Ivy strives for locality, which means thatngtscshould not need any global information in order to compile the system.Tree-Shaking 困境为什么纯局部编译不够不过上述纯局部编译在绝大多数场景下成立却在生成tree-shakable可摇树优化的代码时遇到了例外。为了生成可被打包器裁剪的代码ngtsc反而确实需要一些全局知识。文档给出了一个精简的破坏性示例。假设TooltipDirective与MyAppComponent共同被MyAppModule声明而ngtsc为组件生成的ɵɵdefineComponent会把 NgModule 声明列表原样塞进组件的directives数组Directive({ selector: [tooltip] }) export class TooltipDirective { // ngtsc generates this: static ɵdir ɵɵdefineDirective(...); } Component({ selector: app-root, template: Hello World! }) class MyAppComponent { // ngtsc generates this: static ɵdir ɵɵdefineComponent({ ... directives: [ // BREAKS TREE-SHAKING!!! // TooltipDirective included here because it was declared in the NgModule // ngtsc does not know it can be omitted. // Only way for ngtsc to know that it can omit TooltipDirective is if it knows // its selector and see if the selector matches the current components template. TooltipDirective ] }); } NgModule({ declarations: [MyAppComponent, TooltipDirective], bootstrap: [MyAppComponent], }) class MyAppModule { // ngtsc generates this: static ɵmod ɵɵdefineNgModule(...); }问题点正如注释所强调的MyAppComponent的模板是Hello World!其中并未使用TooltipDirective但ngtsc无法据此把TooltipDirective从directives数组中剔除因为要剔除它ngtsc必须先知道TooltipDirective的selector再拿 selector 去匹配当前组件模板判断二者确实无关而知道别的指令的 selector本身就意味着打破了 locality——这条信息不在当前文件的编译视野内。如果编译器无法剔除则该指令定义、其依赖都会被打包器保留tree-shaking 的目标就此落空。破例存储把 selector / inputs / outputs 塞进.d.ts文档明确指出既然打破 locality 是必要的那就必须为这些局部性信息例外找一个可靠的存放位置——因为ngtsc在编译应用时通常只能拿到第三方库编译后的产物而拿不到TooltipDirective的.ts源码。选定的落点是库产物中的.d.ts文件。做法是不再使用文档中简写的运行时代码而是把声明以静态类型成员的形式写入.d.ts例如class TooltipDirective { static ɵdir: DirectiveDeclarationTooltipDirective, [tooltip], , {}, {}, []; }.d.ts中这个静态成员的类型本身包含了编译器需要的全部元数据第一个类型参数是该指令类第二个是 selector后面依次是exportAs、inputs 映射、outputs 映射与 query 字段列表。ngtsc无需执行任何代码、也无需打开上游源码只要对这一行类型声明做纯静态的 AST 解析即可重新取得TooltipDirective的选择器与输入输出信息从而回到主流程继续做 tree-shaking 判断。注意上面取自设计文档的是示意写法当时的命名没有ɵɵ前缀当前仓库实际生成的声明形态前缀为ɵɵ可参考真实 golden 输出见下文。类型骨架ɵɵDirectiveDeclaration与各泛型参数设计文档中提到的DirectiveDeclaration在真实代码中以ɵɵDirectiveDeclaration的名字发布ɵɵ前缀是 Ivy 私有/内部符号约定但这些类型本身被标注为publicApi因为它们会出现在发布到 npm 的库.d.ts中。其定义位于 packages/core/src/render3/interfaces/public_definitions.ts文件头部注释即说明This file contains types that will be published to npm in library typings files.export type ɵɵDirectiveDeclaration T, Selector extends string, ExportAs extends string[], // string keys are for backwards compatibility with pre-16 versions. InputMap extends { [key: string]: string | {alias: string | null; required: boolean; isSignal?: boolean}; }, OutputMap extends {[key: string]: string}, QueryFields extends string[], // Optional as this was added to align the IsStandalone parameters // between directive and component declarations. NgContentSelectors extends never never, // Optional as this was added in Angular v14. All pre-existing directives // are not standalone. IsStandalone extends boolean false, HostDirectives never, IsSignal extends boolean false, unknown;逐参解释其编码语义类型参数含义说明T指令自身类型编译器据此关联回原类SelectorCSS 选择器如[tooltip]tree-shaking 匹配模板的依据ExportAsexportAs名称数组模板局部变量可引用的别名InputMapinputs 的键值映射v16 前值为字符串向后兼容v16 后可为对象{alias, required, isSignal}用于表达别名、必填输入与 signal inputOutputMapoutputs 的键值映射值为字符串含别名展开QueryFields查询字段列表ContentChild/ViewChild等声明字段NgContentSelectors内容选择器指令恒为never组件才可能携带见下文 ComponentDeclarationIsStandalone是否 standalonev14 引入默认falseHostDirectiveshost directives 列表读取失败时对应元数据会被标记为 poisonedIsSignal是否 signal-based由signal相关 API 引入注意这个类型别名的右侧是unknown——它从未被真正实例化为可执行代码。这些类型存在的唯一目的就是给ngtsc提供一个类型层面的、语法上可读的元数据编码容器。整条声明本质上是借 TypeScript 类型系统寄宿数据。兄弟声明类型Component / NgModule / Pipe / Factory同一文件中还定义了其他用于.d.ts元数据的类型共同构成库的完整自描述export type ɵɵComponentDeclaration T, Selector extends String, ExportAs extends string[], InputMap extends {[key: string]: string | {alias: string | null; required: boolean; isSignal?: boolean}}, OutputMap extends {[key: string]: string}, QueryFields extends string[], NgContentSelectors extends string[], // 组件独有的内容选择器ng-content IsStandalone extends boolean false, HostDirectives never, IsSignal extends boolean false, unknown; export type ɵɵNgModuleDeclarationT, Declarations, Imports, Exports unknown; export type ɵɵPipeDeclaration T, Name extends string | null, IsStandalone extends boolean false, unknown; export type ɵɵInjectorDeclarationT unknown; export type ɵɵFactoryDeclarationT, CtorDependencies extends CtorDependency[] (parent?: Typeany) any;各自对应.d.ts中的一个静态成员命名约定为ɵcmp— 组件声明ɵɵComponentDeclarationɵdir— 指令声明ɵɵDirectiveDeclarationɵpipe— 管道声明ɵɵPipeDeclarationɵmod— NgModule 声明ɵɵNgModuleDeclaration其泛型分别编码 declarations/imports/exports 三个引用列表ɵfac— 工厂声明ɵɵFactoryDeclaration编码构造函数依赖元数据CtorDependency可表达Attribute/Optional/Host/Self/SkipSelf等修饰符ɵinj— 注入器声明ɵɵInjectorDeclaration。这些类型从 packages/core/src/render3/index.ts 统一对外导出构成i0.ɵɵDirectiveDeclaration之类的完整引用路径库.d.ts顶部通常有import * as i0 from angular/core。读取端实现DtsMetadataReader如何把类型还原为元数据.d.ts只是载体真正逆向解析它的逻辑在编译器的元数据层。packages/compiler-cli/src/ngtsc/metadata/src/dts.ts 中定义了DtsMetadataReader其类注释点明了职责AMetadataReaderthat can read metadata from.d.tsfiles, which have static Ivy properties from an upstream compilation already.其内部原理值得逐条对照定位静态成员例如读取 NgModule 元数据时在类成员中寻找name ɵmod isStatic的成员dts.ts 中对应逻辑读取指令元数据则寻找ɵcmp或ɵdir管道寻找ɵpipe形状校验若成员无类型、不是类型引用、或类型参数数量不对例如ɵmod必须恰好 4 个类型参数一律返回null——宁可当作没有元数据也不猜测解包类型参数例如 NgModule 的 4 个参数[_, Declarations, Imports, Exports]会被逐一求值遇到never关键字则等价于空数组这是未提供的编码惯例否则借助PartialEvaluator.evaluateType把类型节点求值成对类的Reference列表指令/组件解析第 2 个参数读 selector第 3 个读exportAs第 4 个读 inputs经ClassPropertyMapping.fromMappedObject归一化映射第 5 个读 outputs第 6 个读 queries随后按参数数量向后兼容地读取ngContentSelectors第 7 位、isStandalone第 8 位、host directives第 9 位、isSignal第 10 位结构性指令判定一个指令若构造函数注入了来自angular/core的TemplateRef会被标记为 structural结构型指令Poisoned 机制当某条信息典型如 host directives 引用无法完整解析时元数据被标为isPoisoned调用方据此知道该条数据不完整避免基于残缺信息做出错误编译决策。代码注释原话为 If this cannot be done completely, the metadata is incomplete and poisoned。配套的单元测试见 packages/compiler-cli/src/ngtsc/metadata/test/dts_spec.ts其中以大量static ɵdir: i0.ɵɵDirectiveDeclaration...形式覆盖了各种输入输出组合、standalone、host directives、signal 等读取路径是理解解析行为的首选参考。真实产物从 golden 测试看.d.ts的最终形态仓库的 compliance golden 测试保留了 ngtsc 编译库代码后的精确输出是最直观的实证。下面是一个含exportAs的真实指令声明export_as_isolated.golden.d.tsimport * as i0 from angular/core; export declare class SomeDirective { static ɵfac: i0.ɵɵFactoryDeclarationSomeDirective, never; static ɵdir: i0.ɵɵDirectiveDeclarationSomeDirective, [some-directive], [someDir, otherDir], {}, {}, never, never, false, never; } export declare class MyModule { static ɵfac: i0.ɵɵFactoryDeclarationMyModule, never; static ɵmod: i0.ɵɵNgModuleDeclarationMyModule, never, never, never; static ɵinj: i0.ɵɵInjectorDeclarationMyModule; }可清晰读到ɵdir的第 3 个参数[someDir, otherDir]即exportAs别名ɵmod因该模块未使用内部列表而以never填充同时每个类都带ɵfac/ɵinj。而携带丰富 inputs 的真实样例mixed_model_types_isolated.golden.d.ts展示了 input 对象形态在 v16 的完整编码——别名、必填性与 signal 标记都藏在类型参数中static ɵdir: i0.ɵɵDirectiveDeclarationTestDir, never, never, { counter: { alias: counter; required: false; isSignal: true; }; modelWithAlias: { alias: alias; required: false; isSignal: true; }; decoratorInput: { alias: decoratorInput; required: false; }; ... }, { counter: counterChange; modelWithAlias: aliasChange; ... }, never, never, true, never;对照 public_definitions.ts 中InputMap的约束{alias, required, isSignal}对象形态与 golden 输出一一对应设计文档-类型定义-实际产物的链路完全闭合。standalone 场景的输出可再参考 standalone/directive_isolated.golden.d.ts其中第 8 个类型参数为true。设计取舍总结回顾这套机制其核心是一个清晰的权衡可归纳为三点妥协范围被刻意收窄文档指出selector、inputs、outputs 是 locality 规则的明确例外是为了让编译产物可 tree-shaking 而必须跨文件可见的最小集合.d.ts是信息载体而非执行代码声明类被发布到 npm 库的类型文件中.d.ts天然存在且无需额外分发物用静态成员类型存放元数据既零运行时开销又让ngtsc只需做语法级解析读取端与写入端解耦写入由 ngtsc 在编译库时负责产物以ɵcmp/ɵdir/ɵpipe/ɵmod等成员形式出现在.d.ts中读取由DtsMetadataReader负责二者通过泛型参数的位序这一隐式契约达成一致因此类型参数数量成为向后兼容的关键——读取端对缺位参数使用默认值并兼容旧产物。理解这层机制对排查两类问题尤其有价值一是库产物 tree-shaking 失效时可检查第三方包.d.ts中的ɵdir类型参数是否正确携带 selector 与 inputs/outputs二是升级后编译器行为变化时可对比不同版本 golden.d.ts中类型参数的位序差异。原始设计文档见 STORING_METADATA_IN_D.TS.md建议与类型定义、DtsMetadataReader实现及 dts_spec 测试一并阅读以获得从设计动机到实现细节的完整视图。【免费下载链接】angularDeliver web apps with confidence 项目地址: https://gitcode.com/GitHub_Trending/an/angular创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表