转载https://segmentfault.com/a/1190000011562077
Angular编译机制
前言
这是我用来进行实验的代码,它是基于quickstart项目,并根据aot文档修改得到的。各位可以用它来进行探索,也可以自己基于quickstart进行修改(个人建议后者)。
2018年2月17日更新:最近又做了2个小Demo用来研究Angular的编译和打包,基于Angular5,一个使用rollup,一个使用webpack,(rollup目前无法做到Angular的lazy loading)。不仅项目文件结构非常简洁,而且使用ngc(Angular compiler)的输出作为打包的输入,这意味着:你不仅可以修改ts代码然后查看ngc输出有何变化,而且可以修改ngc输出然后查看最终的应用会如何运行,类似于玩“汇编”的感觉,我相信这能加深学习者对Angular的理解甚至开启源码学习之路。
什么是Angular编译
Angular应用由许多组件、指令、管道等组成,并且每个组件有自己的HTML模板,它们按照Angular规定的语法进行组织。然而Angular的语法并不能被浏览器直接理解。为了让浏览器能运行我们写的项目,这些组件、指令、管道和HTML模板必须先被Angular编译器编译成浏览器可执行的Javascript。
为什么Angular需要编译
这个问题相当于:“为什么不让用户像以前一样,写浏览器能直接执行的JS代码?”
-
对于Angular来说,简练的js代码执行起来不高效(从时间、内存、文件大小的角度),高效的js代码写起来不简练。为了让Angular既易于书写又能拥有极高的效率,我们可以先用一种简练的Angular语法表达我们语义,然后让编译器根据我们写的源代码编译出同等语义的、真正用来执行的、但难以阅读和手写的js代码。
内存、文件大小的效率提升比较容易理解,Angular编译器会输出尽可能优化、简洁(牺牲可读性)的代码。时间上的效率提升很大程度来自于Angular2的变化检测代码对于Javascript虚拟机更友好,简单来说就是为每个组件都生成一段自己的变化检测代码,直接对这个组件的每一个绑定逐一检查,而不是像AngularJS一样,对所有组件都同一个通用的检测算法。可以阅读参考资料5的 Why we need compilation in Angular? 段落。
- 编译可以让Angular与客户端(浏览器)解耦。也就是说,可以用另一种编译器,输入相同的Angular项目代码,输出能在手机上运行的APP!Angular首页就是这样介绍的:"Learn one way to build applications with Angular and reuse your code and abilities to build apps for any deployment target. For web, mobile web, native mobile and native desktop."
Angular编译器(ngc)
普通的typescript项目需要用typescript编译器(tsc)来编译,而ngc是专用于Angular项目的tsc替代者。它内部封装了tsc,还额外增加了用于Angular的选项、输出额外的文件。
截图自ng-conf视频,除以上三种输出之外ngc还可以产生ngfactory
、ngstyle
文件。如视频中所说,图中三种输出是Angular library(第三方库,比如Angular Material)需要发布的,ngfactory
、ngstyle
应该由library的使用者在编译自己的Angular项目的时候产生(tsconfig
中的angularCompilerOptions.skipTemplateCodegen
字段可以控制AOT是否产生这2种文件)。
根据最新的讲座,在AOT模式下输出的是ts代码而不是js代码。在JIT模式下直接输出js代码。tsc读取tsconfig配置文件的
compilerOptions
部分,ngc读取angularCompilerOptions
部分。Angular文档:There is actually only one Angular compiler. The difference between AOT and JIT is a matter of timing and tooling.
Angular编译有两种:Ahead-of-time (AOT) 和 just-in-time (JIT)。但是实际上使用的是同一个编译器,AOT和JIT的区别只是编译的时机和编译所使用的工具库。Angular文档对.metadata.json的解释。
.metadata.json
文件是Angular编译器产生的,它用json的形式记录了源.ts
中decorator信息、依赖注入信息,从而Angular二次编译时不再需要从.ts
中提取metadata(从而不需要.ts
源代码的参与)。二次编译的情形:第三方库作者进行第一次编译,产生图中展示的三种文件并发布(不需要发布.ts
源代码),然后,库的用户将这些库文件与自己的项目一起编译(第二次编译),产生可运行的应用。如果你是Angular library的开发者并且希望你的library支持用户进行AOT,那么你需要发布.metadata.json
和.js
文件,否则,你不需要.metadata.json
。
just-in-time (JIT)
JIT一般经历的步骤:
- 程序员用Typescript和Angular语法编写源代码。
- 用
tsc
将Typescript代码(包括我们写的,以及Angular框架、Angular编译器代码)编译成JavaScript代码。 - 打包、混淆、压缩。
- 将得到的bundle以及其他需要的静态资源部署到服务器上。
以下是发生在客户端(用户浏览器)的步骤: - 客户端下载bundle,开始执行这些JavaScript。
-
Angular启动,Angular调用Angular编译器,将Angular源代码(Javascript代码)编译成浏览器真正执行的Javascript目标代码(也就是后面会讲的
NgFactories
)。Angular的启动源于main.js(由main.ts编译得到)的执行。
- 创建各种组件的实例(通过
NgFactories
),产生了我们看到的应用。
Ahead-of-time (AOT)
AOT一般经历的步骤:
- 程序员用Typescript和Angular语法编写源代码。
-
用
ngc
编译应用,其中包括两步:- 2.1 将Angular源代码(此时是Typescript代码)编译,输出Typescript目标代码(也就是后面会讲的
NgFactories
)。这一步是Angular编译的核心,我们在后文仔细研究。后面将反复提及“AOT步骤2.1”。 - 2.2
ngc
调用tsc
将应用的Typescript代码编译成Javascript代码(包括2.1产生的、我们写的源代码、Angular框架的Typescript代码)。
将ts编译为js的过程中,能发现Angular程序中的类型错误,比如class没有定义a属性你却去访问它。
哪些代码是需要编译的?根据tsconfig-aot.json的"files"字段,以app.module.ts
和main.ts
为起点,直接或间接import
的所有.ts
都需要编译。当然,Lazy loading module由于没有被import
而不会被加入bundle中,但是Angular AOT Webpack 插件会智能地找到Lazy loading module并将它编译成另外一个bundle。 - 2.1 将Angular源代码(此时是Typescript代码)编译,输出Typescript目标代码(也就是后面会讲的
-
摇树优化(Tree shaking),将没有用的代码删掉。
Angular文档:Tree shaking and AOT compilation are separate steps. Tree shaking can only target JavaScript code(目前的工具只能对Javascript代码进行摇树优化). AOT compilation converts more of the application to JavaScript, which in turn makes more of the application "tree shakable".
- 打包、混淆、压缩。
- 将得到的bundle以及其他需要的静态资源部署到服务器上。
以下是发生在客户端(用户浏览器)的步骤:
- 客户端下载bundle,开始执行这些JavaScript。
- Angular启动,由于bundle中已经有了
NgFactories
的Javascript代码,因此Angular直接用它们来创建各种组件的实例,产生了我们看到的应用。
Angular编译(JIT步骤6、AOT步骤2.1)的顺序
Angular编译器输入NgModule,编译其中的entryComponents指定的那些组件。对每个entryComponents都产生对应的ComponentFactory类型,保存在一个ComponentFactoryResolver类型中。最后输出NgModuleFactory类型。
我们知道,组件的模板中可以引用别的组件,从而构成了组件树。entryComponents就是组件树的根节点,每一个entryComponents都引申出一颗组件树。编译器从一个entryComponent出发,就能编译到组件树中的所有组件。虽然编译器为每个组件都生成了工厂函数,但是只需要将entryComponents的工厂函数保存在ComponentFactoryResolver对象中就够了,因为父组件工厂在创建实例的时候会递归调用子组件的工厂。因此运行时只需要调用根组件的工厂函数,就能得到一颗组件树。为什么产生的都是类型而不是对象?因为编译是静态的,编译器只能依赖于静态的数据(编译器只是静态地提取分析decorators和metadata;编译器不会执行源代码、也不知道我们定义的那些函数是干什么的),并且产生静态的结果(输出客户端要执行代码),只有类型这种静态的信息能够用代码来表示。而对象是动态的,它是运行时在内存中的一段数据,不能用ts/js代码来表示。
NgModules是编译组件的上下文:编译一个组件的时候,除了需要本组件的模板和metadata信息,编译器还需要知道当前NgModule中声明的其他组件、指令、管道,因为在这个组件的template中可能使用它们。所以,不像AngularJS,组件、指令、管道不是全局有效的,只有声明(declare)了它们的NgModule,或者import它们所在的NgModule,才能使用它们,否则编译报错。这有助于在大型项目中隔离功能模块、防止命名(selector)冲突。
在运行时,Angular会使用NgModuleFactory创建出模块的实例:NgModuleRef。
在NgModuleRef中有一个重要的属性:componentFactoryResolver,它就是刚才那个ComponentFactoryResolver类型的实例,给它一个组件类(类型在运行时的形态,即function),它会给你返回对应的ComponentFactory类型实例。
AOT步骤2.1产生的NgFactories
NgFactories
是浏览器真正执行的代码(如果是Typescript形式的,则需要先编译成Javascript)。每个组件、NgModule都会生成对应的工厂。组件工厂中包含了创建组件、渲染组件——这涉及DOM操作、执行变化检测——获取oldValue和newValue并对比、销毁组件的逻辑。当需要产生某个组件的实例的时候,Angular用组件工厂来实例化一个组件对象。NgModule
实例也是Angular用NgModule factory来创建的。
Angular文档:JIT compilation generates these same NgFactories in memory where they are largely invisible. AOT compilation reveals them as separate, physical files.
其实无论是AOT还是JIT,angular-complier都输出NgFactories
,只不过AOT产生的输出到*.ngfactory.ts
文件中,JIT产生的输出到客户端内存中。Angular文档:Each component factory creates an instance of the component at runtime by combining the original class file and a JavaScript representation of the component‘s template. Note that the original component class is still referenced internally by the generated factory.
每一个component factory可以在运行时创建组件的实例,通过组合组件类(比如classAppComponent
)和组件模板的JavaScript表示。注意,在*.ngfactory.ts
中,仍然引用源文件中的组件类(见下例)。
这是步骤2.1产生的其中一个文件app.component.ngfactory.ts
:
/**
* @fileoverview This file is generated by the Angular template compiler.
* Do not edit.
* @suppress {suspiciousCode,uselessCode,missingProperties,missingOverride}
*/
/* tslint:disable */
import * as i0 from ‘./app.component.css.shim.ngstyle‘;
import * as i1 from ‘@angular/core‘;
import * as i2 from ‘../../../src/app/app.component‘;
import * as i3 from ‘@angular/common‘;
import * as i4 from ‘@angular/forms‘;
import * as i5 from ‘./child1.component.ngfactory‘;
import * as i6 from ‘../../../src/app/child1.component‘;
const styles_AppComponent:any[] = [i0.styles];
export const RenderType_AppComponent:i1.RendererType2 = i1.?crt({encapsulation:0,styles:styles_AppComponent,
data:{}});
function View_AppComponent_1(_l:any):i1.?ViewDefinition {
return i1.?vid(0,[(_l()(),i1.?eld(0,(null as any),(null as any),1,‘h1‘,([] as any[]),
(null as any),(null as any),(null as any),(null as any),(null as any))),(_l()(),
i1.?ted((null as any),[‘This is heading‘]))],(null as any),(null as any));
}
function View_AppComponent_2(_l:any):i1.?ViewDefinition {
return i1.?vid(0,[(_l()(),i1.?eld(0,(null as any),(null as any),1,‘div‘,([] as any[]),
(null as any),(null as any),(null as any),(null as any),(null as any))),(_l()(),
i1.?ted((null as any),[‘‘,‘‘]))],(null as any),(_ck,_v) => {
const currVal_0:any = _v.context.$implicit;
_ck(_v,1,0,currVal_0);
});
}
export function View_AppComponent_0(_l:any):i1.?ViewDefinition {
return i1.?vid(0,[(_l()(),i1.?eld(0,(null as any),(null as any),1,‘button‘,([] as any[]),
(null as any),[[(null as any),‘click‘]],(_v,en,$event) => {
var ad:boolean = true;
var _co:i2.AppComponent = _v.component;
if ((‘click‘ === en)) {
const pd_0:any = ((<any>_co.toggleHeading()) !== false);
ad = (pd_0 && ad);
}
return ad;
},(null as any),(null as any))),(_l()(),i1.?ted((null as any),[‘Toggle Heading‘])),
(_l()(),i1.?ted((null as any),[‘\n‘])),(_l()(),i1.?and(16777216,(null as any),
(null as any),1,(null as any),View_AppComponent_1)),i1.?did(16384,(null as any),
0,i3.NgIf,[i1.ViewContainerRef,i1.TemplateRef],{ngIf:[0,‘ngIf‘]},(null as any)),
(_l()(),i1.?ted((null as any),[‘\n\n‘])),(_l()(),i1.?eld(0,(null as any),(null as any),
1,‘h3‘,([] as any[]),(null as any),(null as any),(null as any),(null as any),
(null as any))),(_l()(),i1.?ted((null as any),[‘List of Heroes‘])),(_l()(),
i1.?ted((null as any),[‘\n‘])),(_l()(),i1.?and(16777216,(null