angular編譯機制
轉載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應用由許多組件、指令、管道等組成,並且每個組件有自己的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
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(