Angular框架解读--Ivy编译器之CLI编译器
更新日期:
作为“为大型前端项目”而设计的前端框架,Angular 其实有许多值得参考和学习的设计,本系列主要用于研究这些设计和功能的实现原理。本文围绕 Angular 的核心功能 Ivy 编译器,介绍其中CLI层面的编译器编译过程。
在 Angular 中实现了自己的编译器,来处理 TypeScript 编译器无法完全做到的一些事情。在 Ivy 编译器中,这部分的能力又做了进一步的提升,比如模板类型检查、模块依赖解析等等。
Ivy 编译器
在前面Angular 框架解读–Ivy 编译器整体设计一文中,我有介绍 Ivy 编译器主要包括两部分:
ngtsc
:作为主要的 Ivy 编译器,将 Angular 装饰器化为静态属性。ngcc
:作为兼容性的 Ivy 编译器,主要负责处理来自 NPM 的代码并生成等效的 Ivy 版本。
本文将会主要围绕ngtsc
该编译器进行介绍。
Angular 中的 AST 解析
要实现 AST 的解析和转换,离不开解析器。对于 Typescript 代码来说,编译器的整体流程为:
1 | |------------| |
该过程包括四个步骤:
- parse 解析:它是一个传统的递归下降解析器,稍微调整以支持增量解析,它发出一个抽象语法树 (AST),有助于识别文件中导入了哪些文件。
- type-check 类型检查器:类型检查器构建一个符号表,然后对文件中的每个表达式进行类型分析,报告它发现的错误。
- transform 转换:转换步骤是一组 AST 到 AST 转换,它们执行各种任务,例如删除类型声明、将模块和类声明降低到 ES5、将异步方法转换为状态机等。
- print 打印:TS 到 JS 的实际转换是整个过程中最昂贵的操作。
在了解 Angular 是如何处理之前,我们需要知道,对 TypeScript 编译器 API 的任何使用都遵循一个多步骤过程,包括:
- 一个
ts.CompilerHost
被创建 ts.CompilerHost
加上一组“根文件”,用于创建ts.Program
,ts.Program
用于收集各种诊断(类型检查)ts.Program
被要求emit
,并生成 JavaScript 代码
将 Angular 编译集成到此过程中的编译器遵循非常相似的流程,但有一些额外的步骤:
- 一个
ts.CompilerHost
被创建 ts.CompilerHost
包含在NgCompilerHost
中,它将 Angular 特定文件添加到编译中ts.Program
是从NgCompilerHost
及其增强的根文件集创建的- 一个
CompilationTicket
被创建,可选择合并来自先前编译运行的任何状态 NgCompiler
是使用CompilationTicket
创建的- 诊断信息可以正常从
ts.Program
收集,也可以从NgCompiler
收集 - 在发射之前,调用
NgCompiler.prepareEmit
以检索需要馈送到ts.Program.emit
的 Angular 转换器 - 使用上面的 Angular 转换器在
ts.Program
上调用发射,它生成带有 Angular 扩展的 JavaScript 代码
在这些 Angular 特定的步骤中,主要进行几件事:
- 会将特定于 Angular 的文件添加到编译过程中,比如
NgModele
、Component
的解析。 - 修改生成的
d.ts
,来保存 Angular 中模块和文件间的依赖关系。 - 会增加 Angular 中的类型校验,包括
<tmeplate>
模板的类型校验。
而在自定义 TypeScript 编译器中执行 Angular 编译,主要依赖于NgCompiler
,我们来看一下核心的一些方法:
1 | export class NgCompiler { |
可见,NgCompiler
主要负责将 Angular 编译集成到 TypeScript 编译器的编译流程中,并支持了上述提到的错误信息诊断(类型检查)、依赖关系检索,其中的设计还支持了增量编译、异步编译等能力。
ngtsc 编译器
ngtsc
是一个 Typescript-to-Javascript 编译器。它是一个最小包装器,包裹在tsc
之外,而tsc
中则包含一系列的 Angular 变换。
编译器流程
和tsc
一样,当ngtsc
开始运行时,它首先解析tsconfig.json
文件,然后创建一个ts.Program
。在上述转换可以运行之前,需要进行几件事情:
- 为包含修饰符的输入源文件收集元数据
@Component
装饰器中列出的资源文件必须异步解析- 例如 CLI 中,可能希望运行的 WebPack 以产生
.css
输入到styleUrls
的属性@Component
- 例如 CLI 中,可能希望运行的 WebPack 以产生
- 运行诊断程序,这会创建
TypeChecker
并触及程序中的每个节点(一个相当昂贵的操作)
因为资源加载是异步的(特别是可能通过子进程并发),所以最好在做任何昂贵的事情之前启动尽可能多的资源加载。
ngtsc
的运行入口位于NgtscProgram
中,可直接替代传统的 View Engine 编译器到诸如命令行main()
函数或 Angular 之类的工具命令行界面。
1 | export class NgtscProgram implements api.Program { |
编译器流程如下所示:
- 创建
ts.Program
。 - 扫描源文件以查找具有微不足道的可检测
@Component
注释的顶级声明,这避免了创建TypeChecker
。- 对于每个具有
templateUrlor
的此类声明styleUrls
,启动该 URL 的资源加载并将加入Promise
队列
- 对于每个具有
- 获取诊断信息并报告任何初始错误消息。此时,
TypeChecker
已准备就绪。 - 对
@Component
注释进行彻底扫描,使用TypeChecker
和元数据系统来解析任何复杂的表达式。 - 等待所有资源得到解决。
- 计算需要应用的一组变换。
- 启动
Tsickle
发射,它运行变换。 - 在
.d.ts
文件的发出回调期间,重新解析发出的.d.ts
并合并来自Angular
编译器的任何请求更改。
Angular 编译涉及将 Angular 装饰器转换为静态定义字段。在构建时,这是在 TypeScript 编译的整个过程中完成的,其中 TypeScript 代码经过类型检查,然后降级为 JavaScript 代码。在此过程中,还可以生成特定于 Angular 的诊断。
增量编译
前面我们介绍了 Ivy 编译器的一些特性,其中包括了通过增加增量编译,来缩短构建时间。
作为在 TypeScript 编译器中执行 Angular 编译的核心 API,NgCompiler
的每个实例都支持单个编译,因此也支持增量编译。
Angular 编译器能够进行增量编译,其中来自先前编译的信息用于加速下一次编译。在编译期间,编译器产生两种主要信息:本地信息(如组件和指令元数据)和全局信息(如具体化的NgModule
范围)。增量编译通过两种方式进行管理:
- 对于大多数更改,新的
NgCompiler
可以有选择地从以前的实例继承本地信息,并且只需要在底层 TypeScript 文件发生更改的地方重新计算它。在这种情况下,全局信息总是从头开始重新计算。 - 对于特定的更改,例如组件资源中的更改,
NgCompiler
可以整体重用,并更新以合并此类更改的影响,而无需重新计算任何其他信息。
请注意,这两种模式在是否需要新NgCompiler
实例或是否可以重用之前的实例方面有所不同。为了防止泄漏这种实现的复杂性并保护消费者不必管理NgCompiler
如此具体的生命周期,这个过程通过CompilationTicket
进行了抽象。
1 | export type CompilationTicket = |
CompilationTicket
用于初始化(或更新)NgCompiler
实例,该实例为 Angular 编译器的核心。CompilationTicket
抽象了编译的起始状态,并允许独立于任何增量编译生命周期管理NgCompiler
。
消费者首先获得一个CompilationTicket
(取决于传入更改的性质),然后使用该票据获取NgCompiler
实例。在创建CompilationTicket
时,编译器可以决定是重用旧NgCompiler
实例还是创建新实例。
异步编译
在某些编译环境(例如 Angular CLI 中的 Webpack 驱动编译)中,编译的各种输入只能以异步方式生成。例如,styleUrls
链接到 SASS 文件的 SASS 编译需要产生一个子 Webpack 编译。为了支持这一点,Angular 有一个异步接口来加载这些资源。
如果使用此接口,则NgCompiler
创建后的另一个异步步骤是调用NgCompiler.analyzeAsync
并等待其Promise
:
1 | export class NgtscProgram implements api.Program { |
此操作完成后,所有资源均已加载,其余NgCompilerAPI
可以同步使用。
总结
Angular 是一套大而全的解决方案,想必大家早已对此有所了解。但实际上 Angular 做了很多深度的设计和能力,包括给开发者更好的体验,比如模板类型检查中,是如何将这些 Angular 特定的类型检查能力添加到 TypeScript 编译过程中,并且能通过文件映射能准确反馈给用户具体的代码位置,这些都是作为开发者的我未曾考虑过的。
感觉 Angular 里面还有特别多值得学习的东西。
参考
码生艰难,写文不易,给我家猪囤点猫粮了喵~
查看Github有更多内容噢:https://github.com/godbasin
更欢迎来被删的前端游乐场边撸猫边学前端噢
如果你想要关注日常生活中的我,欢迎关注“牧羊的猪”公众号噢