angular编译机制

 

转载https://segmentfault.com/a/1190000011562077

Angular编译机制

前言

http://www.cnblogs.com/ztwBlog/p/6209759.htmlcss

这是我用来进行实验的代码,它是基于quickstart项目,并根据aot文档修改获得的。各位能够用它来进行探索,也能够本身基于quickstart进行修改(我的建议后者)。html

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。webpack

为何Angular须要编译

这个问题至关于:“为何不让用户像之前同样,写浏览器能直接执行的JS代码?”git

  1. 对于Angular来讲,简练的js代码执行起来不高效(从时间、内存、文件大小的角度),高效的js代码写起来不简练。为了让Angular既易于书写又能拥有极高的效率,咱们能够先用一种简练的Angular语法表达咱们语义,而后让编译器根据咱们写的源代码编译出同等语义的、真正用来执行的、但难以阅读和手写的js代码。程序员

    内存、文件大小的效率提高比较容易理解,Angular编译器会输出尽量优化、简洁(牺牲可读性)的代码。时间上的效率提高很大程度来自于Angular2的变化检测代码对于Javascript虚拟机更友好,简单来讲就是为 每一个组件都生成一段本身的变化检测代码,直接对这个组件的每个绑定逐一检查,而不是像AngularJS同样,对全部组件都同一个 通用的检测算法。能够阅读 参考资料5的  Why we need compilation in Angular? 段落。
  2. 编译可让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还能够产生ngfactoryngstyle文件。如视频中所说,图中三种输出是Angular library(第三方库,好比Angular Material)须要发布的,ngfactoryngstyle应该由library的使用者在编译本身的Angular项目的时候产生(tsconfig中的angularCompilerOptions.skipTemplateCodegen字段能够控制AOT是否产生这2种文件)。
github

根据 最新的讲座,在AOT模式下输出的是ts代码而不是js代码。在JIT模式下直接输出js代码。

tsc读取tsconfig配置文件compilerOptions部分,ngc读取angularCompilerOptions部分。web

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.jsontypescript

just-in-time (JIT)

JIT通常经历的步骤:

  1. 程序员用Typescript和Angular语法编写源代码。
  2. tsc将Typescript代码(包括咱们写的,以及Angular框架、Angular编译器代码)编译成JavaScript代码。
  3. 打包、混淆、压缩。
  4. 将获得的bundle以及其余须要的静态资源部署到服务器上。
    如下是发生在客户端(用户浏览器)的步骤:
  5. 客户端下载bundle,开始执行这些JavaScript。
  6. Angular启动,Angular调用Angular编译器,将Angular源代码(Javascript代码)编译成浏览器真正执行的Javascript目标代码(也就是后面会讲的NgFactories)。

    Angular的启动源于main.js(由main.ts编译获得)的执行。
  7. 建立各类组件的实例(经过NgFactories),产生了咱们看到的应用。

Ahead-of-time (AOT)

AOT通常经历的步骤:

  1. 程序员用Typescript和Angular语法编写源代码。
  2. 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.tsmain.ts为起点,直接或间接 import的全部 .ts都须要编译。固然,Lazy loading module因为没有被 import而不会被加入bundle中,可是 Angular AOT Webpack 插件会智能地找到Lazy loading module并将它编译成另一个bundle。
  3. 摇树优化(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".
  4. 打包、混淆、压缩。
  5. 将获得的bundle以及其余须要的静态资源部署到服务器上。

如下是发生在客户端(用户浏览器)的步骤:

  1. 客户端下载bundle,开始执行这些JavaScript。
  2. Angular启动,因为bundle中已经有了NgFactories的Javascript代码,所以Angular直接用它们来建立各类组件的实例,产生了咱们看到的应用。

Angular编译(JIT步骤六、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能够在运行时建立组件的实例,经过组合组件类(好比class AppComponent)和组件模板的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 as any),(null as any), 1,(null as any),View_AppComponent_2)),i1.ɵdid(802816,(null as any),0,i3.NgForOf, [i1.ViewContainerRef,i1.TemplateRef,i1.IterableDiffers],{ngForOf:[0,'ngForOf']}, (null as any)),(_l()(),i1.ɵted((null as any),['\n\n'])),(_l()(),i1.ɵeld(0, (null as any),(null as any),1,'h5',([] as any[]),(null as any),(null as any), (null as any),(null as any),(null as any))),(_l()(),i1.ɵted((null as any), ['my name: ',''])),(_l()(),i1.ɵted((null as any),['\n'])),(_l()(),i1.ɵeld(0, (null as any),(null as any),5,'input',[['type','text']],[[2,'ng-untouched', (null as any)],[2,'ng-touched',(null as any)],[2,'ng-pristine',(null as any)], [2,'ng-dirty',(null as any)],[2,'ng-valid',(null as any)],[2,'ng-invalid', (null as any)],[2,'ng-pending',(null as any)]],[[(null as any),'ngModelChange'], [(null as any),'input'],[(null as any),'blur'],[(null as any),'compositionstart'], [(null as any),'compositionend']],(_v,en,$event) => { var ad:boolean = true; var _co:i2.AppComponent = _v.component; if (('input' === en)) { const pd_0:any = ((<any>i1.ɵnov(_v,16)._handleInput($event.target.value)) !== false); ad = (pd_0 && ad); } if (('blur' === en)) { const pd_1:any = ((<any>i1.ɵnov(_v,16).onTouched()) !== false); ad = (pd_1 && ad); } if (('compositionstart' === en)) { const pd_2:any = ((<any>i1.ɵnov(_v,16)._compositionStart()) !== false); ad = (pd_2 && ad); } if (('compositionend' === en)) { const pd_3:any = ((<any>i1.ɵnov(_v,16)._compositionEnd($event.target.value)) !== false); ad = (pd_3 && ad); } if (('ngModelChange' === en)) { const pd_4:any = ((<any>(_co.myName = $event)) !== false); ad = (pd_4 && ad); } return ad; },(null as any),(null as any))),i1.ɵdid(16384,(null as any),0,i4.DefaultValueAccessor, [i1.Renderer2,i1.ElementRef,[2,i4.COMPOSITION_BUFFER_MODE]],(null as any), (null as any)),i1.ɵprd(1024,(null as any),i4.NG_VALUE_ACCESSOR,(p0_0:any) => { return [p0_0]; },[i4.DefaultValueAccessor]),i1.ɵdid(671744,(null as any),0,i4.NgModel,[[8,(null as any)], [8,(null as any)],[8,(null as any)],[2,i4.NG_VALUE_ACCESSOR]],{model:[0, 'model']},{update:'ngModelChange'}),i1.ɵprd(2048,(null as any),i4.NgControl, (null as any),[i4.NgModel]),i1.ɵdid(16384,(null as any),0,i4.NgControlStatus, [i4.NgControl],(null as any),(null as any)),(_l()(),i1.ɵted((null as any), ['\n\n'])),(_l()(),i1.ɵeld(0,(null as any),(null as any),1,'h5',([] as any[]), (null as any),(null as any),(null as any),(null as any),(null as any))), (_l()(),i1.ɵted((null as any),['',''])),(_l()(),i1.ɵted((null as any),['\n\n'])), (_l()(),i1.ɵeld(0,(null as any),(null as any),1,'child1',([] as any[]),(null as any), (null as any),(null as any),i5.View_Child1Component_0,i5.RenderType_Child1Component)), i1.ɵdid(49152,(null as any),0,i6.Child1Component,([] as any[]),{ipt:[0,'ipt']}, (null as any)),(_l()(),i1.ɵted((null as any),['\n']))],(_ck,_v) => { var _co:i2.AppComponent = _v.component; const currVal_0:any = _co.showHeading; _ck(_v,4,0,currVal_0); const currVal_1:any = _co.heroes; _ck(_v,10,0,currVal_1); const currVal_10:any = _co.myName; _ck(_v,18,0,currVal_10); const currVal_12:any = _co.myName; _ck(_v,26,0,currVal_12); },(_ck,_v) => { var _co:i2.AppComponent = _v.component; const currVal_2:any = _co.myName; _ck(_v,13,0,currVal_2); const currVal_3:any = i1.ɵnov(_v,20).ngClassUntouched; const currVal_4:any = i1.ɵnov(_v,20).ngClassTouched; const currVal_5:any = i1.ɵnov(_v,20).ngClassPristine; const currVal_6:any = i1.ɵnov(_v,20).ngClassDirty; const currVal_7:any = i1.ɵnov(_v,20).ngClassValid; const currVal_8:any = i1.ɵnov(_v,20).ngClassInvalid; const currVal_9:any = i1.ɵnov(_v,20).ngClassPending; _ck(_v,15,0,currVal_3,currVal_4,currVal_5,currVal_6,currVal_7,currVal_8,currVal_9); const currVal_11:any = _co.someText; _ck(_v,23,0,currVal_11); }); } export function View_AppComponent_Host_0(_l:any):i1.ɵViewDefinition { return i1.ɵvid(0,[(_l()(),i1.ɵeld(0,(null as any),(null as any),1,'my-app',([] as any[]), (null as any),(null as any),(null as any),View_AppComponent_0,RenderType_AppComponent)), i1.ɵdid(49152,(null as any),0,i2.AppComponent,([] as any[]),(null as any),(null as any))], (null as any),(null as any)); } export const AppComponentNgFactory:i1.ComponentFactory<i2.AppComponent> = i1.ɵccf('my-app', i2.AppComponent,View_AppComponent_Host_0,{},{},([] as any[]));
变量名是否是很奇怪?这是为了防止命名冲突,因此在export的时候增长了一些特殊的字符,这些名字表明什么能够在 codegen_private_exports.tsidentifiers.ts中找到。

能够看出,在app.component.ngfactory.tsimport了咱们写的app.component.ts文件。更具体地说,是引用了其中的AppComponent类来做为变量_co的类型,你能够看看代码中的变量i2在哪里被使用。

_co是"context"的缩写。context(上下文)是 组件类在运行时实例化的对象(好比经过 new AppComponent())。组件类彻底是由Angular开发者编写的,Angular用context中的数据来渲染template(建立view)、更新view。
  • "View_AppComponent_"+数字 - the internal component,负责(根据template)渲染出组件的视图,和进行变化检测。

    在这篇文章(以及多数前端相关的文章),渲染的意思是构建出DOM树,DOM是Javascript控制Web应用显示的接口。
  • "View_AppComponent_Host_"+数字 - the internal host component,负责渲染出宿主元素<my-app></my-app>,而且使用"the internal component"管理组件的内部视图。
  • AppComponentNgFactory - 类型是ComponentFactory<AppComponent>。使用"the internal host component"来实例化组件(见 ComponentRef API)。

如下图片表示了*.component.ngfactory.ts中各类对象之间的关系:

为何在模板中只能访问public属性

若是在AppComponent中定义属性private someText = 'hahaha';而后在template中这样绑定{{someText}},那么在进行AOT编译的时候会报错(更具体地说,是步骤2.2),将private去掉之后又能够成功进行AOT编译。
这是由于在app.component.ngfactory.ts中,经过const currVal_11:any = _co.someText;这样的方式访问context(上下文对象)的属性,因此若是someTextAppComponent的private属性,那么tsc在编译的时候就会报错。

若是经过JIT方式编译,在模板中访问private属性不会出现问题。前面说过JIT直接生成Javascript代码,不区分private和public。

若是你实在是既要在模板中访问某属性,又要将这个属性设置为private(处于封装性的考虑),你能够看看参考资料5的"AoT and encapsulation"章节。

AOT步骤2.1如何解析文件的metadata

Angular编译器经过metadata中提供的信息,来生成组件/NgModule的工厂。

Angular编译器是如何解析文件的metadata的呢?它怎么能从咱们写的源代码中读懂代码的语义呢?

咱们经过decorator(好比@Component(), @Input())来将metadata附加到JavaScript类上。metadata告诉Angular compiler如何处理这个Component/NgModule。在构造函数的声明中也包含了 隐式的metadata。
好比 constructor(private heroService: HeroService){}告诉编译器:该组件须要注入HeroService这个依赖。

即便Typescript被tsc编译成Javascript,metadata依然保留着。这也是为何JIT与AOT的原理是相同的。

AOT编译(AOT步骤2.1)分为两个阶段

  1. "AOT collector"收集每一个源文件的metadata,并为每一个源文件输出一个*.metadata.json文件,它是metadata的abstract syntax tree (AST)表示,见下面的参考资料2。

    "AOT collector"并不尝试去理解metadata信息,它只是将其中的信息放进AST。
  2. "compiler"解析*.metadata.json中的AST,生成Typescript代码。这里的"compiler"是更狭义的编译器,你能够将它理解为编译器的核心部分。
前面已经说过,生成的Typescript代码会引用咱们写的源文件。为何这是必需要的?由于"compiler"的输入仅仅是 *.metadata.json而已,它并不知道程序员写的业务逻辑(constructor中的代码、clickHandler中的代码、其余自定义函数中的代码),这些业务逻辑代码的执行依然要交给源文件中定义的组件类(好比 AppComponent)。

所以,Angular源代码要想经过编译,要前后知足:

  1. metadata能被"AOT collector"识别并表示成AST。AOT collector只能识别一部分表达式语法,而且它不能识别箭头函数。若是违反了这两点,AOT collector将在AST的对应位置记录一个“错误节点”。若是稍后compiler要用到这个位置的节点,compiler会报错。
  2. AST节点能被compiler解析。compiler只能访问那些被export的symbol,所以未export的symbol不能做为AST的节点。此外,compiler只容许在metadata中建立某些类的实例只支持某些decorators只能在metadata中调用一小部分的函数,详见官方文档
官方文档说:"Decorated component class members must be public. You cannot make an @Input() property private or internal."可是通过实验, @Input() private ipt: any;这样的代码不会出问题(只要不将私有的 ipt变量绑定在模板上)。

官方文档还说:"Data bound properties must also be public"。这句话虽然是对的,可是它被放在了Phase 2: code generation这一节,这是有问题的。由于“在模板中绑定私有变量”的出错时间不是在AOT步骤2.1,而是步骤2.2。见下图:此时app.component.ngfactory.ts已经生成了,说明compiler已经解析AST完毕,只不过产生的代码违反了Typescript的私有成员访问限制,这才形成步骤2.2的错误。

 

 

读后笔记: ngc包含两个动做,一、把组件、指令、管道和HTML模板被Angular编译器编译成NgFactories的组件工厂(包含建立、渲染、变化检测等逻辑),NgFactories是实例化组件的根本。 二、将第一步的工厂函数(TS)经过tsc转换成浏览器可识别并执行的的JS代码

相关文章
相关标签/搜索