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í)。
AngularJS 應(yīng)用程序的組織方式有很多種。當(dāng)你想把它們升級(jí)到 Angular 的時(shí)候, 有些做起來會(huì)比其它的更容易些。即使在開始升級(jí)之前,也有一些關(guān)鍵的技術(shù)和模式可以讓你將來升級(jí)時(shí)更輕松。
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í)變得更簡單:
如果應(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、 Webpack或Browserify, 可以讓你在程序中使用 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)品包變得容易一些。
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)然,如果僅僅這樣做也沒什么大用,也沒什么有意思的地方。 下面這些額外的步驟可以讓你打起精神:
let
?、?const
?、默認(rèn)函數(shù)參數(shù)、解構(gòu)賦值等也可以逐漸添加進(jìn)來,讓代碼更有表現(xià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>
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ì)更容易。
不管要升級(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
?提供的主要工具之一被稱為 ?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è)注入器。
|
這是一個(gè)樹狀分層注入器:有一個(gè)根注入器,而且每個(gè)組件也有一個(gè)自己的注入器。 |
就算有這么多不同點(diǎn),也并不妨礙你在依賴注入時(shí)進(jìn)行互操作。?UpgradeModule
?解決了這些差異,并讓它們無縫的對(duì)接:
在混合式應(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è)框架之間自由穿梭:
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ā)生了什么呢?
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(或控制器)上。
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)用,就必須在應(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 代碼了。
一旦你開始運(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>
現(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'
|
用作雙向綁定: |
舉個(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 模板中使用降級(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 框架管理著。
就像可以把 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');
}
當(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í) —— 盡管這不是必須的。
除了能升級(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];
}]
};
在構(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)該如何去做:
matcher
?函數(shù),并為 AngularJS 的各個(gè)路由配上帶有自定義匹配器的 Angular 路由器。在 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 將處理所有其它路由。
在 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)用中被銷毀。
為了配置 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 框架。
在 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)航。
在本節(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)備步驟。
core
?、?phone-detail
? 和 ?phone-list
? 模塊都在它們自己的子目錄中。那些子目錄除了包含 HTML 模板之外,還包含 JavaScript 代碼,它們共同完成一個(gè)特性。 這是按特性分目錄的結(jié)構(gòu) 和模塊化規(guī)則所要求的。因?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ù)。
準(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)備工作中的指南,并從那里獲得如下配置:
package.json
? 中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',
/* . . . */
},
現(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 {
}
接下來,你把該應(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)生成包。
npm
?全局安裝 ?rollup
?npm i -g rollup
npm i -g 匯總
rollup
?的版本并驗(yàn)證安裝是否成功rollup -v
rollup
?創(chuàng)建 ?rollup.config.js
? 配置文件,以使用全局 ?ng
?命令來引用所有 Angular 框架的導(dǎo)出。export default {
input: 'node_modules/@angular/core/fesm2015/core.js',
output: {
file: 'bundle.js',
format: 'umd',
name: 'ng'
}
}
rollup
?根據(jù) ?rollup.config.js
? 中的設(shè)置創(chuàng)建 ?bundle.js
? UMD 包rollup -c rollup.config.js
?bundle.js
? 文件包含你的 UMD 包。有關(guān) GitHub 上的示例,請(qǐng)參閱 UMD Angular 包。
你要移植到 Angular 的第一個(gè)片段是 ?Phone
?工廠(位于 ?app/js/core/phones.factory.ts
?), 并且讓它能幫助控制器從服務(wù)器上加載電話信息。目前,它是用 ?ngResource
?實(shí)現(xiàn)的,你用它做兩件事:
你可以用 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)一步減小組件控制器中需要修改的代碼量。
接下來,把 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 中并不存在 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):
$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
?事件的綁定。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>。
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']);
}
}
要在混合式應(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 編譯所需的一切!
此刻,你已經(jīng)把所有 AngularJS 的組件替換成了它們在 Angular 中的等價(jià)物,不過你仍然在 AngularJS 路由器中使用它們。
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>
無論在 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è)電話的 ?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>
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)用了!
終于可以把輔助訓(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í)過程中被保留,它還是確保應(yīng)用在升級(jí)過程中不會(huì)被破壞的一個(gè)安全指示器。 要達(dá)到這個(gè)目的,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 中的 |
by.repeater('phone in $ctrl.phones')
|
by.css('.phones li')
|
repeater 匹配器依賴于 AngularJS 中的 |
by.model('$ctrl.query')
|
by.css('input')
|
模型匹配器依賴于 AngularJS |
by.model('$ctrl.orderProp')
|
by.css('select')
|
模型匹配器依賴于 AngularJS |
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);
});
更多建議: