1. 程式人生 > >angular編譯機制

angular編譯機制

java ear trap desktop 核心 cti member property emit

轉載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代碼?”

  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種文件)。

根據最新的講座,在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一般經歷的步驟:

  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步驟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可以在運行時創建組件的實例,通過組合組件類(比如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.ts或identifiers.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的錯誤。

angular編譯機制