Angular 升級(jí)說明

2022-07-22 14:44 更新

從 AngularJS 升級(jí)到 Angular

Angular是現(xiàn)在和未來的 Angular 名稱。

AngularJS是所有 1.x 版本的 Angular 的名稱。

有很多大型 AngularJS 應(yīng)用。在遷移到 Angular 之前,請(qǐng)始終考慮其業(yè)務(wù)案例。該案例的一個(gè)重要部分是遷移的時(shí)間和精力。本指南描述了用于將 AngularJS 項(xiàng)目高效遷移到 Angular 平臺(tái)的內(nèi)置工具,一次一個(gè)。

有些應(yīng)用可能比其它的升級(jí)起來簡單,還有一些方法能讓把這項(xiàng)工作變得更簡單。 即使在正式開始升級(jí)過程之前,可以提前準(zhǔn)備 AngularJS 的程序,讓它向 Angular 看齊。 這些準(zhǔn)備步驟幾乎都是關(guān)于如何讓代碼更加松耦合、更有可維護(hù)性,以及用現(xiàn)代開發(fā)工具提高速度的。 這意味著,這種準(zhǔn)備工作不僅能讓最終的升級(jí)變得更簡單,而且還能提升 AngularJS 程序的質(zhì)量。

成功升級(jí)的關(guān)鍵之一是增量式的實(shí)現(xiàn)它,通過在同一個(gè)應(yīng)用中一起運(yùn)行這兩個(gè)框架,并且逐個(gè)把 AngularJS 的組件遷移到 Angular 中。 這意味著可以在不必打斷其它業(yè)務(wù)的前提下,升級(jí)更大、更復(fù)雜的應(yīng)用程序,因?yàn)檫@項(xiàng)工作可以多人協(xié)作完成,在一段時(shí)間內(nèi)逐漸鋪開。 Angular upgrade 模塊的設(shè)計(jì)目標(biāo)就是讓你漸進(jìn)、無縫的完成升級(jí)。

準(zhǔn)備

AngularJS 應(yīng)用程序的組織方式有很多種。當(dāng)你想把它們升級(jí)到 Angular 的時(shí)候, 有些做起來會(huì)比其它的更容易些。即使在開始升級(jí)之前,也有一些關(guān)鍵的技術(shù)和模式可以讓你將來升級(jí)時(shí)更輕松。

遵循 AngularJS 風(fēng)格指南

AngularJS 風(fēng)格指南收集了一些已證明能寫出干凈且可維護(hù)的 AngularJS 程序的模式與實(shí)踐。 它包含了很多關(guān)于如何書寫和組織 AngularJS 代碼的有價(jià)值信息,同樣重要的是,不應(yīng)該采用的書寫和組織 AngularJS 代碼的方式。

Angular 是一個(gè)基于 AngularJS 中最好的部分構(gòu)思出來的版本。在這種意義上,它的目標(biāo)和 AngularJS 風(fēng)格指南是一樣的: 保留 AngularJS 中好的部分,去掉壞的部分。當(dāng)然,Angular 還做了更多。 說這些的意思是:遵循這個(gè)風(fēng)格指南可以讓你寫出更接近 Angular 程序的 AngularJS 程序。

有一些特別的規(guī)則可以讓使用 Angular 的 ?upgrade/static? 模塊進(jìn)行增量升級(jí)變得更簡單:

  • 單一規(guī)則 規(guī)定每個(gè)文件應(yīng)該只放一個(gè)組件。這不僅讓組件更容易瀏覽和查找,而且還讓你能逐個(gè)遷移它們的語言和框架。 在這個(gè)范例程序中,每個(gè)控制器、工廠和過濾器都位于各自的源文件中。
  • 按特性分目錄的結(jié)構(gòu)模塊化規(guī)則在較高的抽象層定義了一些相似的原則:應(yīng)用程序中的不同部分應(yīng)該被分到不同的目錄和 NgModule 中。

如果應(yīng)用程序能用這種方式把每個(gè)特性分到一個(gè)獨(dú)立目錄中,它也就能每次遷移一個(gè)特性。 對(duì)于那些還沒有這么做的程序,強(qiáng)烈建議把應(yīng)用這條規(guī)則作為準(zhǔn)備步驟。而且這也不僅僅對(duì)升級(jí)有價(jià)值, 它還是一個(gè)通用的規(guī)則,可以讓你的程序更“堅(jiān)實(shí)”。

使用模塊加載器

當(dāng)你把應(yīng)用代碼分解到每個(gè)文件中只放一個(gè)組件的粒度后,通常會(huì)得到一個(gè)由大量相對(duì)較小的文件組成的項(xiàng)目結(jié)構(gòu)。 這比組織成少量大文件要整潔得多,但如果你不得不通過 ?<script>? 標(biāo)簽在 HTML 頁面中加載所有這些文件,那就不好玩了。 尤其是當(dāng)你不得不自己按正確的順序維護(hù)這些標(biāo)簽時(shí)更是如此,就要開始使用模塊加載器了。

使用模塊加載器,比如SystemJS、 WebpackBrowserify, 可以讓你在程序中使用 TypeScript 或 ES2015 語言內(nèi)置的模塊系統(tǒng)。 你可以使用 ?import ?和 ?export ?特性來明確指定哪些代碼應(yīng)該以及將會(huì)被在程序的不同部分之間共享。 對(duì)于 ES5 程序來說,可以改用 CommonJS 風(fēng)格的 ?require ?和 ?module.exports? 特性代替。 無是論哪種情況,模塊加載器都會(huì)按正確的順序加載程序中用到的所有代碼。

當(dāng)要把應(yīng)用程序投入生產(chǎn)環(huán)境時(shí),模塊加載器也會(huì)讓你把所有這些文件打成完整的產(chǎn)品包變得容易一些。

遷移到 TypeScript

Angular 升級(jí)計(jì)劃的一部分是引入 TypeScript,即使在開始升級(jí)之前,引入 TypeScript 編譯器也是有意義的。 這意味著等真正升級(jí)的時(shí)候需要學(xué)習(xí)和思考的東西會(huì)更少,并且你可以在 AngularJS 代碼中開始使用 TypeScript 的特性。

TypeScript 是 ECMAScript 2015 的超集,而 ES2015 又是 ECMAScript 5 的超集。 這意味著除了安裝一個(gè) TypeScript 編譯器,并把文件名都從 ?*.js? 改成 ?*.ts? 之外,其實(shí)什么都不用做。 當(dāng)然,如果僅僅這樣做也沒什么大用,也沒什么有意思的地方。 下面這些額外的步驟可以讓你打起精神:

  • 對(duì)那些使用了模塊加載器的程序,TypeScript 的導(dǎo)入和導(dǎo)出語法(實(shí)際上是 ECMAScript 2015 的導(dǎo)入和導(dǎo)出)可以把代碼組織成模塊。
  • 可以逐步把類型注解添加到現(xiàn)有函數(shù)和變量上,以固定它們的類型,并獲得其優(yōu)點(diǎn):比如編譯期錯(cuò)誤檢查、更好的支持自動(dòng)完成,以及內(nèi)聯(lián)式文檔等。
  • 那些 ES2015 中新增的特性,比如箭頭函數(shù)、?let?、?const?、默認(rèn)函數(shù)參數(shù)、解構(gòu)賦值等也可以逐漸添加進(jìn)來,讓代碼更有表現(xiàn)力。
  • 服務(wù)和控制器可以轉(zhuǎn)成。這樣它們就能一步步接近 Angular 的服務(wù)和組件類了,也會(huì)讓升級(jí)變得簡單一點(diǎn)。

使用組件型指令

在 Angular 中,組件是用來構(gòu)建用戶界面的主要元素。你把 UI 中的不同部分定義成組件,然后在模板中使用這些組件合成出最終的 UI。

你在 AngularJS 中也能這么做。那就是一種定義了自己的模板、控制器和輸入/輸出綁定的指令 —— 跟 Angular 中對(duì)組件的定義是一樣的。 要遷移到 Angular,通過組件型指令構(gòu)建的應(yīng)用程序會(huì)比直接用 ?ng-controller?、?ng-include? 和作用域繼承等底層特性構(gòu)建的要容易得多。

要與 Angular 兼容,AngularJS 的組件型指令應(yīng)該配置下列屬性:

  • ?restrict: 'E'?。組件通常會(huì)以元素的方式使用。
  • ?scope: {}? - 一個(gè)獨(dú)立作用域。在 Angular 中,組件永遠(yuǎn)是從它們的環(huán)境中被隔離出來的,在 AngularJS 中也同樣如此。
  • ?bindToController: {}?。組件的輸入和輸出應(yīng)該綁定到控制器,而不是 ?$scope?。
  • ?controller ?和 ?controllerAs?。組件要有自己的控制器。
  • ?template ?或 ?templateUrl?。組件要有自己的模板。

組件型指令還可能使用下列屬性:

  • ?transclude: true?:如果組件需要從其它地方透傳內(nèi)容,就設(shè)置它。
  • ?require?:如果組件需要和父組件的控制器通訊,就設(shè)置它。

組件型指令不能使用下列屬性:

  • ?compile?。Angular 不再支持它。
  • ?replace: true?。Angular 永遠(yuǎn)不會(huì)用組件模板替換一個(gè)組件元素。這個(gè)特性在 AngularJS 中也同樣不建議使用了。
  • ?priority ?和 ?terminal?。雖然 AngularJS 的組件可能使用這些,但它們在 Angular 中已經(jīng)沒用了,并且最好不要再寫依賴它們的代碼。

AngularJS 中一個(gè)完全向 Angular 架構(gòu)對(duì)齊過的組件型指令是這樣的:

export function heroDetailDirective() {
  return {
    restrict: 'E',
    scope: {},
    bindToController: {
      hero: '=',
      deleted: '&'
    },
    template: `
      <h2>{{$ctrl.hero.name}} details!</h2>
      <div><label>id: </label>{{$ctrl.hero.id}}</div>
      <button ng-click="$ctrl.onDelete()">Delete</button>
    `,
    controller: function HeroDetailController() {
      this.onDelete = () => {
        this.deleted({hero: this.hero});
      };
    },
    controllerAs: '$ctrl'
  };
}

AngularJS 1.5 引入了組件 API,它讓定義指令變得更簡單了。 為組件型指令使用這個(gè) API 是一個(gè)好主意,因?yàn)椋?/p>

  • 它需要更少的樣板代碼。
  • 它強(qiáng)制你遵循組件的最佳實(shí)踐,比如 ?controllerAs?。
  • 指令中像 ?scope ?和 ?restrict ?這樣的屬性應(yīng)該有良好的默認(rèn)值。

如果使用這個(gè)組件 API 進(jìn)行表示,那么上面看到的組件型指令就變成了這樣:

export const heroDetail = {
  bindings: {
    hero: '<',
    deleted: '&'
  },
  template: `
    <h2>{{$ctrl.hero.name}} details!</h2>
    <div><label>id: </label>{{$ctrl.hero.id}}</div>
    <button ng-click="$ctrl.onDelete()">Delete</button>
  `,
  controller: function HeroDetailController() {
    this.onDelete = () => {
      this.deleted(this.hero);
    };
  }
};

控制器的生命周期鉤子 ?$onInit()?、?$onDestroy()? 和 ?$onChanges()? 是 AngularJS 1.5 引入的另一些便利特性。 它們都很像Angular 中的等價(jià)物,所以,圍繞它們組織組件生命周期的邏輯在升級(jí)到 Angular 時(shí)會(huì)更容易。

使用 ngUpgrade 升級(jí)

不管要升級(jí)什么,Angular 中的 ?ngUpgrade ?庫都會(huì)是一個(gè)非常有用的工具 —— 除非是小到?jīng)]功能的應(yīng)用。 借助它,你可以在同一個(gè)應(yīng)用程序中混用并匹配 AngularJS 和 Angular 的組件,并讓它們實(shí)現(xiàn)無縫的互操作。 這意味著你不用被迫一次性做完所有的升級(jí)工作,因?yàn)樵谡麄€(gè)演進(jìn)過程中,這兩個(gè)框架可以很自然的和睦相處。

由于 AngularJS 即將停止維護(hù) ,ngUpgrade 現(xiàn)在處于特性開發(fā)完畢的狀態(tài)。我們將會(huì)繼續(xù)發(fā)布安全補(bǔ)丁和 BUG 修復(fù),直到 2022-12-31。

ngUpgrade 的工作原理

?ngUpgrade ?提供的主要工具之一被稱為 ?UpgradeModule?。這是一個(gè)服務(wù),它可以啟動(dòng)并管理一個(gè)能同時(shí)支持 Angular 和 AngularJS 的混合式應(yīng)用。

當(dāng)使用 ngUpgrade 時(shí),你實(shí)際上在同時(shí)運(yùn)行 AngularJS 和 Angular。所有 Angular 的代碼運(yùn)行在 Angular 框架中,而 AngularJS 的代碼運(yùn)行在 AngularJS 框架中。所有這些都是真實(shí)的、全功能的框架版本。 沒有進(jìn)行任何仿真,所以你可以認(rèn)為同時(shí)存在著這兩個(gè)框架的所有特性和自然行為。

所有這些事情的背后,本質(zhì)上是一個(gè)框架中管理的組件和服務(wù)能和來自另一個(gè)框架的進(jìn)行互操作。 這些主要體現(xiàn)在三個(gè)方面:依賴注入、DOM 和變更檢測。

依賴注入

無論是在 AngularJS 中還是在 Angular 中,依賴注入都位于前沿和中心的位置,但在兩個(gè)框架的工作原理上,卻存在著一些關(guān)鍵的不同之處。

ANGULARJS ANGULAR

依賴注入的令牌(Token)永遠(yuǎn)是字符串(譯注:指服務(wù)名稱)。

令牌可以有不同的類型

只有一個(gè)注入器。
即使在多模塊的應(yīng)用程序中,每樣?xùn)|西也都會(huì)被裝入一個(gè)巨大的命名空間中。

這是一個(gè)樹狀分層注入器:有一個(gè)根注入器,而且每個(gè)組件也有一個(gè)自己的注入器。

就算有這么多不同點(diǎn),也并不妨礙你在依賴注入時(shí)進(jìn)行互操作。?UpgradeModule ?解決了這些差異,并讓它們無縫的對(duì)接:

  • 通過升級(jí)它們,你就能讓那些在 AngularJS 中能被注入的服務(wù)也可用于 Angular 的代碼中。 在框架之間共享的是服務(wù)的同一個(gè)單例對(duì)象。在 Angular 中,這些外來服務(wù)總是被放在根注入器中,并可用于所有組件。 它們總是具有字符串令牌 —— 跟它們在 AngularJS 中的令牌相同。
  • 通過降級(jí)它們,你也能讓那些在 Angular 中能被注入的服務(wù)在 AngularJS 的代碼中可用。 只有那些來自 Angular 根注入器的服務(wù)才能被降級(jí)。同樣的,在框架之間共享的是同一個(gè)單例對(duì)象。 當(dāng)你注冊一個(gè)要降級(jí)的服務(wù)時(shí),要明確指定一個(gè)打算在 AngularJS 中使用的字符串令牌。


組件與 DOM

在混合式應(yīng)用中,同時(shí)存在來自 AngularJS 和 Angular 中組件和指令的 DOM。 這些組件通過它們各自框架中的輸入和輸出綁定來互相通訊,它們由 ?UpgradeModule ?橋接在一起。 它們也能通過共享被注入的依賴彼此通訊,就像前面所說的那樣。

理解混合式應(yīng)用的關(guān)鍵在于,DOM 中的每一個(gè)元素都只能屬于這兩個(gè)框架之一,而另一個(gè)框架則會(huì)忽略它。如果一個(gè)元素屬于 AngularJS,那么 Angular 就會(huì)當(dāng)它不存在,反之亦然。

所以,混合式應(yīng)用總是像 AngularJS 程序那樣啟動(dòng),處理根模板的也是 AngularJS. 然后,當(dāng)這個(gè)應(yīng)用的模板中使用到了 Angular 的組件時(shí),Angular 才開始參與。 這個(gè)組件的視圖由 Angular 進(jìn)行管理,而且它還可以使用一系列的 Angular 組件和指令。

更進(jìn)一步說,你可以按照需要,任意穿插使用這兩個(gè)框架。 使用下面的兩種方式之一,你可以在這兩個(gè)框架之間自由穿梭:

  1. 通過使用來自另一個(gè)框架的組件:AngularJS 的模板中用到了 Angular 的組件,或者 Angular 的模板中使用了 AngularJS 的組件。
  2. 通過透傳(transclude)或投影(project)來自另一個(gè)框架的內(nèi)容。?UpgradeModule ?牽線搭橋,把 AngularJS 的透傳概念和 Angular 的內(nèi)容投影概念關(guān)聯(lián)起來。


當(dāng)你使用一個(gè)屬于另一個(gè)框架的組件時(shí),就會(huì)發(fā)生一次跨框架邊界的切換。不過,這種切換只發(fā)生在該組件元素的子節(jié)點(diǎn)上。 考慮一個(gè)場景,你從 AngularJS 中使用一個(gè) Angular 組件,就像這樣:

<a-component></a-component>

此時(shí),?<a-component>? 這個(gè) DOM 元素仍然由 AngularJS 管理,因?yàn)樗窃?nbsp;AngularJS 的模板中定義的。 這也意味著你可以往它上面添加別的 AngularJS 指令,卻不能添加 Angular 的指令。 只有在 ?<a-component>? 組件的模板中才是 Angular 的天下。同樣的規(guī)則也適用于在 Angular 中使用 AngularJS 組件型指令的情況。

變更檢測

AngularJS 中的變更檢測全是關(guān)于 ?scope.$apply()? 的。在每個(gè)事件發(fā)生之后,?scope.$apply()? 就會(huì)被調(diào)用。 這或者由框架自動(dòng)調(diào)用,或者在某些情況下由你自己的代碼手動(dòng)調(diào)用。

在 Angular 中,事情有點(diǎn)不一樣。雖然變更檢測仍然會(huì)在每一個(gè)事件之后發(fā)生,卻不再需要每次調(diào)用 ?scope.$apply()? 了。 這是因?yàn)樗?nbsp;Angular 代碼都運(yùn)行在一個(gè)叫做 ?Angular zone? 的地方。 Angular 總是知道什么時(shí)候代碼執(zhí)行完了,也就知道了它什么時(shí)候應(yīng)該觸發(fā)變更檢測。代碼本身并不需要調(diào)用 ?scope.$apply()? 或其它類似的東西。

在這種混合式應(yīng)用的案例中,?UpgradeModule ?在 AngularJS 的方法和 Angular 的方法之間建立了橋梁。發(fā)生了什么呢?

  • 應(yīng)用中發(fā)生的每件事都運(yùn)行在 Angular 的 zone 里。 無論事件發(fā)生在 AngularJS 還是 Angular 的代碼中,都是如此。 這個(gè) zone 會(huì)在每個(gè)事件之后觸發(fā) Angular 的變更檢測。
  • ?UpgradeModule ?將在每一次離開 Angular zone 時(shí)調(diào)用 AngularJS 的 ?$rootScope.$apply()?。這樣也就同樣會(huì)在每個(gè)事件之后觸發(fā) AngularJS 的變更檢測。


在實(shí)踐中,你不用在自己的代碼中調(diào)用 ?$apply()?,而不用管這段代碼是在 AngularJS 還是 Angular 中。 ?UpgradeModule ?都替你做了。你仍然可以調(diào)用 ?$apply()?,也就是說你不必從現(xiàn)有代碼中移除此調(diào)用。 在混合式應(yīng)用中,這些調(diào)用只會(huì)觸發(fā)一次額外的 AngularJS 變更檢測。

當(dāng)你降級(jí)一個(gè) Angular 組件,然后把它用于 AngularJS 中時(shí),組件的輸入屬性就會(huì)被 AngularJS 的變更檢測體系監(jiān)視起來。 當(dāng)那些輸入屬性發(fā)生變化時(shí),組件中相應(yīng)的屬性就會(huì)被設(shè)置。你也能通過實(shí)現(xiàn)?OnChanges ?接口來掛鉤到這些更改,就像它未被降級(jí)時(shí)一樣。

相應(yīng)的,當(dāng)你把 AngularJS 的組件升級(jí)給 Angular 使用時(shí),在這個(gè)組件型指令的 ?scope?(或 ?bindToController?)中定義的所有綁定, 都將被掛鉤到 Angular 的變更檢測體系中。它們將和標(biāo)準(zhǔn)的 Angular 輸入屬性被同等對(duì)待,并當(dāng)它們發(fā)生變化時(shí)設(shè)置回 scope(或控制器)上。

通過 Angular 的 NgModule 來使用 UpgradeModule

AngularJS 還是 Angular 都有自己的模塊概念,來幫你把應(yīng)用組織成一些內(nèi)聚的功能塊。

它們在架構(gòu)和實(shí)現(xiàn)的細(xì)節(jié)上有著顯著的不同。 在 AngularJS 中,你要把 AngularJS 的資源添加到 ?angular.module? 屬性上。 在 Angular 中,你要?jiǎng)?chuàng)建一個(gè)或多個(gè)帶有 ?NgModule ?裝飾器的類,這些裝飾器用來在元數(shù)據(jù)中描述 Angular 資源。差異主要來自這里。

在混合式應(yīng)用中,你同時(shí)運(yùn)行了兩個(gè)版本的 Angular。 這意味著你至少需要 AngularJS 和 Angular 各提供一個(gè)模塊。 當(dāng)你使用 AngularJS 的模塊進(jìn)行引導(dǎo)時(shí),就得把 Angular 的模塊傳給 ?UpgradeModule?。

引導(dǎo)混合應(yīng)用程序

要想引導(dǎo)混合式應(yīng)用,就必須在應(yīng)用中分別引導(dǎo) Angular 和 AngularJS 應(yīng)用的一部分。你必須先引導(dǎo) Angular,然后再調(diào)用 ?UpgradeModule ?來引導(dǎo) AngularJS。

在 AngularJS 應(yīng)用中有一個(gè) AngularJS 的根模塊,它用于引導(dǎo) AngularJS 應(yīng)用。

angular.module('heroApp', [])
  .controller('MainCtrl', function() {
    this.message = 'Hello world';
  });

單純的 AngularJS 應(yīng)用可以在 HTML 頁面中使用 ?ng-app? 指令進(jìn)行引導(dǎo),但對(duì)于混合式應(yīng)用你要通過 ?UpgradeModule ?模塊進(jìn)行手動(dòng)引導(dǎo)。因此,在切換成混合式應(yīng)用之前,最好先把 AngularJS 改寫成使用 angular.bootstrap 進(jìn)行手動(dòng)引導(dǎo)的方式。

比如你現(xiàn)在有這樣一個(gè)通過 ?ng-app? 進(jìn)行引導(dǎo)的應(yīng)用:

<!DOCTYPE HTML>
<html lang="en">
  <head>
    <base href="/">
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.3/angular.js" rel="external nofollow" ></script>
    <script src="app/ajs-ng-app/app.module.js"></script>
  </head>

  <body ng-app="heroApp" ng-strict-di>
    <div id="message" ng-controller="MainCtrl as mainCtrl">
      {{ mainCtrl.message }}
    </div>
  </body>
</html>

你可以從 HTML 中移除 ?ng-app? 和 ?ng-strict-di? 指令,改為從 JavaScript 中調(diào)用 ?angular.bootstrap?,它能達(dá)到同樣效果:

angular.bootstrap(document.body, ['heroApp'], { strictDi: true });

要想把 AngularJS 應(yīng)用變成 Hybrid 應(yīng)用,就要先加載 Angular 框架。 根據(jù)準(zhǔn)備升級(jí)到 AngularJS 中給出的步驟,選擇性的把快速入門 github 代碼倉中的代碼復(fù)制過來。

也可以通過 ?npm install @angular/upgrade --save? 命令來安裝 ?@angular/upgrade? 包,并給它添加一個(gè)到 ?@angular/upgrade/static? 包的映射。

'@angular/upgrade/static': 'npm:@angular/upgrade/fesm2015/static.mjs',

接下來,創(chuàng)建一個(gè) ?app.module.ts? 文件,并添加下列 ?NgModule ?類:

import { DoBootstrap, NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { UpgradeModule } from '@angular/upgrade/static';

@NgModule({
  imports: [
    BrowserModule,
    UpgradeModule
  ]
})
export class AppModule implements DoBootstrap {
  constructor(private upgrade: UpgradeModule) { }
  ngDoBootstrap() {
    this.upgrade.bootstrap(document.body, ['heroApp'], { strictDi: true });
  }
}

最小化的 ?NgModule ?導(dǎo)入了 ?BrowserModule?,它是每個(gè)基于瀏覽器的 Angular 應(yīng)用必備的。 它還從 ?@angular/upgrade/static? 中導(dǎo)入了 ?UpgradeModule?,它導(dǎo)出了一些服務(wù)提供者,這些提供者會(huì)用于升級(jí)、降級(jí)服務(wù)和組件。

在 ?AppModule ?的構(gòu)造函數(shù)中,使用依賴注入技術(shù)獲取了一個(gè) ?UpgradeModule ?實(shí)例,并用它在 ?AppModule.ngDoBootstrap? 方法中啟動(dòng) AngularJS 應(yīng)用。 ?upgrade.bootstrap? 方法接受和 angular.bootstrap 完全相同的參數(shù)。

注意,你不需要在 ?@NgModule? 中加入 ?bootstrap ?聲明,因?yàn)?nbsp;AngularJS 控制著該應(yīng)用的根模板。

現(xiàn)在,你就可以使用 ?platformBrowserDynamic.bootstrapModule? 方法來啟動(dòng) ?AppModule ?了。

import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

platformBrowserDynamic().bootstrapModule(AppModule);

恭喜!你就要開始運(yùn)行這個(gè)混合式應(yīng)用了!所有現(xiàn)存的 AngularJS 代碼會(huì)像以前一樣正常工作,但是你現(xiàn)在也同樣可以運(yùn)行 Angular 代碼了。

從 AngularJS 代碼中使用 Angular 組件


一旦你開始運(yùn)行混合式應(yīng)用,你就可以開始逐漸升級(jí)代碼了。一種更常見的工作模式就是在 AngularJS 的上下文中使用 Angular 的組件。 該組件可能是全新的,也可能是把原本 AngularJS 的組件用 Angular 重寫而成的。

假設(shè)你有一個(gè)用來顯示英雄信息的 Angular 組件:

import { Component } from '@angular/core';

@Component({
  selector: 'hero-detail',
  template: `
    <h2>Windstorm details!</h2>
    <div><label>id: </label>1</div>
  `
})
export class HeroDetailComponent { }

如果你想在 AngularJS 中使用這個(gè)組件,就得用 ?downgradeComponent()? 方法把它降級(jí)。 其結(jié)果是一個(gè) AngularJS 的指令,你可以把它注冊到 AngularJS 的模塊中:

import { HeroDetailComponent } from './hero-detail.component';

/* . . . */

import { downgradeComponent } from '@angular/upgrade/static';

angular.module('heroApp', [])
  .directive(
    'heroDetail',
    downgradeComponent({ component: HeroDetailComponent }) as angular.IDirectiveFactory
  );

默認(rèn)情況下,Angular 變更檢測也會(huì)在 AngularJS 的每個(gè) ?$digest? 周期中運(yùn)行。如果你希望只在輸入屬性發(fā)生變化時(shí)才運(yùn)行變更檢測,可以在調(diào)用 ?downgradeComponent()? 時(shí)把 ?propagateDigest ?設(shè)置為 ?false?。

由于 ?HeroDetailComponent ?是一個(gè) Angular 組件,所以你必須同時(shí)把它加入 ?AppModule ?的 ?declarations ?字段中。

import { HeroDetailComponent } from './hero-detail.component';

@NgModule({
  imports: [
    BrowserModule,
    UpgradeModule
  ],
  declarations: [
    HeroDetailComponent
  ]
})
export class AppModule implements DoBootstrap {
  constructor(private upgrade: UpgradeModule) { }
  ngDoBootstrap() {
    this.upgrade.bootstrap(document.body, ['heroApp'], { strictDi: true });
  }
}

所有 Angular 組件、指令和管道都必須聲明在 NgModule 中。

最終的結(jié)果是一個(gè)叫做 ?heroDetail ?的 AngularJS 指令,你可以像用其它指令一樣把它用在 AngularJS 模板中。

<hero-detail></hero-detail>

注意,它在 AngularJS 中是一個(gè)名叫 ?heroDetail ?的元素型指令(?restrict: 'E'?)。 AngularJS 的元素型指令是基于它的名字匹配的。 Angular 組件中的 ?selector ?元數(shù)據(jù),在降級(jí)后的版本中會(huì)被忽略。

當(dāng)然,大多數(shù)組件都不像這個(gè)這么簡單。它們中很多都有輸入屬性和輸出屬性,來把它們連接到外部世界。 Angular 的英雄詳情組件帶有像這樣的輸入屬性與輸出屬性:

import { Component, EventEmitter, Input, Output } from '@angular/core';
import { Hero } from '../hero';

@Component({
  selector: 'hero-detail',
  template: `
    <h2>{{hero.name}} details!</h2>
    <div><label>id: </label>{{hero.id}}</div>
    <button (click)="onDelete()">Delete</button>
  `
})
export class HeroDetailComponent {
  @Input() hero!: Hero;
  @Output() deleted = new EventEmitter<Hero>();
  onDelete() {
    this.deleted.emit(this.hero);
  }
}

這些輸入屬性和輸出屬性的值來自于 AngularJS 的模板,而 ?downgradeComponent()? 方法負(fù)責(zé)橋接它們:

<div ng-controller="MainController as mainCtrl">
  <hero-detail [hero]="mainCtrl.hero"
               (deleted)="mainCtrl.onDelete($event)">
  </hero-detail>
</div>

注意,雖然你正在 AngularJS 的模板中,但卻在使用 Angular 的屬性(Attribute)語法來綁定到輸入屬性與輸出屬性。 這是降級(jí)的組件本身要求的。而表達(dá)式本身仍然是標(biāo)準(zhǔn)的 AngularJS 表達(dá)式。

在降級(jí)過的組件屬性中使用中線命名法
為降級(jí)過的組件使用 Angular 的屬性(Attribute)語法規(guī)則時(shí)有一個(gè)值得注意的例外。 它適用于由多個(gè)單詞組成的輸入或輸出屬性。在 Angular 中,你要使用小駝峰命名法綁定這些屬性:
[myHero]="hero"
(heroDeleted)="handleHeroDeleted($event)"

但是從 AngularJS 的模板中使用它們時(shí),你得使用中線命名法:

[my-hero]="hero"
(hero-deleted)="handleHeroDeleted($event)"

?$event? 變量能被用在輸出屬性里,以訪問這個(gè)事件所發(fā)出的對(duì)象。這個(gè)案例中它是 ?Hero ?對(duì)象,因?yàn)?nbsp;?this.deleted.emit()? 函數(shù)曾把它傳了出來。

由于這是一個(gè) AngularJS 模板,雖然它已經(jīng)有了 Angular 中綁定的屬性(Attribute),你仍可以在這個(gè)元素上使用其它 AngularJS 指令。 例如,你可以用 ?ng-repeat? 簡單的制作該組件的多份拷貝:

<div ng-controller="MainController as mainCtrl">
  <hero-detail [hero]="hero"
               (deleted)="mainCtrl.onDelete($event)"
               ng-repeat="hero in mainCtrl.heroes">
  </hero-detail>
</div>

從 Angular 代碼使用 AngularJS 組件指令


現(xiàn)在,你已經(jīng)能在 Angular 中寫一個(gè)組件,并把它用于 AngularJS 代碼中了。 當(dāng)你從低級(jí)組件開始移植,并往上走時(shí),這非常有用。但在另外一些情況下,從相反的方向進(jìn)行移植會(huì)更加方便: 從高級(jí)組件開始,然后往下走。這也同樣能用 ?UpgradeModule ?完成。 你可以升級(jí)AngularJS 組件型指令,然后從 Angular 中用它們。

不是所有種類的 AngularJS 指令都能升級(jí)。該指令必須是一個(gè)嚴(yán)格的組件型指令,具有上面的準(zhǔn)備指南中描述的那些特征。 確保兼容性的最安全的方式是 AngularJS 1.5 中引入的組件 API

可升級(jí)組件的簡單例子是只有一個(gè)模板和一個(gè)控制器的指令:

export const heroDetail = {
  template: `
    <h2>Windstorm details!</h2>
    <div><label>id: </label>1</div>
  `,
  controller: function HeroDetailController() {
  }
};

你可以使用 ?UpgradeComponent ?方法來把這個(gè)組件升級(jí)到 Angular。 具體方法是創(chuàng)建一個(gè) Angular指令,繼承 ?UpgradeComponent?,在其構(gòu)造函數(shù)中進(jìn)行 ?super ?調(diào)用, 這樣你就得到一個(gè)完全升級(jí)的 AngularJS 組件,并且可以 Angular 中使用。 剩下是工作就是把它加入到 ?AppModule ?的 ?declarations ?數(shù)組。

import { Directive, ElementRef, Injector, SimpleChanges } from '@angular/core';
import { UpgradeComponent } from '@angular/upgrade/static';

@Directive({
  selector: 'hero-detail'
})
export class HeroDetailDirective extends UpgradeComponent {
  constructor(elementRef: ElementRef, injector: Injector) {
    super('heroDetail', elementRef, injector);
  }
}
@NgModule({
  imports: [
    BrowserModule,
    UpgradeModule
  ],
  declarations: [
    HeroDetailDirective,
  /* . . . */
  ]
})
export class AppModule implements DoBootstrap {
  constructor(private upgrade: UpgradeModule) { }
  ngDoBootstrap() {
    this.upgrade.bootstrap(document.body, ['heroApp'], { strictDi: true });
  }
}

升級(jí)后的組件是 Angular 的指令,而不是組件,因?yàn)?nbsp;Angular 不知道 AngularJS 將在它下面創(chuàng)建元素。 Angular 所知道的是升級(jí)后的組件只是一個(gè)指令(一個(gè)標(biāo)簽),Angular 不需要關(guān)心組件本身及其子元素。

升級(jí)后的組件也可能有輸入屬性和輸出屬性,它們是在原 AngularJS 組件型指令的 scope/controller 綁定中定義的。 當(dāng)你從 Angular 模板中使用該組件時(shí),就要使用Angular 模板語法來提供這些輸入屬性和輸出屬性,但要遵循下列規(guī)則:

綁定定義

模板語法

屬性綁定

myAttribute: '@myAttribute' <my-component myAttribute="value">

表達(dá)式綁定

myOutput: '&myOutput' <my-component (myOutput)="action()">

單向綁定

myValue: '<myValue' <my-component [myValue]="anExpression">

雙向綁定

myValue: '=myValue'

用作雙向綁定:<my-component [(myValue)]="anExpression">。
由于大多數(shù) AngularJS 的雙向綁定實(shí)際上只是單向綁定,因此通常寫成 <my-component [myValue]="anExpression"> 也夠用了。

舉個(gè)例子,假設(shè) AngularJS 中有一個(gè)表示“英雄詳情”的組件型指令,它帶有一個(gè)輸入屬性和一個(gè)輸出屬性:

export const heroDetail = {
  bindings: {
    hero: '<',
    deleted: '&'
  },
  template: `
    <h2>{{$ctrl.hero.name}} details!</h2>
    <div><label>id: </label>{{$ctrl.hero.id}}</div>
    <button ng-click="$ctrl.onDelete()">Delete</button>
  `,
  controller: function HeroDetailController() {
    this.onDelete = () => {
      this.deleted(this.hero);
    };
  }
};

你可以把這個(gè)組件升級(jí)到 Angular,然后使用 Angular 的模板語法提供這個(gè)輸入屬性和輸出屬性:

import { Directive, ElementRef, Injector, Input, Output, EventEmitter } from '@angular/core';
import { UpgradeComponent } from '@angular/upgrade/static';
import { Hero } from '../hero';

@Directive({
  selector: 'hero-detail'
})
export class HeroDetailDirective extends UpgradeComponent {
  @Input() hero: Hero;
  @Output() deleted: EventEmitter<Hero>;

  constructor(elementRef: ElementRef, injector: Injector) {
    super('heroDetail', elementRef, injector);
  }
}
import { Component } from '@angular/core';
import { Hero } from '../hero';

@Component({
  selector: 'my-container',
  template: `
    <h1>Tour of Heroes</h1>
    <hero-detail [hero]="hero"
                 (deleted)="heroDeleted($event)">
    </hero-detail>
  `
})
export class ContainerComponent {
  hero = new Hero(1, 'Windstorm');
  heroDeleted(hero: Hero) {
    hero.name = 'Ex-' + hero.name;
  }
}

把 AngularJS 的內(nèi)容投影到 Angular 組件中


如果你在 AngularJS 模板中使用降級(jí)后的 Angular 組件時(shí),可能會(huì)需要把模板中的一些內(nèi)容投影進(jìn)那個(gè)組件。 這也是可能的,雖然在 Angular 中并沒有透傳(transclude)這樣的東西,但它有一個(gè)非常相似的概念,叫做內(nèi)容投影。 ?UpgradeModule ?也能讓這兩個(gè)特性實(shí)現(xiàn)互操作。

Angular 的組件通過使用 ?<ng-content>? 標(biāo)簽來支持內(nèi)容投影。下面是這類組件的一個(gè)例子:

import { Component, Input } from '@angular/core';
import { Hero } from '../hero';

@Component({
  selector: 'hero-detail',
  template: `
    <h2>{{hero.name}}</h2>
    <div>
      <ng-content></ng-content>
    </div>
  `
})
export class HeroDetailComponent {
  @Input() hero!: Hero;
}

當(dāng)從 AngularJS 中使用該組件時(shí),你可以為它提供內(nèi)容。正如它們將在 AngularJS 中被透傳一樣, 它們也在 Angular 中被投影到了 ?<ng-content>? 標(biāo)簽所在的位置:

<div ng-controller="MainController as mainCtrl">
  <hero-detail [hero]="mainCtrl.hero">
    <!-- Everything here will get projected -->
    <p>{{mainCtrl.hero.description}}</p>
  </hero-detail>
</div>

當(dāng) AngularJS 的內(nèi)容被投影到 Angular 組件中時(shí),它仍然留在“AngularJS 王國”中,并被 AngularJS 框架管理著。

把 Angular 的內(nèi)容透傳進(jìn) AngularJS 的組件型指令


就像可以把 AngularJS 的內(nèi)容投影進(jìn) Angular 組件一樣,你也能把 Angular 的內(nèi)容透傳進(jìn) AngularJS 的組件, 但不管怎樣,你都要使用它們升級(jí)過的版本。

如果一個(gè) AngularJS 組件型指令支持透傳,它就會(huì)在自己的模板中使用 ?ng-transclude? 指令標(biāo)記出透傳到的位置:

export const heroDetail = {
  bindings: {
    hero: '='
  },
  template: `
    <h2>{{$ctrl.hero.name}}</h2>
    <div>
      <ng-transclude></ng-transclude>
    </div>
  `,
  transclude: true
};

如果你升級(jí)這個(gè)組件,并把它用在 Angular 中,你就能把準(zhǔn)備透傳的內(nèi)容放進(jìn)這個(gè)組件的標(biāo)簽中。

import { Component } from '@angular/core';
import { Hero } from '../hero';

@Component({
  selector: 'my-container',
  template: `
    <hero-detail [hero]="hero">
      <!-- Everything here will get transcluded -->
      <p>{{hero.description}}</p>
    </hero-detail>
  `
})
export class ContainerComponent {
  hero = new Hero(1, 'Windstorm', 'Specific powers of controlling winds');
}

讓 AngularJS 中的依賴可被注入到 Angular

當(dāng)運(yùn)行一個(gè)混合式應(yīng)用時(shí),可能會(huì)遇到這種情況:你需要把某些 AngularJS 的依賴注入到 Angular 代碼中。 這可能是因?yàn)槟承I(yè)務(wù)邏輯仍然在 AngularJS 服務(wù)中,或者需要某些 AngularJS 的內(nèi)置服務(wù),比如 ?$location? 或 ?$timeout?。

在這些情況下,把一個(gè) AngularJS 提供者升級(jí)到Angular 也是有可能的。這就讓它將來有可能被注入到 Angular 代碼中的某些地方。 比如,你可能在 AngularJS 中有一個(gè)名叫 ?HeroesService ?的服務(wù):

import { Hero } from '../hero';

export class HeroesService {
  get() {
    return [
      new Hero(1, 'Windstorm'),
      new Hero(2, 'Spiderman')
    ];
  }
}

你可以用 Angular 的工廠提供者升級(jí)該服務(wù), 它從 AngularJS 的 ?$injector? 請(qǐng)求服務(wù)。

很多開發(fā)者都喜歡在一個(gè)獨(dú)立的 ?ajs-upgraded-providers.ts? 中聲明這個(gè)工廠提供者,以便把它們都放在一起,這樣便于引用、創(chuàng)建新的以及在升級(jí)完畢時(shí)刪除它們。

同時(shí),建議導(dǎo)出 ?heroesServiceFactory ?函數(shù),以便 AOT 編譯器可以拿到它們。

注意:這個(gè)工廠中的字符串 'heroes' 指向的是 AngularJS 的 ?HeroesService?。 AngularJS 應(yīng)用中通常使用服務(wù)名作為令牌,比如 'heroes',并為其追加 'Service' 后綴來創(chuàng)建其類名。

import { HeroesService } from './heroes.service';

export function heroesServiceFactory(i: any) {
  return i.get('heroes');
}

export const heroesServiceProvider = {
  provide: HeroesService,
  useFactory: heroesServiceFactory,
  deps: ['$injector']
};

然后,你就可以把這個(gè)服務(wù)添加到 ?@NgModule? 中來把它暴露給 Angular:

import { heroesServiceProvider } from './ajs-upgraded-providers';

@NgModule({
  imports: [
    BrowserModule,
    UpgradeModule
  ],
  providers: [
    heroesServiceProvider
  ],
/* . . . */
})
export class AppModule implements DoBootstrap {
  constructor(private upgrade: UpgradeModule) { }
  ngDoBootstrap() {
    this.upgrade.bootstrap(document.body, ['heroApp'], { strictDi: true });
  }
}

然后在組件的構(gòu)造函數(shù)中使用該服務(wù)的類名作為類型注解注入到組件中,從而在組件中使用它:

import { Component } from '@angular/core';
import { HeroesService } from './heroes.service';
import { Hero } from '../hero';

@Component({
  selector: 'hero-detail',
  template: `
    <h2>{{hero.id}}: {{hero.name}}</h2>
  `
})
export class HeroDetailComponent {
  hero: Hero;
  constructor(heroes: HeroesService) {
    this.hero = heroes.get()[0];
  }
}

在這個(gè)例子中,你升級(jí)了服務(wù)類。當(dāng)注入它時(shí),你可以使用 TypeScript 類型注解來獲得這些額外的好處。 它沒有影響該依賴的處理過程,同時(shí)還得到了啟用靜態(tài)類型檢查的好處。 任何 AngularJS 中的服務(wù)、工廠和提供者都能被升級(jí) —— 盡管這不是必須的。

讓 Angular 的依賴能被注入到 AngularJS 中

除了能升級(jí) AngularJS 依賴之外,你還能降級(jí)Angular 的依賴,以便在 AngularJS 中使用它們。 當(dāng)你已經(jīng)開始把服務(wù)移植到 Angular 或在 Angular 中創(chuàng)建新服務(wù),但同時(shí)還有一些用 AngularJS 寫成的組件時(shí),這會(huì)非常有用。

例如,你可能有一個(gè) Angular 的 ?Heroes ?服務(wù):

import { Injectable } from '@angular/core';
import { Hero } from '../hero';

@Injectable()
export class Heroes {
  get() {
    return [
      new Hero(1, 'Windstorm'),
      new Hero(2, 'Spiderman')
    ];
  }
}

仿照 Angular 組件,把該提供者加入 ?NgModule ?的 ?providers ?列表中,以注冊它。

import { Heroes } from './heroes';

@NgModule({
  imports: [
    BrowserModule,
    UpgradeModule
  ],
  providers: [ Heroes ]
})
export class AppModule implements DoBootstrap {
  constructor(private upgrade: UpgradeModule) { }
  ngDoBootstrap() {
    this.upgrade.bootstrap(document.body, ['heroApp'], { strictDi: true });
  }
}

現(xiàn)在,用 ?downgradeInjectable()? 來把 Angular 的 ?Heroes ?包裝成AngularJS 的工廠函數(shù),并把這個(gè)工廠注冊進(jìn) AngularJS 的模塊中。 依賴在 AngularJS 中的名字你可以自己定:

import { Heroes } from './heroes';
/* . . . */
import { downgradeInjectable } from '@angular/upgrade/static';

angular.module('heroApp', [])
  .factory('heroes', downgradeInjectable(Heroes))
  .component('heroDetail', heroDetailComponent);

此后,該服務(wù)就能被注入到 AngularJS 代碼中的任何地方了:

export const heroDetailComponent = {
  template: `
    <h2>{{$ctrl.hero.id}}: {{$ctrl.hero.name}}</h2>
  `,
  controller: ['heroes', function(heroes: Heroes) {
    this.hero = heroes.get()[0];
  }]
};

惰性加載 AngularJS

在構(gòu)建應(yīng)用時(shí),你需要確保只在必要的時(shí)候才加載所需的資源,無論是加載靜態(tài)資產(chǎn)(Asset)還是代碼。要確保任何事都盡量推遲到必要時(shí)才去做,以便讓應(yīng)用更高效的運(yùn)行。當(dāng)要在同一個(gè)應(yīng)用中運(yùn)行不同的框架時(shí),更是如此。

惰性加載是一項(xiàng)技術(shù),它會(huì)推遲到使用時(shí)才加載所需靜態(tài)資產(chǎn)和代碼資源。這可以減少啟動(dòng)時(shí)間、提高效率,特別是要在同一個(gè)應(yīng)用中運(yùn)行不同的框架時(shí)。

當(dāng)你采用混合式應(yīng)用的方式將大型應(yīng)用從 AngularJS 遷移到 Angular 時(shí),你首先要遷移一些最常用的特性,并且只在必要的時(shí)候才使用那些不太常用的特性。這樣做有助于確保應(yīng)用程序在遷移過程中仍然能為用戶提供無縫的體驗(yàn)。

在大多數(shù)需要同時(shí)用 Angular 和 AngularJS 渲染應(yīng)用的環(huán)境中,這兩個(gè)框架都會(huì)包含在發(fā)送給客戶端的初始發(fā)布包中。這會(huì)導(dǎo)致發(fā)布包的體積增大、性能降低。

當(dāng)用戶停留在由 Angular 渲染的頁面上時(shí),應(yīng)用的整體性能也會(huì)受到影響。這是因?yàn)?nbsp;AngularJS 的框架和應(yīng)用仍然被加載并運(yùn)行了 —— 即使它們從未被訪問過。

你可以采取一些措施來緩解這些包的大小和性能問題。通過把 AngularJS 應(yīng)用程序分離到一個(gè)單獨(dú)的發(fā)布包中,你就可以利用惰性加載技術(shù)來只在必要的時(shí)候才加載、引導(dǎo)和渲染這個(gè) AngularJS 應(yīng)用。這種策略減少了你的初始發(fā)布包大小,推遲了同時(shí)加載兩個(gè)框架的潛在影響 —— 直到絕對(duì)必要時(shí)才加載,以便讓你的應(yīng)用盡可能高效地運(yùn)行。

下面的步驟介紹了應(yīng)該如何去做:

  • 為 AngularJS 發(fā)布包設(shè)置一個(gè)回調(diào)函數(shù)。
  • 創(chuàng)建一個(gè)服務(wù),以便惰性加載并引導(dǎo)你的 AngularJS 應(yīng)用。
  • 為 AngularJS 內(nèi)容創(chuàng)建一個(gè)可路由的組件
  • 為 AngularJS 特有的 URL 創(chuàng)建自定義的 ?matcher ?函數(shù),并為 AngularJS 的各個(gè)路由配上帶有自定義匹配器的 Angular 路由器。

為惰性加載 AngularJS 創(chuàng)建一個(gè)服務(wù)

在 Angular 的版本 8 中,惰性加載代碼只需使用動(dòng)態(tài)導(dǎo)入語法 ?import('...')? 即可。在這個(gè)應(yīng)用中,你創(chuàng)建了一個(gè)新服務(wù),它使用動(dòng)態(tài)導(dǎo)入技術(shù)來惰性加載 AngularJS。

import { Injectable } from '@angular/core';
import * as angular from 'angular';

@Injectable({
  providedIn: 'root'
})
export class LazyLoaderService {
  private app: angular.auto.IInjectorService | undefined;

  load(el: HTMLElement): void {
    import('./angularjs-app').then(app => {
      try {
        this.app = app.bootstrap(el);
      } catch (e) {
        console.error(e);
      }
    });
  }

  destroy() {
    if (this.app) {
      this.app.get('$rootScope').$destroy();
    }
  }
}

該服務(wù)使用 ?import()? 方法惰性加載打包好的 AngularJS 應(yīng)用。這會(huì)減少應(yīng)用初始包的大小,因?yàn)槟闵形醇虞d用戶目前不需要的代碼。你還要提供一種方法,在加載完畢后手動(dòng)啟動(dòng)它。AngularJS 提供了一種使用 angular.bootstrap() 方法并傳入一個(gè) HTML 元素來手動(dòng)引導(dǎo)應(yīng)用的方法。你的 AngularJS 應(yīng)用也應(yīng)該公開一個(gè)用來引導(dǎo) AngularJS 應(yīng)用的 ?bootstrap ?方法。

要確保 AngularJS 應(yīng)用中的任何清理工作都觸發(fā)過(比如移除全局監(jiān)聽器),你還可以實(shí)現(xiàn)一個(gè)方法來調(diào)用 ?$rootScope.destroy()? 方法。

import * as angular from 'angular';
import 'angular-route';

const appModule = angular.module('myApp', [
  'ngRoute'
])
.config(['$routeProvider', '$locationProvider',
  function config($routeProvider: angular.route.IRouteProvider,
                  $locationProvider: angular.ILocationProvider) {
    $locationProvider.html5Mode(true);

    $routeProvider.
      when('/users', {
        template: `
          <p>
            Users Page
          </p>
        `
      }).
      otherwise({
        template: ''
      });
  }]
);

export function bootstrap(el: HTMLElement) {
  return angular.bootstrap(el,  [appModule.name]);
}

你的 AngularJS 應(yīng)用只配置了渲染內(nèi)容所需的那部分路由。而 Angular 路由器會(huì)處理應(yīng)用中其余的路由。你的 Angular 應(yīng)用中會(huì)調(diào)用公開的 ?bootstrap ?方法,讓它在加載完發(fā)布包之后引導(dǎo) AngularJS 應(yīng)用。

注意:當(dāng) AngularJS 加載并引導(dǎo)完畢后,監(jiān)聽器(比如路由配置中的那些監(jiān)聽器)會(huì)繼續(xù)監(jiān)聽路由的變化。為了確保當(dāng) AngularJS 尚未顯示時(shí)先關(guān)閉監(jiān)聽器,請(qǐng)?jiān)?nbsp;$routeProvider 中配置一個(gè)渲染空模板 ?otherwise ?選項(xiàng)。這里假設(shè) Angular 將處理所有其它路由。

創(chuàng)建一個(gè)用來渲染 AngularJS 內(nèi)容的組件

在 Angular 應(yīng)用中,你需要一個(gè)組件作為 AngularJS 內(nèi)容的占位符。該組件使用你創(chuàng)建的服務(wù),并在組件初始化完成后加載并引導(dǎo)你的 AngularJS 應(yīng)用。

import { Component, OnInit, OnDestroy, ElementRef } from '@angular/core';
import { LazyLoaderService } from '../lazy-loader.service';

@Component({
  selector: 'app-angular-js',
  template: '<div ng-view></div>'
})
export class AngularJSComponent implements OnInit, OnDestroy {
  constructor(
    private lazyLoader: LazyLoaderService,
    private elRef: ElementRef
  ) {}

  ngOnInit() {
    this.lazyLoader.load(this.elRef.nativeElement);
  }


  ngOnDestroy() {
    this.lazyLoader.destroy();
  }
}

當(dāng) Angular 的路由器匹配到使用 AngularJS 的路由時(shí),會(huì)渲染 AngularJSComponent,并在 AngularJS 的 ng-view 指令中渲染內(nèi)容。當(dāng)用戶導(dǎo)航離開本路由時(shí),$rootScope 會(huì)在 AngularJS 應(yīng)用中被銷毀。

為那些 AngularJS 路由配置自定義路由匹配器

為了配置 Angular 的路由器,你必須為 AngularJS 的 URL 定義路由。要匹配這些 URL,你需要添加一個(gè)使用 ?matcher ?屬性的路由配置。這個(gè) ?matcher ?允許你使用自定義模式來匹配這些 URL 路徑。Angular 的路由器會(huì)首先嘗試匹配更具體的路由,比如靜態(tài)路由和可變路由。當(dāng)它找不到匹配項(xiàng)時(shí),就會(huì)求助于路由配置中的自定義匹配器。如果自定義匹配器與某個(gè)路由不匹配,它就會(huì)轉(zhuǎn)到用于 "捕獲所有"(catch-all)的路由,比如 404 頁面。

下面的例子給 AngularJS 路由定義了一個(gè)自定義匹配器函數(shù)。

export function isAngularJSUrl(url: UrlSegment[]) {
  return url.length > 0 && url[0].path.startsWith('users') ? ({consumed: url}) : null;
}

下列代碼往你的路由配置中添加了一個(gè)路由對(duì)象,其 ?matcher ?屬性是這個(gè)自定義匹配器,而 ?component ?屬性為 ?AngularJSComponent?。

import { NgModule } from '@angular/core';
import { Routes, RouterModule, UrlSegment } from '@angular/router';
import { AngularJSComponent } from './angular-js/angular-js.component';
import { HomeComponent } from './home/home.component';
import { App404Component } from './app404/app404.component';

// Match any URL that starts with `users`
export function isAngularJSUrl(url: UrlSegment[]) {
  return url.length > 0 && url[0].path.startsWith('users') ? ({consumed: url}) : null;
}

export const routes: Routes = [
  // Routes rendered by Angular
  { path: '', component: HomeComponent },

  // AngularJS routes
  { matcher: isAngularJSUrl, component: AngularJSComponent },

  // Catch-all route
  { path: '**', component: App404Component }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

當(dāng)你的應(yīng)用匹配上需要 AngularJS 的路由時(shí),AngularJS 應(yīng)用就會(huì)被加載并引導(dǎo)。AngularJS 路由會(huì)匹配必要的 URL 以渲染它們的內(nèi)容,而接下來你的應(yīng)用就會(huì)同時(shí)運(yùn)行 AngularJS 和 Angular 框架。

使用統(tǒng)一的 Angular 位置服務(wù)(Location)

在 AngularJS 中,$location 服務(wù)會(huì)處理所有路由配置和導(dǎo)航工作,并對(duì)各個(gè) URL 進(jìn)行編碼和解碼、重定向、以及與瀏覽器 API 交互。Angular 在所有這些任務(wù)中都使用了自己的底層服務(wù) ?Location?。

當(dāng)你從 AngularJS 遷移到 Angular 時(shí),你會(huì)希望把盡可能多的責(zé)任移交給 Angular,以便利用新的 API。為了幫你完成這種轉(zhuǎn)換,Angular 提供了 ?LocationUpgradeModule?。該模塊支持統(tǒng)一位置服務(wù),可以把 AngularJS 中 ?$location? 服務(wù)的職責(zé)轉(zhuǎn)給 Angular 的 ?Location ?服務(wù)。

要使用 ?LocationUpgradeModule?,就會(huì)從 ?@angular/common/upgrade? 中導(dǎo)入此符號(hào),并使用靜態(tài)方法 ?LocationUpgradeModule.config()? 把它添加到你的 ?AppModule ?導(dǎo)入表(?imports?)中。

// Other imports ...
import { LocationUpgradeModule } from '@angular/common/upgrade';

@NgModule({
  imports: [
    // Other NgModule imports...
    LocationUpgradeModule.config()
  ]
})
export class AppModule {}

?LocationUpgradeModule.config()? 方法接受一個(gè)配置對(duì)象,該對(duì)象的 ?useHash ?為 ?LocationStrategy?,?hashPrefix ?為 URL 前綴。

?useHash ?屬性默認(rèn)為 ?false?,而 ?hashPrefix ?默認(rèn)為空 ?string?。傳遞配置對(duì)象可以覆蓋默認(rèn)值。

LocationUpgradeModule.config({
  useHash: true,
  hashPrefix: '!'
})

這會(huì)為 AngularJS 中的 ?$location? 提供者注冊一個(gè)替代品。一旦注冊成功,導(dǎo)航過程中所有由 AngularJS 觸發(fā)的導(dǎo)航、路由廣播消息以及任何必需的變更檢測周期都會(huì)改由 Angular 進(jìn)行處理。這樣,你就可以通過這個(gè)唯一的途徑在此混合應(yīng)用的兩個(gè)框架間進(jìn)行導(dǎo)航了。

要想在 AngularJS 中使用 ?$location? 服務(wù)作為提供者,你需要使用一個(gè)工廠提供者來降級(jí) ?$locationShim?。

// Other imports ...
import { $locationShim } from '@angular/common/upgrade';
import { downgradeInjectable } from '@angular/upgrade/static';

angular.module('myHybridApp', [...])
  .factory('$location', downgradeInjectable($locationShim));

一旦引入了 Angular 路由器,你只要使用 Angular 路由器就可以通過統(tǒng)一位置服務(wù)來觸發(fā)導(dǎo)航了,同時(shí),你仍然可以通過 AngularJS 和 Angular 進(jìn)行導(dǎo)航。

PhoneCat 升級(jí)教程

在本節(jié)和下節(jié)中,你將看一個(gè)完整的例子,它使用 ?upgrade ?模塊準(zhǔn)備和升級(jí)了一個(gè)應(yīng)用程序。 該應(yīng)用就是來自原 AngularJS 教程中的Angular PhoneCat。 那是我們很多人當(dāng)初開始 Angular 探險(xiǎn)之旅的地方。 現(xiàn)在,你會(huì)看到如何把該應(yīng)用帶入 Angular 的美麗新世界。

這期間,你將學(xué)到如何在實(shí)踐中應(yīng)用準(zhǔn)備指南中列出的那些重點(diǎn)步驟: 你先讓該應(yīng)用向 Angular 看齊,然后為它引入 SystemJS 模塊加載器和 TypeScript。

本教程基于 ?angular-phonecat? 教程的 1.5.x 版本,該教程保存在代碼倉庫的 1.5-snapshot 分支中。接下來,克隆 angular-phonecat 代碼倉庫,check out ?1.5-snapshot? 分支并應(yīng)用這些步驟。

在項(xiàng)目結(jié)構(gòu)方面,工作的起點(diǎn)是這樣的:

這確實(shí)是一個(gè)很好地起點(diǎn)。這些代碼使用了 AngularJS 1.5 的組件 API,并遵循了 AngularJS 風(fēng)格指南進(jìn)行組織, 在成功升級(jí)之前,這是一個(gè)很重要的準(zhǔn)備步驟。

  • 每個(gè)組件、服務(wù)和過濾器都在它自己的源文件中 —— 就像單一規(guī)則所要求的。
  • ?core?、?phone-detail? 和 ?phone-list? 模塊都在它們自己的子目錄中。那些子目錄除了包含 HTML 模板之外,還包含 JavaScript 代碼,它們共同完成一個(gè)特性。 這是按特性分目錄的結(jié)構(gòu) 和模塊化規(guī)則所要求的。
  • 單元測試都和應(yīng)用代碼在一起,它們很容易找到。就像規(guī)則 組織測試文件中要求的那樣。

切換到 TypeScript

因?yàn)槟銓⑹褂?nbsp;TypeScript 編寫 Angular 的代碼,所以在開始升級(jí)之前,先要把 TypeScript 的編譯器設(shè)置好。

你還將開始逐步淘汰 Bower 包管理器,換成 NPM。后面你將使用 NPM 來安裝新的依賴包,并最終從項(xiàng)目中移除 Bower。

先把 TypeScript 包安裝到項(xiàng)目中。

npm i typescript --save-dev

還要為那些沒有自帶類型信息的庫(比如 AngularJS、AngularJS Material 和 Jasmine)安裝類型定義文件。

對(duì)于 PhoneCat 應(yīng)用,我們可以運(yùn)行下列命令來安裝必要的類型定義文件:

npm install @types/jasmine @types/angular @types/angular-animate @types/angular-aria @types/angular-cookies @types/angular-mocks @types/angular-resource @types/angular-route @types/angular-sanitize --save-dev

如果你正在使用 AngularJS Material,你可以通過下列命令安裝其類型定義:

npm install @types/angular-material --save-dev

你還應(yīng)該要往項(xiàng)目目錄下添加一個(gè) ?tsconfig.json? 文件,?tsconfig.json? 文件會(huì)告訴 TypeScript 編譯器如何把 TypeScript 文件轉(zhuǎn)成 ES5 代碼,并打包進(jìn) CommonJS 模塊中。

最后,你應(yīng)該把下列 npm 腳本添加到 ?package.json? 中,用于把 TypeScript 文件編譯成 JavaScript (根據(jù) ?tsconfig.json? 的配置):

"scripts": {
  "tsc": "tsc",
  "tsc:w": "tsc -w",
  ...

現(xiàn)在,從命令行中用監(jiān)視模式啟動(dòng) TypeScript 編譯器:

npm run tsc:w

讓這個(gè)進(jìn)程一直在后臺(tái)運(yùn)行,監(jiān)聽任何變化并自動(dòng)重新編譯。

接下來,把 JavaScript 文件轉(zhuǎn)換成 TypeScript 文件。 由于 TypeScript 是 ECMAScript 2015 的一個(gè)超集,而 ES2015 又是 ECMAScript 5 的超集,所以你可以簡單的把文件的擴(kuò)展名從 ?.js? 換成 ?.ts?, 它們還是會(huì)像以前一樣工作。由于 TypeScript 編譯器仍在運(yùn)行,它會(huì)為每一個(gè) ?.ts? 文件生成對(duì)應(yīng)的 ?.js? 文件,而真正運(yùn)行的是編譯后的 ?.js? 文件。 如果你用 ?npm start? 開啟了本項(xiàng)目的 HTTP 服務(wù)器,你會(huì)在瀏覽器中看到一個(gè)功能完好的應(yīng)用。

有了 TypeScript,你就可以從它的一些特性中獲益了。此語言可以為 AngularJS 應(yīng)用提供很多價(jià)值。

首先,TypeScript 是一個(gè) ES2015 的超集。任何以前用 ES5 寫的程序(就像 PhoneCat 范例)都可以開始通過 TypeScript 納入那些添加到 ES2015 中的新特性。 這包括 ?let?、?const?、箭頭函數(shù)、函數(shù)默認(rèn)參數(shù)以及解構(gòu)(destructure)賦值。

你能做的另一件事就是把類型安全添加到代碼中。這實(shí)際上已經(jīng)部分完成了,因?yàn)槟阋呀?jīng)安裝了 AngularJS 的類型定義。 TypeScript 會(huì)幫你檢查是否正確調(diào)用了 AngularJS 的 API,—— 比如往 Angular 模塊中注冊組件。

你還能開始把類型注解添加到自己的代碼中,來從 TypeScript 的類型系統(tǒng)中獲得更多幫助。 比如,你可以給 ?checkmark ?過濾器加上注解,表明它期待一個(gè) ?boolean ?類型的參數(shù)。 這可以更清楚的表明此過濾器打算做什么

angular.
  module('core').
  filter('checkmark', () => {
    return (input: boolean) => input ? '\u2713' : '\u2718';
  });

在 ?Phone ?服務(wù)中,你可以明確的把 ?$resource? 服務(wù)聲明為 ?angular.resource.IResourceService?,一個(gè) AngularJS 類型定義提供的類型。

angular.
  module('core.phone').
  factory('Phone', ['$resource',
    ($resource: angular.resource.IResourceService) => {
      return $resource('phones/:phoneId.json', {}, {
        query: {
          method: 'GET',
          params: {phoneId: 'phones'},
          isArray: true
        }
      });
    }
  ]);

你可以在應(yīng)用的路由配置中使用同樣的技巧,那里你用到了 location 和 route 服務(wù)。 一旦為它們提供了類型信息,TypeScript 就能檢查你是否在用類型的正確參數(shù)來調(diào)用它們了。

angular.
  module('phonecatApp').
  config(['$locationProvider', '$routeProvider',
    function config($locationProvider: angular.ILocationProvider,
                    $routeProvider: angular.route.IRouteProvider) {
      $locationProvider.hashPrefix('!');

      $routeProvider.
        when('/phones', {
          template: '<phone-list></phone-list>'
        }).
        when('/phones/:phoneId', {
          template: '<phone-detail></phone-detail>'
        }).
        otherwise('/phones');
    }
  ]);
你安裝的這個(gè)AngularJS.x 類型定義文件 并不是由 Angular 開發(fā)組維護(hù)的,但它也已經(jīng)足夠全面了。借助這些類型定義的幫助,它可以為 AngularJS.x 程序加上全面的類型注解。
如果你想這么做,就在 ?tsconfig.json? 中啟用 ?noImplicitAny ?配置項(xiàng)。 這樣,如果遇到什么還沒有類型注解的代碼,TypeScript 編譯器就會(huì)顯示一個(gè)警告。 你可以用它作為指南,告訴你現(xiàn)在與一個(gè)完全類型化的項(xiàng)目距離還有多遠(yuǎn)。

你能用的另一個(gè) TypeScript 特性是。具體來講,你可以把控制器轉(zhuǎn)換成類。 這種方式下,你離成為 Angular 組件類就又近了一步,它會(huì)令你的升級(jí)之路變得更簡單。

AngularJS 期望控制器是一個(gè)構(gòu)造函數(shù)。這實(shí)際上就是 ES2015/TypeScript 中的類, 這也就意味著只要你把一個(gè)類注冊為組件控制器,AngularJS 就會(huì)愉快的使用它。

新的“電話列表(phone list)”組件控制器類是這樣的:

class PhoneListController {
  phones: any[];
  orderProp: string;
  query: string;

  static $inject = ['Phone'];
  constructor(Phone: any) {
    this.phones = Phone.query();
    this.orderProp = 'age';
  }

}

angular.
  module('phoneList').
  component('phoneList', {
    templateUrl: 'phone-list/phone-list.template.html',
    controller: PhoneListController
  });

以前在控制器函數(shù)中實(shí)現(xiàn)的一切,現(xiàn)在都改由類的構(gòu)造函數(shù)來實(shí)現(xiàn)了。類型注入注解通過靜態(tài)屬性 ?$inject? 被附加到了類上。在運(yùn)行時(shí),它們變成了 ?PhoneListController.$inject?。

該類還聲明了另外三個(gè)成員:電話列表、當(dāng)前排序鍵的名字和搜索條件。 這些東西你以前就加到了控制器上,只是從來沒有在任何地方顯式定義過它們。最后一個(gè)成員從未真正在 TypeScript 代碼中用過, 因?yàn)樗皇窃谀0逯斜灰眠^。但為了清晰起見,你還是應(yīng)該定義出此控制器應(yīng)有的所有成員。

在電話詳情控制器中,你有兩個(gè)成員:一個(gè)是用戶正在查看的電話,另一個(gè)是正在顯示的圖像:

class PhoneDetailController {
  phone: any;
  mainImageUrl: string;

  static $inject = ['$routeParams', 'Phone'];
  constructor($routeParams: angular.route.IRouteParamsService, Phone: any) {
    const phoneId = $routeParams.phoneId;
    this.phone = Phone.get({phoneId}, (phone: any) => {
      this.setImage(phone.images[0]);
    });
  }

  setImage(imageUrl: string) {
    this.mainImageUrl = imageUrl;
  }
}

angular.
  module('phoneDetail').
  component('phoneDetail', {
    templateUrl: 'phone-detail/phone-detail.template.html',
    controller: PhoneDetailController
  });

這已經(jīng)讓你的控制器代碼看起來更像 Angular 了。你的準(zhǔn)備工作做好了,可以引進(jìn) Angular 到項(xiàng)目中了。

如果項(xiàng)目中有任何 AngularJS 的服務(wù),它們也是轉(zhuǎn)換成類的優(yōu)秀候選人,像控制器一樣,它們也是構(gòu)造函數(shù)。 但是在本項(xiàng)目中,你只有一個(gè) ?Phone ?工廠,這有點(diǎn)特別,因?yàn)樗且粋€(gè) ?ngResource ?工廠。 所以你不會(huì)在準(zhǔn)備階段中處理它,而是在下一節(jié)中直接把它轉(zhuǎn)換成 Angular 服務(wù)。

安裝 Angular

準(zhǔn)備工作做完了,接下來就開始把 PhoneCat 升級(jí)到 Angular。 做完這些之后,就能把 AngularJS 從項(xiàng)目中完全移除了,但其中的關(guān)鍵是在不破壞此程序的前提下一小塊一小塊的完成它。

該項(xiàng)目還包含一些動(dòng)畫,在此指南的當(dāng)前版本你先不升級(jí)它,請(qǐng)到 Angular 動(dòng)畫中進(jìn)一步學(xué)習(xí)。

用 SystemJS 模塊加載器把 Angular 安裝到項(xiàng)目中。 看看升級(jí)的準(zhǔn)備工作中的指南,并從那里獲得如下配置:

  • 把 Angular 和其它新依賴添加到 ?package.json? 中
  • 把 SystemJS 的配置文件 ?systemjs.config.js? 添加到項(xiàng)目的根目錄。

這些完成之后,就運(yùn)行:

npm install

很快你就可以通過 ?index.html? 來把 Angular 的依賴快速加載到應(yīng)用中, 但首先,你得做一些目錄結(jié)構(gòu)調(diào)整。這是因?yàn)槟阏郎?zhǔn)備從 ?node_modules ?中加載文件,然而目前項(xiàng)目中的每一個(gè)文件都是從 ?/app? 目錄下加載的。

把 ?app/index.html? 移入項(xiàng)目的根目錄,然后把 ?package.json? 中的開發(fā)服務(wù)器根目錄也指向項(xiàng)目的根目錄,而不再是 ?app ?目錄:

"start": "http-server ./ -a localhost -p 8000 -c-1",

現(xiàn)在,你就能把項(xiàng)目根目錄下的每一樣?xùn)|西發(fā)給瀏覽器了。但你不想為了適應(yīng)開發(fā)環(huán)境中的設(shè)置,被迫修改應(yīng)用代碼中用到的所有圖片和數(shù)據(jù)的路徑。因此,你要往 ?index.html? 中添加一個(gè) ?<base>? 標(biāo)簽,它將導(dǎo)致各種相對(duì)路徑被解析回 ?/app? 目錄:

<base href="/app/">

現(xiàn)在你可以通過 SystemJS 加載 Angular 了。你還要把 Angular 的膩?zhàn)幽_本(polyfills) 和 SystemJS 的配置加到 ?<head>? 區(qū)的末尾,然后,你能就用 ?System.import? 來加載實(shí)際的應(yīng)用了:

<script src="/node_modules/core-js/client/shim.min.js"></script>
<script src="/node_modules/zone.js/bundles/zone.umd.js"></script>
<script src="/node_modules/systemjs/dist/system.src.js"></script>
<script src="/systemjs.config.js"></script>
<script>
  System.import('/app');
</script>

你還需要對(duì)升級(jí)的準(zhǔn)備工作期間安裝的 ?systemjs.config.js? 文件做一些調(diào)整。

在 SystemJS 加載期間為瀏覽器指出項(xiàng)目的根在哪里,而不再使用 ?<base>? URL。

再通過 ?npm install @angular/upgrade --save? 安裝 ?upgrade ?包,并為 ?@angular/upgrade/static? 包添加一個(gè)映射。

System.config({
  paths: {
    // paths serve as alias
    'npm:': '/node_modules/'
  },
  map: {
    'ng-loader': '../src/systemjs-angular-loader.js',
    app: '/app',
    /* . . . */
    '@angular/upgrade/static': 'npm:@angular/upgrade/fesm2015/static.mjs',
    /* . . . */
  },

創(chuàng)建 AppModule

現(xiàn)在,創(chuàng)建一個(gè)名叫 ?AppModule ?的根 ?NgModule ?類。 這里已經(jīng)有了一個(gè)名叫 ?app.module.ts? 的文件,其中存放著 AngularJS 的模塊。 把它改名為 ?app.module.ng1.ts?,同時(shí)也要在 ?index.html? 中修改對(duì)應(yīng)的腳本名。 文件的內(nèi)容保留:

// Define the `phonecatApp` AngularJS module
angular.module('phonecatApp', [
  'ngAnimate',
  'ngRoute',
  'core',
  'phoneDetail',
  'phoneList',
]);

然后創(chuàng)建一個(gè)新的 ?app.module.ts? 文件,其中是一個(gè)最小化的 ?NgModule ?類:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

@NgModule({
  imports: [
    BrowserModule,
  ],
})
export class AppModule {
}

引導(dǎo) PhoneCat 的混合式應(yīng)用

接下來,你把該應(yīng)用程序引導(dǎo)改裝為一個(gè)同時(shí)支持 AngularJS 和 Angular 的混合式應(yīng)用。 然后,就能開始把這些不可分割的小塊轉(zhuǎn)換到 Angular 了。

本應(yīng)用現(xiàn)在是使用宿主頁面中附加到 ?<html>? 元素上的 AngularJS 指令 ?ng-app? 引導(dǎo)的。 但在混合式應(yīng)用中,不能再這么用了。你得用?ngUpgrade bootstrap?方法代替。

首先,從 ?index.html? 中移除 ?ng-app?。然后在 ?AppModule ?中導(dǎo)入 ?UpgradeModule?,并改寫它的 ?ngDoBootstrap ?方法:

import { UpgradeModule } from '@angular/upgrade/static';

@NgModule({
  imports: [
    BrowserModule,
    UpgradeModule,
  ],
})
export class AppModule {
  constructor(private upgrade: UpgradeModule) { }
  ngDoBootstrap() {
    this.upgrade.bootstrap(document.documentElement, ['phonecatApp']);
  }
}

注意,你正在從內(nèi)部的 ?ngDoBootstrap ?中引導(dǎo) AngularJS 模塊。 它的參數(shù)和你在手動(dòng)引導(dǎo) AngularJS 時(shí)傳給 ?angular.bootstrap? 的是一樣的:應(yīng)用的根元素,和所要加載的 AngularJS 1.x 模塊的數(shù)組。

最后,在 ?app/main.ts? 中引導(dǎo)這個(gè) ?AppModule?。該文件在 ?systemjs.config.js? 中被配置為了應(yīng)用的入口,所以它已經(jīng)被加載進(jìn)了瀏覽器中。

import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app.module';

platformBrowserDynamic().bootstrapModule(AppModule);

現(xiàn)在,你同時(shí)運(yùn)行著 AngularJS 和 Angular。漂亮!不過你還沒有運(yùn)行什么實(shí)際的 Angular 組件,這就是接下來要做的。

為何要聲明 *angular* 為*angular.IAngularStatic*?
?@types/angular? 聲明為 UMD 模塊,根據(jù) UMD 類型的工作方式,一旦你在文件中有一條 ES6 的 ?import? 語句,所有的 UMD 類型化的模型必須都通過 ?import ?語句導(dǎo)入, 而是不是全局可用。
AngularJS 是日前是通過 ?index.html? 中的 script 標(biāo)簽加載,這意味著整個(gè)應(yīng)用是作為一個(gè)全局變量進(jìn)行訪問的, 使用同一個(gè) ?angular ?變量的實(shí)例。 但如果你使用 ?import * as angular from 'angular'?,我還需要徹底修改 AngularJS 應(yīng)用中加載每個(gè)文件的方式, 確保 AngularJS 應(yīng)用被正確加載。
這需要相當(dāng)多的努力,通常也不值得去做,特別是當(dāng)你正在朝著 Angular 前進(jìn)時(shí)。 但如果你把 ?angular ?聲明為 ?angular.IAngularStatic?,指明它是一個(gè)全局變量, 仍然可以獲得全面的類型支持。
為 Angular 應(yīng)用手動(dòng)創(chuàng)建 UMD 包
從 Angular 版本 13 開始,分發(fā)格式 中不再包含 UMD 包。
如果你的用例需要 UMD 格式,請(qǐng)使用 rollup 從平面 ES 模塊文件手動(dòng)生成包。
  1. 使用 ?npm ?全局安裝 ?rollup?
  2. npm i -g rollup
    
    npm i -g 匯總
  3. 輸出 ?rollup ?的版本并驗(yàn)證安裝是否成功
  4. rollup -v
  5. 為 ?rollup ?創(chuàng)建 ?rollup.config.js? 配置文件,以使用全局 ?ng ?命令來引用所有 Angular 框架的導(dǎo)出。
    • 創(chuàng)建一個(gè)名為 rollup.config.js 的文件
    • 將以下內(nèi)容復(fù)制到 rollup.config.js
    • export default {
        input: 'node_modules/@angular/core/fesm2015/core.js',
        output: {
          file: 'bundle.js',
          format: 'umd',
          name: 'ng'
        }
      }
  6. 使用 ?rollup ?根據(jù) ?rollup.config.js? 中的設(shè)置創(chuàng)建 ?bundle.js? UMD 包
  7. rollup -c rollup.config.js

?bundle.js? 文件包含你的 UMD 包。有關(guān) GitHub 上的示例,請(qǐng)參閱 UMD Angular 包。

升級(jí) Phone 服務(wù)

你要移植到 Angular 的第一個(gè)片段是 ?Phone ?工廠(位于 ?app/js/core/phones.factory.ts?), 并且讓它能幫助控制器從服務(wù)器上加載電話信息。目前,它是用 ?ngResource ?實(shí)現(xiàn)的,你用它做兩件事:

  • 把所有電話的列表加載到電話列表組件中。
  • 把一臺(tái)電話的詳情加載到電話詳情組件中。

你可以用 Angular 的服務(wù)類來替換這個(gè)實(shí)現(xiàn),而把控制器繼續(xù)留在 AngularJS 的地盤上。

在這個(gè)新版本中,你導(dǎo)入了 Angular 的 HTTP 模塊,并且用它的 ?HttpClient ?服務(wù)替換掉 ?ngResource?。

再次打開 ?app.module.ts? 文件,導(dǎo)入并把 ?HttpClientModule ?添加到 ?AppModule ?的 ?imports ?數(shù)組中:

import { HttpClientModule } from '@angular/common/http';

@NgModule({
  imports: [
    BrowserModule,
    UpgradeModule,
    HttpClientModule,
  ],
})
export class AppModule {
  constructor(private upgrade: UpgradeModule) { }
  ngDoBootstrap() {
    this.upgrade.bootstrap(document.documentElement, ['phonecatApp']);
  }
}

現(xiàn)在,你已經(jīng)準(zhǔn)備好了升級(jí) ?Phones ?服務(wù)本身。你將為 ?phone.service.ts? 文件中基于 ngResource 的服務(wù)加上 ?@Injectable? 裝飾器:

@Injectable()
export class Phone {
/* . . . */
}

?@Injectable? 裝飾器將把一些依賴注入相關(guān)的元數(shù)據(jù)附加到該類上,讓 Angular 知道它的依賴信息。 就像在依賴注入指南中描述過的那樣, 這是一個(gè)標(biāo)記裝飾器,你要把它用在那些沒有其它 Angular 裝飾器,并且自己有依賴注入的類上。

在它的構(gòu)造函數(shù)中,該類期待一個(gè) ?HttpClient ?服務(wù)。?HttpClient ?服務(wù)將被注入進(jìn)來并存入一個(gè)私有字段。 然后該服務(wù)在兩個(gè)實(shí)例方法中被使用到,一個(gè)加載所有電話的列表,另一個(gè)加載一臺(tái)指定電話的詳情:

@Injectable()
export class Phone {
  constructor(private http: HttpClient) { }
  query(): Observable<PhoneData[]> {
    return this.http.get<PhoneData[]>(`phones/phones.json`);
  }
  get(id: string): Observable<PhoneData> {
    return this.http.get<PhoneData>(`phones/${id}.json`);
  }
}

該方法現(xiàn)在返回一個(gè) ?Phone ?類型或 ?Phone[]? 類型的可觀察對(duì)象(Observable)。 這是一個(gè)你從未用過的類型,因此你得為它新增一個(gè)簡單的接口:

export interface PhoneData {
  name: string;
  snippet: string;
  images: string[];
}

?@angular/upgrade/static? 有一個(gè) ?downgradeInjectable ?方法,可以使 Angular 服務(wù)在 AngularJS 的代碼中可用。 使用它來插入 ?Phone ?服務(wù):

declare var angular: angular.IAngularStatic;
import { downgradeInjectable } from '@angular/upgrade/static';
/* . . . */
@Injectable()
export class Phone {
/* . . . */
}

angular.module('core.phone')
  .factory('phone', downgradeInjectable(Phone));

最終,該類的全部代碼如下:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

declare var angular: angular.IAngularStatic;
import { downgradeInjectable } from '@angular/upgrade/static';

export interface PhoneData {
  name: string;
  snippet: string;
  images: string[];
}

@Injectable()
export class Phone {
  constructor(private http: HttpClient) { }
  query(): Observable<PhoneData[]> {
    return this.http.get<PhoneData[]>(`phones/phones.json`);
  }
  get(id: string): Observable<PhoneData> {
    return this.http.get<PhoneData>(`phones/${id}.json`);
  }
}

angular.module('core.phone')
  .factory('phone', downgradeInjectable(Phone));

注意,你要單獨(dú)導(dǎo)入了 RxJS ?Observable ?中的 ?map ?操作符。 對(duì)每個(gè) RxJS 操作符都要這么做。

這個(gè)新的 ?Phone ?服務(wù)具有和老的基于 ?ngResource ?的服務(wù)相同的特性。 因?yàn)樗?nbsp;Angular 服務(wù),你通過 ?NgModule ?的 ?providers ?數(shù)組來注冊它:

import { Phone } from './core/phone/phone.service';

@NgModule({
  imports: [
    BrowserModule,
    UpgradeModule,
    HttpClientModule,
  ],
  providers: [
    Phone,
  ]
})
export class AppModule {
  constructor(private upgrade: UpgradeModule) { }
  ngDoBootstrap() {
    this.upgrade.bootstrap(document.documentElement, ['phonecatApp']);
  }
}

現(xiàn)在,你正在用 SystemJS 加載 ?phone.service.ts?,你應(yīng)該從 ?index.html? 中移除該服務(wù)的 ?<script>? 標(biāo)簽。 這也是你在升級(jí)所有組件時(shí)將會(huì)做的事。在從 AngularJS 向 Angular 升級(jí)的同時(shí),你也把代碼從腳本移植為模塊。

這時(shí),你可以把兩個(gè)控制器從使用老的服務(wù)切換成使用新的。你像降級(jí)過的 ?phones ?工廠一樣 ?$inject? 它, 但它實(shí)際上是一個(gè) ?Phones ?類的實(shí)例,并且你可以據(jù)此注解它的類型:

declare var angular: angular.IAngularStatic;
import { Phone, PhoneData } from '../core/phone/phone.service';

class PhoneListController {
  phones: PhoneData[];
  orderProp: string;

  static $inject = ['phone'];
  constructor(phone: Phone) {
    phone.query().subscribe(phones => {
      this.phones = phones;
    });
    this.orderProp = 'age';
  }

}

angular.
  module('phoneList').
  component('phoneList', {
    templateUrl: 'app/phone-list/phone-list.template.html',
    controller: PhoneListController
  });
declare var angular: angular.IAngularStatic;
import { Phone, PhoneData } from '../core/phone/phone.service';

class PhoneDetailController {
  phone: PhoneData;
  mainImageUrl: string;

  static $inject = ['$routeParams', 'phone'];
  constructor($routeParams: angular.route.IRouteParamsService, phone: Phone) {
    const phoneId = $routeParams.phoneId;
    phone.get(phoneId).subscribe(data => {
      this.phone = data;
      this.setImage(data.images[0]);
    });
  }

  setImage(imageUrl: string) {
    this.mainImageUrl = imageUrl;
  }
}

angular.
  module('phoneDetail').
  component('phoneDetail', {
    templateUrl: 'phone-detail/phone-detail.template.html',
    controller: PhoneDetailController
  });

這里的兩個(gè) AngularJS 控制器在使用 Angular 的服務(wù)!控制器不需要關(guān)心這一點(diǎn),盡管實(shí)際上該服務(wù)返回的是可觀察對(duì)象(Observable),而不是承諾(Promise)。 無論如何,你達(dá)到的效果都是把服務(wù)移植到 Angular,而不用被迫移植組件來使用它。

你也能使用 ?Observable ?的 ?toPromise ?方法來在服務(wù)中把這些可觀察對(duì)象轉(zhuǎn)變成承諾,以進(jìn)一步減小組件控制器中需要修改的代碼量。

升級(jí)組件

接下來,把 AngularJS 的控制器升級(jí)成 Angular 的組件。每次升級(jí)一個(gè),同時(shí)仍然保持應(yīng)用運(yùn)行在混合模式下。 在做轉(zhuǎn)換的同時(shí),你還將自定義首個(gè) Angular管道

先看看電話列表組件。它目前包含一個(gè) TypeScript 控制器類和一個(gè)組件定義對(duì)象。重命名控制器類, 并把 AngularJS 的組件定義對(duì)象更換為 Angular ?@Component? 裝飾器,這樣你就把它變形為 Angular 的組件了。然后,你還要從類中移除靜態(tài) ?$inject? 屬性。

import { Component } from '@angular/core';
import { Phone, PhoneData } from '../core/phone/phone.service';

@Component({
  selector: 'phone-list',
  templateUrl: './phone-list.template.html'
})
export class PhoneListComponent {
  phones: PhoneData[];
  query: string;
  orderProp: string;

  constructor(phone: Phone) {
    phone.query().subscribe(phones => {
      this.phones = phones;
    });
    this.orderProp = 'age';
  }
  /* . . . */
}

?selector? 屬性是一個(gè) CSS 選擇器,用來定義組件應(yīng)該被放在頁面的哪。在 AngularJS 中,你會(huì)基于組件名字來匹配, 但是在 Angular 中,你要顯式指定這些選擇器。本組件將會(huì)對(duì)應(yīng)元素名字 ?phone-list?,和 AngularJS 版本一樣。

現(xiàn)在,將組件的模版也轉(zhuǎn)換為 Angular 語法。在搜索控件中,把 AngularJS 的 ?$ctrl? 表達(dá)式替換成 Angular 的雙向綁定語法 ?[(ngModel)]?:

<p>
  Search:
  <input [(ngModel)]="query" />
</p>

<p>
  Sort by:
  <select [(ngModel)]="orderProp">
    <option value="name">Alphabetical</option>
    <option value="age">Newest</option>
  </select>
</p>

把列表中的 ?ng-repeat? 替換為 ?*ngFor? 以及它的 ?let var of iterable? 語法, 該語法在模板語法指南中講過。 再把 ?img ?標(biāo)簽的 ?ng-src? 替換為一個(gè)標(biāo)準(zhǔn)的 ?src ?屬性(property)綁定。

<ul class="phones">
  <li *ngFor="let phone of getPhones()"
      class="thumbnail phone-list-item">
    <a href="/#!/phones/{{phone.id}}" class="thumb">
      <img [src]="phone.imageUrl" [alt]="phone.name" />
    </a>
    <a href="/#!/phones/{{phone.id}}" class="name">{{phone.name}}</a>
    <p>{{phone.snippet}}</p>
  </li>
</ul>

Angular 中沒有 filter 或 orderBy 過濾器

Angular 中并不存在 AngularJS 中內(nèi)置的 ?filter ?和 ?orderBy ?過濾器。 所以你得自己實(shí)現(xiàn)進(jìn)行過濾和排序。

你把 ?filter ?和 ?orderBy ?過濾器改成綁定到控制器中的 ?getPhones()? 方法,通過該方法,組件本身實(shí)現(xiàn)了過濾和排序邏輯。

getPhones(): PhoneData[] {
  return this.sortPhones(this.filterPhones(this.phones));
}

private filterPhones(phones: PhoneData[]) {
  if (phones && this.query) {
    return phones.filter(phone => {
      const name = phone.name.toLowerCase();
      const snippet = phone.snippet.toLowerCase();
      return name.indexOf(this.query) >= 0 || snippet.indexOf(this.query) >= 0;
    });
  }
  return phones;
}

private sortPhones(phones: PhoneData[]) {
  if (phones && this.orderProp) {
    return phones
      .slice(0) // Make a copy
      .sort((a, b) => {
        if (a[this.orderProp] < b[this.orderProp]) {
          return -1;
        } else if ([b[this.orderProp] < a[this.orderProp]]) {
          return 1;
        } else {
          return 0;
        }
      });
  }
  return phones;
}

現(xiàn)在你需要降級(jí)你的 Angular 組件,這樣你就可以在 AngularJS 中使用它了。 你要注冊一個(gè) ?phoneList?指令,而不是注冊一個(gè)組件,它是一個(gè)降級(jí)版的 Angular 組件。

強(qiáng)制類型轉(zhuǎn)換 ?as angular.IDirectiveFactory? 告訴 TypeScript 編譯器 ?downgradeComponent ?方法 的返回值是一個(gè)指令工廠。

declare var angular: angular.IAngularStatic;
import { downgradeComponent } from '@angular/upgrade/static';

/* . . . */
@Component({
  selector: 'phone-list',
  templateUrl: './phone-list.template.html'
})
export class PhoneListComponent {
  /* . . . */
}

angular.module('phoneList')
  .directive(
    'phoneList',
    downgradeComponent({component: PhoneListComponent}) as angular.IDirectiveFactory
  );

新的 ?PhoneListComponent ?使用 Angular 的 ?ngModel ?指令,它位于 ?FormsModule ?中。 把 ?FormsModule ?添加到 ?NgModule ?的 ?imports ?中,并聲明新的 ?PhoneListComponent ?組件, 最后,把降級(jí)的結(jié)果添加到 ?entryComponents ?中:

import { FormsModule } from '@angular/forms';
import { PhoneListComponent } from './phone-list/phone-list.component';

@NgModule({
  imports: [
    BrowserModule,
    UpgradeModule,
    HttpClientModule,
    FormsModule,
  ],
  declarations: [
    PhoneListComponent,
  ],
})
export class AppModule {
  constructor(private upgrade: UpgradeModule) { }
  ngDoBootstrap() {
    this.upgrade.bootstrap(document.documentElement, ['phonecatApp']);
  }
}

從 ?index.html? 中移除電話列表組件的<script>標(biāo)簽。

現(xiàn)在,剩下的 ?phone-detail.component.ts? 文件變成了這樣:

declare var angular: angular.IAngularStatic;
import { downgradeComponent } from '@angular/upgrade/static';

import { Component } from '@angular/core';

import { Phone, PhoneData } from '../core/phone/phone.service';
import { RouteParams } from '../ajs-upgraded-providers';

@Component({
  selector: 'phone-detail',
  templateUrl: './phone-detail.template.html',
})
export class PhoneDetailComponent {
  phone: PhoneData;
  mainImageUrl: string;

  constructor(routeParams: RouteParams, phone: Phone) {
    phone.get(routeParams.phoneId).subscribe(data => {
      this.phone = data;
      this.setImage(data.images[0]);
    });
  }

  setImage(imageUrl: string) {
    this.mainImageUrl = imageUrl;
  }
}

angular.module('phoneDetail')
  .directive(
    'phoneDetail',
    downgradeComponent({component: PhoneDetailComponent}) as angular.IDirectiveFactory
  );

這和電話列表組件很相似。 這里的竅門在于 ?@Inject? 裝飾器,它標(biāo)記出了 ?$routeParams? 依賴。

AngularJS 注入器具有 AngularJS 路由器的依賴,叫做 ?$routeParams?。 它被注入到了 ?PhoneDetails ?中,但 ?PhoneDetails ?現(xiàn)在還是一個(gè) AngularJS 控制器。 你要把它注入到新的 ?PhoneDetailsComponent ?中。

不幸的是,AngularJS 的依賴不會(huì)自動(dòng)在 Angular 的組件中可用。 你必須使用工廠提供者(factory provider) 來把 ?$routeParams? 包裝成 Angular 的服務(wù)提供者。 新建一個(gè)名叫 ?ajs-upgraded-providers.ts? 的文件,并且在 ?app.module.ts? 中導(dǎo)入它:

export abstract class RouteParams {
  [key: string]: string;
}

export function routeParamsFactory(i: any) {
  return i.get('$routeParams');
}

export const routeParamsProvider = {
  provide: RouteParams,
  useFactory: routeParamsFactory,
  deps: ['$injector']
};
import { routeParamsProvider } from './ajs-upgraded-providers';
  providers: [
    Phone,
    routeParamsProvider
  ]

把該組件的模板轉(zhuǎn)變成 Angular 的語法,代碼如下:

<div *ngIf="phone">
  <div class="phone-images">
    <img [src]="img" class="phone"
        [ngClass]="{'selected': img === mainImageUrl}"
        *ngFor="let img of phone.images" />
  </div>

  <h1>{{phone.name}}</h1>

  <p>{{phone.description}}</p>

  <ul class="phone-thumbs">
    <li *ngFor="let img of phone.images">
      <img [src]="img" (click)="setImage(img)" />
    </li>
  </ul>

  <ul class="specs">
    <li>
      <span>Availability and Networks</span>
      <dl>
        <dt>Availability</dt>
        <dd *ngFor="let availability of phone.availability">{{availability}}</dd>
      </dl>
    </li>
    <li>
      <span>Battery</span>
      <dl>
        <dt>Type</dt>
        <dd>{{phone.battery?.type}}</dd>
        <dt>Talk Time</dt>
        <dd>{{phone.battery?.talkTime}}</dd>
        <dt>Standby time (max)</dt>
        <dd>{{phone.battery?.standbyTime}}</dd>
      </dl>
    </li>
    <li>
      <span>Storage and Memory</span>
      <dl>
        <dt>RAM</dt>
        <dd>{{phone.storage?.ram}}</dd>
        <dt>Internal Storage</dt>
        <dd>{{phone.storage?.flash}}</dd>
      </dl>
    </li>
    <li>
      <span>Connectivity</span>
      <dl>
        <dt>Network Support</dt>
        <dd>{{phone.connectivity?.cell}}</dd>
        <dt>WiFi</dt>
        <dd>{{phone.connectivity?.wifi}}</dd>
        <dt>Bluetooth</dt>
        <dd>{{phone.connectivity?.bluetooth}}</dd>
        <dt>Infrared</dt>
        <dd>{{phone.connectivity?.infrared | checkmark}}</dd>
        <dt>GPS</dt>
        <dd>{{phone.connectivity?.gps | checkmark}}</dd>
      </dl>
    </li>
    <li>
      <span>Android</span>
      <dl>
        <dt>OS Version</dt>
        <dd>{{phone.android?.os}}</dd>
        <dt>UI</dt>
        <dd>{{phone.android?.ui}}</dd>
      </dl>
    </li>
    <li>
      <span>Size and Weight</span>
      <dl>
        <dt>Dimensions</dt>
        <dd *ngFor="let dim of phone.sizeAndWeight?.dimensions">{{dim}}</dd>
        <dt>Weight</dt>
        <dd>{{phone.sizeAndWeight?.weight}}</dd>
      </dl>
    </li>
    <li>
      <span>Display</span>
      <dl>
        <dt>Screen size</dt>
        <dd>{{phone.display?.screenSize}}</dd>
        <dt>Screen resolution</dt>
        <dd>{{phone.display?.screenResolution}}</dd>
        <dt>Touch screen</dt>
        <dd>{{phone.display?.touchScreen | checkmark}}</dd>
      </dl>
    </li>
    <li>
      <span>Hardware</span>
      <dl>
        <dt>CPU</dt>
        <dd>{{phone.hardware?.cpu}}</dd>
        <dt>USB</dt>
        <dd>{{phone.hardware?.usb}}</dd>
        <dt>Audio / headphone jack</dt>
        <dd>{{phone.hardware?.audioJack}}</dd>
        <dt>FM Radio</dt>
        <dd>{{phone.hardware?.fmRadio | checkmark}}</dd>
        <dt>Accelerometer</dt>
        <dd>{{phone.hardware?.accelerometer | checkmark}}</dd>
      </dl>
    </li>
    <li>
      <span>Camera</span>
      <dl>
        <dt>Primary</dt>
        <dd>{{phone.camera?.primary}}</dd>
        <dt>Features</dt>
        <dd>{{phone.camera?.features?.join(', ')}}</dd>
      </dl>
    </li>
    <li>
      <span>Additional Features</span>
      <dd>{{phone.additionalFeatures}}</dd>
    </li>
  </ul>
</div>

這里有幾個(gè)值得注意的改動(dòng):

  • 你從所有表達(dá)式中移除了 ?$ctrl.? 前綴。
  • 正如你在電話列表中做過的那樣,你把 ?ng-src? 替換成了標(biāo)準(zhǔn)的 ?src ?屬性綁定。
  • 你在 ?ng-class? 周圍使用了屬性綁定語法。雖然 Angular 中有一個(gè) 和 AngularJS 中非常相似的 ?ngClass?指令, 但是它的值不會(huì)神奇的作為表達(dá)式進(jìn)行計(jì)算。在 Angular 中,模板中的屬性(Attribute)值總是被作為 屬性(Property)表達(dá)式計(jì)算,而不是作為字符串字面量。
  • 你把 ?ng-repeat? 替換成了 ?*ngFor?。
  • 你把 ?ng-click? 替換成了一個(gè)到標(biāo)準(zhǔn) ?click ?事件的綁定。
  • 你把整個(gè)模板都包裹進(jìn)了一個(gè) ?ngIf ?中,這導(dǎo)致只有當(dāng)存在一個(gè)電話時(shí)它才會(huì)渲染。你必須這么做, 是因?yàn)榻M件首次加載時(shí)你還沒有 ?phone ?變量,這些表達(dá)式就會(huì)引用到一個(gè)不存在的值。 和 AngularJS 不同,當(dāng)你嘗試引用未定義對(duì)象上的屬性時(shí),Angular 中的表達(dá)式不會(huì)默默失敗。 你必須明確指出這種情況是你所期望的。

把 ?PhoneDetailComponent ?組件添加到 ?NgModule ?的 ?declarations ?和 ?entryComponents ?中:

import { PhoneDetailComponent } from './phone-detail/phone-detail.component';

@NgModule({
  imports: [
    BrowserModule,
    UpgradeModule,
    HttpClientModule,
    FormsModule,
  ],
  declarations: [
    PhoneListComponent,
    PhoneDetailComponent,
  ],
  providers: [
    Phone,
    routeParamsProvider
  ]
})
export class AppModule {
  constructor(private upgrade: UpgradeModule) { }
  ngDoBootstrap() {
    this.upgrade.bootstrap(document.documentElement, ['phonecatApp']);
  }
}

你現(xiàn)在應(yīng)該從 ?index.html? 中移除電話詳情組件的<script>。

添加 CheckmarkPipe

AngularJS 指令中有一個(gè) ?checkmark?過濾器,把它轉(zhuǎn)換成 Angular 的管道。

沒有什么升級(jí)方法能把過濾器轉(zhuǎn)換成管道。 但你也并不需要它。 把過濾器函數(shù)轉(zhuǎn)換成等價(jià)的 Pipe 類非常簡單。 實(shí)現(xiàn)方式和以前一樣,但把它們包裝進(jìn) ?transform ?方法中就可以了。 把該文件改名成 ?checkmark.pipe.ts?,以符合 Angular 中的命名約定:

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({name: 'checkmark'})
export class CheckmarkPipe implements PipeTransform {
  transform(input: boolean) {
    return input ? '\u2713' : '\u2718';
  }
}

現(xiàn)在,導(dǎo)入并聲明這個(gè)新創(chuàng)建的管道,同時(shí)從 ?index.html? 文件中移除該過濾器的 ?<script>? 標(biāo)簽:

import { CheckmarkPipe } from './core/checkmark/checkmark.pipe';

@NgModule({
  imports: [
    BrowserModule,
    UpgradeModule,
    HttpClientModule,
    FormsModule,
  ],
  declarations: [
    PhoneListComponent,
    PhoneDetailComponent,
    CheckmarkPipe
  ],
  providers: [
    Phone,
    routeParamsProvider
  ]
})
export class AppModule {
  constructor(private upgrade: UpgradeModule) { }
  ngDoBootstrap() {
    this.upgrade.bootstrap(document.documentElement, ['phonecatApp']);
  }
}

對(duì)混合式應(yīng)用做 AOT 編譯

要在混合式應(yīng)用中使用 AOT 編譯,你首先要像其它 Angular 應(yīng)用一樣設(shè)置它,就像AOT 編譯一章所講的那樣。

然后修改 ?main-aot.ts? 的引導(dǎo)代碼,來引導(dǎo) AOT 編譯器所生成的 ?AppComponentFactory?:

import { platformBrowser } from '@angular/platform-browser';

import { AppModuleNgFactory } from './app.module.ngfactory';

platformBrowser().bootstrapModuleFactory(AppModuleNgFactory);

你還要把在 ?index.html? 中已經(jīng)用到的所有 AngularJS 文件加載到 ?aot/index.html? 中:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">

    <base href="/app/">

    <title>Google Phone Gallery</title>
    <link rel="stylesheet"  rel="external nofollow" target="_blank"  rel="external nofollow" target="_blank"  />
    <link rel="stylesheet" href="app.css" />
    <link rel="stylesheet" href="app.animations.css" />

    <script src="https://code.jquery.com/jquery-2.2.4.js" rel="external nofollow" ></script>
    <script src="https://code.angularjs.org/1.5.5/angular.js" rel="external nofollow" ></script>
    <script src="https://code.angularjs.org/1.5.5/angular-animate.js" rel="external nofollow" ></script>
    <script src="https://code.angularjs.org/1.5.5/angular-resource.js" rel="external nofollow" ></script>
    <script src="https://code.angularjs.org/1.5.5/angular-route.js" rel="external nofollow" ></script>

    <script src="app.module.ajs.js"></script>
    <script src="app.config.js"></script>
    <script src="app.animations.js"></script>
    <script src="core/core.module.js"></script>
    <script src="core/phone/phone.module.js"></script>
    <script src="phone-list/phone-list.module.js"></script>
    <script src="phone-detail/phone-detail.module.js"></script>

    <script src="/node_modules/core-js/client/shim.min.js"></script>
    <script src="/node_modules/zone.js/bundles/zone.umd.min.js"></script>

    <script>window.module = 'aot';</script>
  </head>

  <body>
    <div class="view-container">
      <div ng-view class="view-frame"></div>
    </div>
  </body>
  <script src="/dist/build.js"></script>
</html>

這些文件要帶著相應(yīng)的膩?zhàn)幽_本復(fù)制到一起。應(yīng)用運(yùn)行時(shí)需要的文件,比如電話列表 ?.json? 和圖片,也需要復(fù)制過去。

通過 ?npm install fs-extra --save-dev? 安裝 ?fs-extra? 可以更好的復(fù)制文件,并且把 ?copy-dist-files.js? 文件改成這樣:

var fsExtra = require('fs-extra');
var resources = [
  // polyfills
  'node_modules/core-js/client/shim.min.js',
  'node_modules/zone.js/bundles/zone.umd.min.js',
  // css
  'app/app.css',
  'app/app.animations.css',
  // images and json files
  'app/img/',
  'app/phones/',
  // app files
  'app/app.module.ajs.js',
  'app/app.config.js',
  'app/app.animations.js',
  'app/core/core.module.js',
  'app/core/phone/phone.module.js',
  'app/phone-list/phone-list.module.js',
  'app/phone-detail/phone-detail.module.js'
];
resources.map(function(sourcePath) {
  // Need to rename zone.umd.min.js to zone.min.js
  var destPath = `aot/${sourcePath}`.replace('.umd.min.js', '.min.js');
  fsExtra.copySync(sourcePath, destPath);
});

這就是想要在升級(jí)應(yīng)用期間 AOT 編譯所需的一切!

添加 Angular 路由器和引導(dǎo)程序

此刻,你已經(jīng)把所有 AngularJS 的組件替換成了它們在 Angular 中的等價(jià)物,不過你仍然在 AngularJS 路由器中使用它們。

添加 Angular 路由器

Angular 有一個(gè)全新的路由器。

像所有的路由器一樣,它需要在 UI 中指定一個(gè)位置來顯示路由的視圖。 在 Angular 中,它是 ?<router-outlet>?,并位于應(yīng)用組件樹頂部的根組件中。

你還沒有這樣一個(gè)根組件,因?yàn)樵搼?yīng)用仍然是像一個(gè) AngularJS 應(yīng)用那樣被管理的。 創(chuàng)建新的 ?app.component.ts? 文件,放入像這樣的 ?AppComponent ?類:

import { Component } from '@angular/core';

@Component({
  selector: 'phonecat-app',
  template: '<router-outlet></router-outlet>'
})
export class AppComponent { }

它有一個(gè)很簡單的模板,只包含 Angular 路由的 ?<router-outlet>?。 該組件只負(fù)責(zé)渲染活動(dòng)路由的內(nèi)容,此外啥也不干。

該選擇器告訴 Angular:當(dāng)應(yīng)用啟動(dòng)時(shí)就把這個(gè)根組件插入到宿主頁面的 ?<phonecat-app>? 元素中。

把這個(gè) ?<phonecat-app>? 元素插入到 ?index.html? 中。 用它來代替 AngularJS 中的 ?ng-view? 指令:

<body>
  <phonecat-app></phonecat-app>
</body>

創(chuàng)建路由模塊

無論在 AngularJS 還是 Angular 或其它框架中,路由器都需要進(jìn)行配置。

Angular 路由器配置的詳情最好去查閱下路由與導(dǎo)航文檔。 它建議你創(chuàng)建一個(gè)專們用于路由器配置的 ?NgModule?(名叫路由模塊)。

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { APP_BASE_HREF, HashLocationStrategy, LocationStrategy } from '@angular/common';

import { PhoneDetailComponent } from './phone-detail/phone-detail.component';
import { PhoneListComponent } from './phone-list/phone-list.component';

const routes: Routes = [
  { path: '', redirectTo: 'phones', pathMatch: 'full' },
  { path: 'phones',          component: PhoneListComponent },
  { path: 'phones/:phoneId', component: PhoneDetailComponent }
];

@NgModule({
  imports: [ RouterModule.forRoot(routes) ],
  exports: [ RouterModule ],
  providers: [
    { provide: APP_BASE_HREF, useValue: '!' },
    { provide: LocationStrategy, useClass: HashLocationStrategy },
  ]
})
export class AppRoutingModule { }

該模塊定義了一個(gè) ?routes ?對(duì)象,它帶有兩個(gè)路由,分別指向兩個(gè)電話組件,以及為空路徑指定的默認(rèn)路由。 它把 ?routes ?傳給 ?RouterModule.forRoot? 方法,該方法會(huì)完成剩下的事。

一些額外的提供者讓路由器使用“hash”策略解析 URL,比如 ?#!/phones?,而不是默認(rèn)的“Push State”策略。

現(xiàn)在,修改 ?AppModule?,讓它導(dǎo)入這個(gè) ?AppRoutingModule?,并同時(shí)聲明根組件 ?AppComponent?。 這會(huì)告訴 Angular,它應(yīng)該使用根組件 ?AppComponent ?引導(dǎo)應(yīng)用,并把它的視圖插入到宿主頁面中。

你還要從 ?app.module.ts? 中移除調(diào)用 ?ngDoBootstrap()? 來引導(dǎo) AngularJS 模塊的代碼,以及對(duì) ?UpgradeModule ?的導(dǎo)入代碼。

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { CheckmarkPipe } from './core/checkmark/checkmark.pipe';
import { Phone } from './core/phone/phone.service';
import { PhoneDetailComponent } from './phone-detail/phone-detail.component';
import { PhoneListComponent } from './phone-list/phone-list.component';

@NgModule({
  imports: [
    BrowserModule,
    FormsModule,
    HttpClientModule,
    AppRoutingModule
  ],
  declarations: [
    AppComponent,
    PhoneListComponent,
    CheckmarkPipe,
    PhoneDetailComponent
  ],
  providers: [
    Phone
  ],
  bootstrap: [ AppComponent ]
})
export class AppModule {}

而且,由于你現(xiàn)在直接路由到 PhoneListComponent 和 PhoneDetailComponent,而不再使用帶 <phone-list> 或 <phone-detail> 標(biāo)簽的路由模板,因此你同樣不再需要它們的 Angular 選擇器。

為每個(gè)電話生成鏈接

在電話列表中,你不用再被迫硬編碼電話詳情的鏈接了。 你可以通過把每個(gè)電話的 ?id ?綁定到 ?routerLink ?指令來生成它們了,該指令的構(gòu)造函數(shù)會(huì)為 ?PhoneDetailComponent ?生成正確的 URL:

<ul class="phones">
  <li *ngFor="let phone of getPhones()"
      class="thumbnail phone-list-item">
    <a [routerLink]="['/phones', phone.id]" class="thumb">
      <img [src]="phone.imageUrl" [alt]="phone.name" />
    </a>
    <a [routerLink]="['/phones', phone.id]" class="name">{{phone.name}}</a>
    <p>{{phone.snippet}}</p>
  </li>
</ul>

使用路由參數(shù)

Angular 路由器會(huì)傳入不同的路由參數(shù)。 改正 ?PhoneDetail ?組件的構(gòu)造函數(shù),讓它改用注入進(jìn)來的 ?ActivatedRoute ?對(duì)象。 從 ?ActivatedRoute.snapshot.params? 中提取出 ?phoneId?,并像以前一樣獲取手機(jī)的數(shù)據(jù):

import { Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router';

import { Phone, PhoneData } from '../core/phone/phone.service';

@Component({
  selector: 'phone-detail',
  templateUrl: './phone-detail.template.html'
})
export class PhoneDetailComponent {
  phone: PhoneData;
  mainImageUrl: string;

  constructor(activatedRoute: ActivatedRoute, phone: Phone) {
    phone.get(activatedRoute.snapshot.paramMap.get('phoneId'))
      .subscribe((p: PhoneData) => {
        this.phone = p;
        this.setImage(p.images[0]);
      });
  }

  setImage(imageUrl: string) {
    this.mainImageUrl = imageUrl;
  }
}

你現(xiàn)在運(yùn)行的就是純正的 Angular 應(yīng)用了!

再見,AngularJS!

終于可以把輔助訓(xùn)練的輪子摘下來了!讓你的應(yīng)用作為一個(gè)純粹、閃亮的 Angular 程序開始它的新生命吧。 剩下的所有任務(wù)就是移除代碼 —— 這當(dāng)然是每個(gè)程序員最喜歡的任務(wù)!

應(yīng)用仍然以混合式應(yīng)用的方式啟動(dòng),然而這再也沒有必要了。

把應(yīng)用的引導(dǎo)(?bootstrap?)方法從 ?UpgradeAdapter ?的改為 Angular 的。

import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

import { AppModule } from './app.module';

platformBrowserDynamic().bootstrapModule(AppModule);

如果你還沒有這么做,請(qǐng)從 ?app.module.ts? 刪除所有 UpgradeModule 的引用, 以及所有用于 AngularJS 服務(wù)的工廠提供者(factory provider)和 ?app/ajs-upgraded-providers.ts? 文件。

還要?jiǎng)h除所有的 ?downgradeInjectable()? 或 ?downgradeComponent()? 以及與 AngularJS 相關(guān)的工廠或指令聲明。 因?yàn)槟悴辉傩枰导?jí)任何組件了,也不再需要把它們列在 ?entryComponents ?中。

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { CheckmarkPipe } from './core/checkmark/checkmark.pipe';
import { Phone } from './core/phone/phone.service';
import { PhoneDetailComponent } from './phone-detail/phone-detail.component';
import { PhoneListComponent } from './phone-list/phone-list.component';

@NgModule({
  imports: [
    BrowserModule,
    FormsModule,
    HttpClientModule,
    AppRoutingModule
  ],
  declarations: [
    AppComponent,
    PhoneListComponent,
    CheckmarkPipe,
    PhoneDetailComponent
  ],
  providers: [
    Phone
  ],
  bootstrap: [ AppComponent ]
})
export class AppModule {}

你還要完全移除了下列文件。它們是 AngularJS 的模塊配置文件和類型定義文件,在 Angular 中不需要了:

  • ?app/app.module.ajs.ts ?
  • ?app/app.config.ts ?
  • ?app/core/core.module.ts ?
  • ?app/core/phone/phone.module.ts ?
  • ?app/phone-detail/phone-detail.module.ts ?
  • ?app/phone-list/phone-list.module.ts ?

還需要卸載 AngularJS 的外部類型定義文件。你現(xiàn)在只需要留下 Jasmine 和 Angular 所需的膩?zhàn)幽_本。 ?systemjs.config.js? 中的 ?@angular/upgrade? 包及其映射也可以移除了。

npm uninstall @angular/upgrade --save
npm uninstall @types/angular @types/angular-animate @types/angular-cookies @types/angular-mocks @types/angular-resource @types/angular-route @types/angular-sanitize --save-dev

最后,從 ?index.html? 和 ?karma.conf.js? 中,移除所有對(duì) AngularJS 和 jQuery 腳本的引用。 當(dāng)這些全部做完時(shí),?index.html? 應(yīng)該是這樣的:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <base href="/app/">
    <title>Google Phone Gallery</title>
    <link rel="stylesheet"  rel="external nofollow" target="_blank"  rel="external nofollow" target="_blank"  />
    <link rel="stylesheet" href="app.css" />

    <script src="/node_modules/core-js/client/shim.min.js"></script>
    <script src="/node_modules/zone.js/bundles/zone.umd.js"></script>
    <script src="/node_modules/systemjs/dist/system.src.js"></script>
    <script src="/systemjs.config.js"></script>
    <script>
      System.import('/app');
    </script>
  </head>
  <body>
    <phonecat-app></phonecat-app>
  </body>
</html>

這是你最后一次看到 AngularJS 了!它曾經(jīng)帶給你很多幫助,不過現(xiàn)在,該說再見了。

附錄:升級(jí) PhoneCat 的測試

測試不僅要在升級(jí)過程中被保留,它還是確保應(yīng)用在升級(jí)過程中不會(huì)被破壞的一個(gè)安全指示器。 要達(dá)到這個(gè)目的,E2E 測試尤其有用。

E2E 測試

PhoneCat 項(xiàng)目中同時(shí)有基于 Protractor 的 E2E 測試和一些基于 Karma 的單元測試。 對(duì)這兩者來說,E2E 測試的轉(zhuǎn)換要容易得多:根據(jù)定義,E2E 測試通過與應(yīng)用中顯示的這些 UI 元素互動(dòng),從外部訪問你的應(yīng)用來進(jìn)行測試。 E2E 測試實(shí)際上并不關(guān)心這些應(yīng)用中各部件的內(nèi)部結(jié)構(gòu)。這也意味著,雖然你已經(jīng)修改了此應(yīng)用程序, 但是 E2E 測試套件仍然應(yīng)該能像以前一樣全部通過。因?yàn)閺挠脩舻慕嵌葋碚f,你并沒有改變應(yīng)用的行為。

在轉(zhuǎn)成 TypeScript 期間,你不用做什么就能讓 E2E 測試正常工作。 但是當(dāng)你想改成按照混合式應(yīng)用進(jìn)行引導(dǎo)時(shí),必須做一些修改。

再對(duì) ?protractor-conf.js? 做下列修改,與混合應(yīng)用同步:

ng12Hybrid: true

當(dāng)你開始組件和模塊升級(jí)到 Angular 時(shí),還需要一系列后續(xù)的修改。 這是因?yàn)?nbsp;E2E 測試有一些匹配器是 AngularJS 中特有的。對(duì)于 PhoneCat 來說,為了讓它能在 Angular 下工作,你得做下列修改:

老代碼

新代碼

備注

by.repeater('phone in $ctrl.phones').column('phone.name') by.css('.phones .name')

repeater 匹配器依賴于 AngularJS 中的 ng-repeat

by.repeater('phone in $ctrl.phones') by.css('.phones li')

repeater 匹配器依賴于 AngularJS 中的 ng-repeat

by.model('$ctrl.query') by.css('input')

模型匹配器依賴于 AngularJS ng-model

by.model('$ctrl.orderProp') by.css('select')

模型匹配器依賴于 AngularJS ng-model

by.binding('$ctrl.phone.name') by.css('h1')

binding 匹配器依賴于 AngularJS 的數(shù)據(jù)綁定

當(dāng)引導(dǎo)方式從 ?UpgradeModule ?切換到純 Angular 的時(shí),AngularJS 就從頁面中完全消失了。 此時(shí),你需要告訴 Protractor,它不用再找 AngularJS 應(yīng)用了,而是從頁面中查找 Angular 應(yīng)用。 于是在 ?protractor-conf.js? 中做下列修改:

替換之前在 ?protractor-conf.js? 中加入 ?ng12Hybrid?,像這樣:

useAllAngular2AppRoots: true,

同樣,?PhoneCat ?的測試代碼中有兩個(gè) Protractor API 調(diào)用內(nèi)部使用了 AngularJS 的 ?$location?。該服務(wù)沒有了, 你就得把這些調(diào)用用一個(gè) WebDriver 的通用 URL API 代替。第一個(gè) API 是“重定向(redirect)”規(guī)約:

it('should redirect `index.html` to `index.html#!/phones', async () => {
  await browser.get('index.html');
  await browser.waitForAngular();
  const url = await browser.getCurrentUrl();
  expect(url.endsWith('/phones')).toBe(true);
});

然后是“電話鏈接(phone links)”規(guī)約:

it('should render phone specific links', async () => {
  const query = element(by.css('input'));
  await query.sendKeys('nexus');
  await element.all(by.css('.phones li a')).first().click();
  const url = await browser.getCurrentUrl();
  expect(url.endsWith('/phones/nexus-s')).toBe(true);
});

單元測試

另一方面,對(duì)于單元測試來說,需要更多的轉(zhuǎn)化工作。實(shí)際上,它們需要隨著產(chǎn)品代碼一起升級(jí)。

在轉(zhuǎn)成 TypeScript 期間,嚴(yán)格來講沒有什么改動(dòng)是必須的。但把單元測試代碼轉(zhuǎn)成 TypeScript 仍然是個(gè)好主意,

比如,在這個(gè)電話詳情組件的規(guī)約中,你不僅用到了 ES2015 中的箭頭函數(shù)和塊作用域變量這些特性,還為所用的一些 AngularJS 服務(wù)提供了類型定義。

describe('phoneDetail', () => {

  // Load the module that contains the `phoneDetail` component before each test
  beforeEach(angular.mock.module('phoneDetail'));

  // Test the controller
  describe('PhoneDetailController', () => {
    let $httpBackend: angular.IHttpBackendService;
    let ctrl: any;
    const xyzPhoneData = {
      name: 'phone xyz',
      images: ['image/url1.png', 'image/url2.png']
    };

    beforeEach(inject(($componentController: any,
                       _$httpBackend_: angular.IHttpBackendService,
                       $routeParams: angular.route.IRouteParamsService) => {
      $httpBackend = _$httpBackend_;
      $httpBackend.expectGET('phones/xyz.json').respond(xyzPhoneData);

      $routeParams.phoneId = 'xyz';

      ctrl = $componentController('phoneDetail');
    }));

    it('should fetch the phone details', () => {
      jasmine.addCustomEqualityTester(angular.equals);

      expect(ctrl.phone).toEqual({});

      $httpBackend.flush();
      expect(ctrl.phone).toEqual(xyzPhoneData);
    });

  });

});

一旦你開始了升級(jí)過程并引入了 SystemJS,還需要對(duì) Karma 進(jìn)行配置修改。 你需要讓 SystemJS 加載所有的 Angular 新代碼。

// /*global jasmine, __karma__, window*/
Error.stackTraceLimit = 0; // "No stacktrace"" is usually best for app testing.

// Uncomment to get full stacktrace output. Sometimes helpful, usually not.
// Error.stackTraceLimit = Infinity; //

jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000;

var builtPath = '/base/app/';

__karma__.loaded = function () { };

function isJsFile(path) {
  return path.slice(-3) == '.js';
}

function isSpecFile(path) {
  return /\.spec\.(.*\.)?js$/.test(path);
}

function isBuiltFile(path) {
  return isJsFile(path) && (path.substr(0, builtPath.length) == builtPath);
}

var allSpecFiles = Object.keys(window.__karma__.files)
  .filter(isSpecFile)
  .filter(isBuiltFile);

System.config({
  baseURL: '/base',
  // Extend usual application package list with test folder
  packages: { 'testing': { main: 'index.js', defaultExtension: 'js' } },

  // Assume npm: is set in `paths` in systemjs.config
  // Map the angular testing bundles
  map: {
    '@angular/core/testing': 'npm:@angular/core/fesm2015/testing.mjs',
    '@angular/common/testing': 'npm:@angular/common/fesm2015/testing.mjs',
    '@angular/common/http/testing': 'npm:@angular/common/fesm2015/http/testing.mjs',
    '@angular/compiler/testing': 'npm:@angular/compiler/fesm2015/testing.mjs',
    '@angular/platform-browser/testing': 'npm:@angular/platform-browser/fesm2015/testing.mjs',
    '@angular/platform-browser-dynamic/testing': 'npm:@angular/platform-browser-dynamic/fesm2015/testing.mjs',
    '@angular/router/testing': 'npm:@angular/router/fesm2015/testing.mjs',
    '@angular/forms/testing': 'npm:@angular/forms/fesm2015/testing.mjs',
  },
});

System.import('systemjs.config.js')
  .then(importSystemJsExtras)
  .then(initTestBed)
  .then(initTesting);

/** Optional SystemJS configuration extras. Keep going w/o it */
function importSystemJsExtras(){
  return System.import('systemjs.config.extras.js')
  .catch(function(reason) {
    console.log(
      'Warning: System.import could not load the optional "systemjs.config.extras.js". Did you omit it by accident? Continuing without it.'
    );
    console.log(reason);
  });
}

function initTestBed() {
  return Promise.all([
    System.import('@angular/core/testing'),
    System.import('@angular/platform-browser-dynamic/testing')
  ])

  .then(function (providers) {
    var coreTesting    = providers[0];
    var browserTesting = providers[1];

    coreTesting.TestBed.initTestEnvironment(
      browserTesting.BrowserDynamicTestingModule,
      browserTesting.platformBrowserDynamicTesting());
  })
}

// Import all spec files and start karma
function initTesting() {
  return Promise.all(
    allSpecFiles.map(function (moduleName) {
      return System.import(moduleName);
    })
  )
  .then(__karma__.start, __karma__.error);
}

這個(gè) shim 文件首先加載了 SystemJS 的配置,然后是 Angular 的測試支持庫,然后是應(yīng)用本身的規(guī)約文件。

然后需要修改 Karma 配置,來讓它使用本應(yīng)用的根目錄作為基礎(chǔ)目錄(base directory),而不是 ?app?。

basePath: './',

一旦這些完成了,你就能加載 SystemJS 和其它依賴,并切換配置文件來加載那些應(yīng)用文件,而不用在 Karma 頁面中包含它們。 你要讓這個(gè) shim 文件和 SystemJS 去加載它們。

// System.js for module loading
'node_modules/systemjs/dist/system.src.js',

// Polyfills
'node_modules/core-js/client/shim.js',

// zone.js
'node_modules/zone.js/bundles/zone.umd.js',
'node_modules/zone.js/bundles/zone-testing.umd.js',

// RxJs.
{ pattern: 'node_modules/rxjs/**/*.js', included: false, watched: false },
{ pattern: 'node_modules/rxjs/**/*.js.map', included: false, watched: false },

// Angular itself and the testing library
{ pattern: 'node_modules/@angular/**/*.mjs', included: false, watched: false },
{ pattern: 'node_modules/@angular/**/*.mjs.map', included: false, watched: false },

{ pattern: 'node_modules/tslib/tslib.js', included: false, watched: false },
{ pattern: 'node_modules/systemjs-plugin-babel/**/*.js', included: false, watched: false },

{pattern: 'systemjs.config.js', included: false, watched: false},
'karma-test-shim.js',

{pattern: 'app/**/*.module.js', included: false, watched: true},
{pattern: 'app/*!(.module|.spec).js', included: false, watched: true},
{pattern: 'app/!(bower_components)/**/*!(.module|.spec).js', included: false, watched: true},
{pattern: 'app/**/*.spec.js', included: false, watched: true},

{pattern: '**/*.html', included: false, watched: true},

由于 Angular 組件中的 HTML 模板也同樣要被加載,所以你得幫 Karma 一把,幫它在正確的路徑下找到這些模板:

// proxied base paths for loading assets
proxies: {
  // required for component assets fetched by Angular's compiler
  '/phone-detail': '/base/app/phone-detail',
  '/phone-list': '/base/app/phone-list'
},

如果產(chǎn)品代碼被切換到了 Angular,單元測試文件本身也需要切換過來。對(duì)勾(checkmark)管道的規(guī)約可能是最直觀的,因?yàn)樗鼪]有任何依賴:

import { CheckmarkPipe } from './checkmark.pipe';

describe('CheckmarkPipe', () => {

  it('should convert boolean values to unicode checkmark or cross', () => {
    const checkmarkPipe = new CheckmarkPipe();
    expect(checkmarkPipe.transform(true)).toBe('\u2713');
    expect(checkmarkPipe.transform(false)).toBe('\u2718');
  });
});

?Phone ?服務(wù)的測試會(huì)牽扯到一點(diǎn)別的。你需要把模擬版的 AngularJS ?$httpBackend? 服務(wù)切換到模擬板的 Angular Http 后端。

import { inject, TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { Phone, PhoneData } from './phone.service';

describe('Phone', () => {
  let phone: Phone;
  const phonesData: PhoneData[] = [
    {name: 'Phone X', snippet: '', images: []},
    {name: 'Phone Y', snippet: '', images: []},
    {name: 'Phone Z', snippet: '', images: []}
  ];
  let httpMock: HttpTestingController;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [
        HttpClientTestingModule
      ],
      providers: [
        Phone,
      ]
    });
  });

  beforeEach(inject([HttpTestingController, Phone], (_httpMock_: HttpTestingController, _phone_: Phone) => {
    httpMock = _httpMock_;
    phone = _phone_;
  }));

  afterEach(() => {
    httpMock.verify();
  });

  it('should fetch the phones data from `/phones/phones.json`', () => {
    phone.query().subscribe(result => {
      expect(result).toEqual(phonesData);
    });
    const req = httpMock.expectOne(`/phones/phones.json`);
    req.flush(phonesData);
  });

});

對(duì)于組件的規(guī)約,你可以模擬出 ?Phone ?服務(wù)本身,并且讓它提供電話的數(shù)據(jù)。你可以對(duì)這些組件使用 Angular 的組件單元測試 API。

import { TestBed, waitForAsync } from '@angular/core/testing';
import { ActivatedRoute } from '@angular/router';
import { Observable, of } from 'rxjs';

import { PhoneDetailComponent } from './phone-detail.component';
import { Phone, PhoneData } from '../core/phone/phone.service';
import { CheckmarkPipe } from '../core/checkmark/checkmark.pipe';

function xyzPhoneData(): PhoneData {
  return {name: 'phone xyz', snippet: '', images: ['image/url1.png', 'image/url2.png']};
}

class MockPhone {
  get(id: string): Observable<PhoneData> {
    return of(xyzPhoneData());
  }
}


class ActivatedRouteMock {
  constructor(public snapshot: any) {}
}


describe('PhoneDetailComponent', () => {

  beforeEach(waitForAsync(() => {
    TestBed.configureTestingModule({
      declarations: [ CheckmarkPipe, PhoneDetailComponent ],
      providers: [
        { provide: Phone, useClass: MockPhone },
        { provide: ActivatedRoute, useValue: new ActivatedRouteMock({ params: { phoneId: 1 } }) }
      ]
    })
    .compileComponents();
  }));

  it('should fetch phone detail', () => {
    const fixture = TestBed.createComponent(PhoneDetailComponent);
    fixture.detectChanges();
    const compiled = fixture.debugElement.nativeElement;
    expect(compiled.querySelector('h1').textContent).toContain(xyzPhoneData().name);
  });
});
import {SpyLocation} from '@angular/common/testing';
import {NO_ERRORS_SCHEMA} from '@angular/core';
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
import {ActivatedRoute} from '@angular/router';
import {Observable, of} from 'rxjs';

import {Phone, PhoneData} from '../core/phone/phone.service';

import {PhoneListComponent} from './phone-list.component';

class ActivatedRouteMock {
  constructor(public snapshot: any) {}
}

class MockPhone {
  query(): Observable<PhoneData[]> {
    return of([
      {name: 'Nexus S', snippet: '', images: []}, {name: 'Motorola DROID', snippet: '', images: []}
    ]);
  }
}

let fixture: ComponentFixture<PhoneListComponent>;

describe('PhoneList', () => {
  beforeEach(waitForAsync(() => {
    TestBed
        .configureTestingModule({
          declarations: [PhoneListComponent],
          providers: [
            {provide: ActivatedRoute, useValue: new ActivatedRouteMock({params: {'phoneId': 1}})},
            {provide: Location, useClass: SpyLocation},
            {provide: Phone, useClass: MockPhone},
          ],
          schemas: [NO_ERRORS_SCHEMA]
        })
        .compileComponents();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(PhoneListComponent);
  });

  it('should create "phones" model with 2 phones fetched from xhr', () => {
    fixture.detectChanges();
    let compiled = fixture.debugElement.nativeElement;
    expect(compiled.querySelectorAll('.phone-list-item').length).toBe(2);
    expect(compiled.querySelector('.phone-list-item:nth-child(1)').textContent)
        .toContain('Motorola DROID');
    expect(compiled.querySelector('.phone-list-item:nth-child(2)').textContent)
        .toContain('Nexus S');
  });

  xit('should set the default value of orderProp model', () => {
    fixture.detectChanges();
    let compiled = fixture.debugElement.nativeElement;
    expect(compiled.querySelector('select option:last-child').selected).toBe(true);
  });
});

最后,當(dāng)你切換到 Angular 路由時(shí),需要重新過一遍這些組件測試。對(duì)詳情組件來說,你需要提供一個(gè) Angular ?RouteParams ?的 mock 對(duì)象,而不再用 AngularJS 中的 ?$routeParams?。

import { TestBed, waitForAsync } from '@angular/core/testing';
import { ActivatedRoute } from '@angular/router';
/* . . . */

class ActivatedRouteMock {
  constructor(public snapshot: any) {}
}

  /* . . . */

  beforeEach(waitForAsync(() => {
    TestBed.configureTestingModule({
      declarations: [ CheckmarkPipe, PhoneDetailComponent ],
      providers: [
        { provide: Phone, useClass: MockPhone },
        { provide: ActivatedRoute, useValue: new ActivatedRouteMock({ params: { phoneId: 1 } }) }
      ]
    })
    .compileComponents();
  }));

對(duì)于電話列表組件,還要再做少量的調(diào)整,以便路由器能讓 ?RouteLink ?指令正常工作。

import {SpyLocation} from '@angular/common/testing';
import {NO_ERRORS_SCHEMA} from '@angular/core';
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
import {ActivatedRoute} from '@angular/router';
import {Observable, of} from 'rxjs';

import {Phone, PhoneData} from '../core/phone/phone.service';

import {PhoneListComponent} from './phone-list.component';

  /* . . . */

  beforeEach(waitForAsync(() => {
    TestBed
        .configureTestingModule({
          declarations: [PhoneListComponent],
          providers: [
            {provide: ActivatedRoute, useValue: new ActivatedRouteMock({params: {'phoneId': 1}})},
            {provide: Location, useClass: SpyLocation},
            {provide: Phone, useClass: MockPhone},
          ],
          schemas: [NO_ERRORS_SCHEMA]
        })
        .compileComponents();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(PhoneListComponent);
  });


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)