填寫這份《一分鐘調查》,幫我們(開發組)做得更好!去填寫Home

從 AngularJS 升級到 Angular

Upgrading from AngularJS to Angular

Angular 這個名字專指現在和未來的 Angular 版本,而 AngularJS 專指 Angular 的所有 1.x 版本。

Angular is the name for the Angular of today and tomorrow.
AngularJS is the name for all 1.x versions of Angular.

有很多大型 AngularJS 應用。 在決定遷移到 Angular 之前,首先要深入思考業務案例。 在這些案例中,最重要的部分之一是時間和需要付出的努力。 本章描述用於把 AngularJS 應用高效遷移到 Angular 平臺的內建工具,每次講一點點。

AngularJS apps are great. Always consider the business case before moving to Angular. An important part of that case is the time and effort to get there. This guide describes the built-in tools for efficiently migrating AngularJS projects over to the Angular platform, a piece at a time.

有些應用可能比其它的升級起來簡單,還有一些方法能讓把這項工作變得更簡單。 即使在正式開始升級過程之前,可以提前準備 AngularJS 的程式,讓它向 Angular 看齊。 這些準備步驟幾乎都是關於如何讓程式碼更加鬆耦合、更有可維護性,以及用現代開發工具提高速度的。 這意味著,這種準備工作不僅能讓最終的升級變得更簡單,而且還能提升 AngularJS 程式的品質。

Some applications will be easier to upgrade than others, and there are many ways to make it easier for yourself. It is possible to prepare and align AngularJS applications with Angular even before beginning the upgrade process. These preparation steps are all about making the code more decoupled, more maintainable, and better aligned with modern development tools. That means in addition to making the upgrade easier, you will also improve the existing AngularJS applications.

成功升級的關鍵之一是增量式的實現它,透過在同一個應用中一起執行這兩個框架,並且逐個把 AngularJS 的元件遷移到 Angular 中。 這意味著可以在不必打斷其它業務的前提下,升級更大、更復雜的應用程式,因為這項工作可以多人協作完成,在一段時間內逐漸鋪開。 Angular upgrade 模組的設計目標就是讓你漸進、無縫的完成升級。

One of the keys to a successful upgrade is to do it incrementally, by running the two frameworks side by side in the same application, and porting AngularJS components to Angular one by one. This makes it possible to upgrade even large and complex applications without disrupting other business, because the work can be done collaboratively and spread over a period of time. The upgrade module in Angular has been designed to make incremental upgrading seamless.

準備工作

Preparation

AngularJS 應用程式的組織方式有很多種。當你想把它們升級到 Angular 的時候, 有些做起來會比其它的更容易些。即使在開始升級之前,也有一些關鍵的技術和模式可以讓你將來升級時更輕鬆。

There are many ways to structure AngularJS applications. When you begin to upgrade these applications to Angular, some will turn out to be much more easy to work with than others. There are a few key techniques and patterns that you can apply to future proof apps even before you begin the migration.

遵循 AngularJS 風格指南

Follow the AngularJS Style Guide

AngularJS 風格指南收集了一些已證明能寫出乾淨且可維護的 AngularJS 程式的模式與實踐。 它包含了很多關於如何書寫和組織 AngularJS 程式碼的有價值資訊,同樣重要的是,不應該採用的書寫和組織 AngularJS 程式碼的方式。

The AngularJS Style Guide collects patterns and practices that have been proven to result in cleaner and more maintainable AngularJS applications. It contains a wealth of information about how to write and organize AngularJS code - and equally importantly - how not to write and organize AngularJS code.

Angular 是一個基於 AngularJS 中最好的部分構思出來的版本。在這種意義上,它的目標和 AngularJS 風格指南是一樣的: 保留 AngularJS 中好的部分,去掉壞的部分。當然,Angular 還做了更多。 說這些的意思是:遵循這個風格指南可以讓你寫出更接近 Angular 程式的 AngularJS 程式

Angular is a reimagined version of the best parts of AngularJS. In that sense, its goals are the same as the AngularJS Style Guide's: To preserve the good parts of AngularJS, and to avoid the bad parts. There's a lot more to Angular than just that of course, but this does mean that following the style guide helps make your AngularJS app more closely aligned with Angular.

有一些特別的規則可以讓使用 Angular 的 upgrade/static 模組進行增量升級變得更簡單:

There are a few rules in particular that will make it much easier to do an incremental upgrade using the Angular upgrade/static module:

  • 單一規則 規定每個檔案應該只放一個元件。這不僅讓元件更容易瀏覽和查詢,而且還讓你能逐個遷移它們的語言和框架。 在這個範例程式中,每個控制器、工廠和過濾器都位於各自的原始檔中。

    The Rule of 1 states that there should be one component per file. This not only makes components easy to navigate and find, but will also allow us to migrate them between languages and frameworks one at a time. In this example application, each controller, component, service, and filter is in its own source file.

  • 按特性分目錄的結構模組化規則在較高的抽象層定義了一些相似的原則:應用程式中的不同部分應該被分到不同的目錄和 NgModule 中。

    The Folders-by-Feature Structure and Modularity rules define similar principles on a higher level of abstraction: Different parts of the application should reside in different directories and NgModules.

如果應用程式能用這種方式把每個特性分到一個獨立目錄中,它也就能每次遷移一個特性。 對於那些還沒有這麼做的程式,強烈建議把應用這條規則作為準備步驟。而且這也不僅僅對升級有價值, 它還是一個通用的規則,可以讓你的程式更“堅實”。

When an application is laid out feature per feature in this way, it can also be migrated one feature at a time. For applications that don't already look like this, applying the rules in the AngularJS style guide is a highly recommended preparation step. And this is not just for the sake of the upgrade - it is just solid advice in general!

使用模組載入器

Using a Module Loader

當你把應用程式碼分解到每個檔案中只放一個元件的粒度後,通常會得到一個由大量相對較小的檔案組成的專案結構。 這比組織成少量大檔案要整潔得多,但如果你不得不透過 <script> 標籤在 HTML 頁面中載入所有這些檔案,那就不好玩了。 尤其是當你不得不自己按正確的順序維護這些標籤時更是如此,就要開始使用模組載入器了。

When you break application code down into one component per file, you often end up with a project structure with a large number of relatively small files. This is a much neater way to organize things than a small number of large files, but it doesn't work that well if you have to load all those files to the HTML page with <script> tags. Especially when you also have to maintain those tags in the correct order. That's why it's a good idea to start using a module loader.

使用模組載入器,比如SystemJSWebpackBrowserify, 可以讓你在程式中使用 TypeScript 或 ES2015 語言內建的模組系統。 你可以使用 importexport 特性來明確指定哪些程式碼應該以及將會被在程式的不同部分之間共享。 對於 ES5 程式來說,可以改用 CommonJS 風格的 requiremodule.exports 特性代替。 無是論哪種情況,模組載入器都會按正確的順序載入程式中用到的所有程式碼。

Using a module loader such as SystemJS, Webpack, or Browserify allows us to use the built-in module systems of TypeScript or ES2015. You can use the import and export features that explicitly specify what code can and will be shared between different parts of the application. For ES5 applications you can use CommonJS style require and module.exports features. In both cases, the module loader will then take care of loading all the code the application needs in the correct order.

當要把應用程式投入生產環境時,模組載入器也會讓你把所有這些檔案打成完整的產品包變得容易一些。

When moving applications into production, module loaders also make it easier to package them all up into production bundles with batteries included.

遷移到 TypeScript

Migrating to TypeScript

Angular 升級計劃的一部分是引入 TypeScript,即使在開始升級之前,引入 TypeScript 編譯器也是有意義的。 這意味著等真正升級的時候需要學習和思考的東西會更少,並且你可以在 AngularJS 程式碼中開始使用 TypeScript 的特性。

If part of the Angular upgrade plan is to also take TypeScript into use, it makes sense to bring in the TypeScript compiler even before the upgrade itself begins. This means there's one less thing to learn and think about during the actual upgrade. It also means you can start using TypeScript features in your AngularJS code.

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

Since TypeScript is a superset of ECMAScript 2015, which in turn is a superset of ECMAScript 5, "switching" to TypeScript doesn't necessarily require anything more than installing the TypeScript compiler and renaming files from *.js to *.ts. But just doing that is not hugely useful or exciting, of course. Additional steps like the following can give us much more bang for the buck:

  • 對那些使用了模組載入器的程式,TypeScript 的匯入和匯出語法(實際上是 ECMAScript 2015 的匯入和匯出)可以把程式碼組織成模組。

    For applications that use a module loader, TypeScript imports and exports (which are really ECMAScript 2015 imports and exports) can be used to organize code into modules.

  • 可以逐步把型別註解新增到現有函式和變數上,以固定它們的型別,並獲得其優點:比如編譯期錯誤檢查、更好的支援自動完成,以及內聯式文件等。

    Type annotations can be gradually added to existing functions and variables to pin down their types and get benefits like build-time error checking, great autocompletion support and inline documentation.

  • 那些 ES2015 中新增的特性,比如箭頭函式、letconst、預設函式引數、解構賦值等也可以逐漸新增進來,讓程式碼更有表現力。

    JavaScript features new to ES2015, like arrow functions, lets and consts, default function parameters, and destructuring assignments can also be gradually added to make the code more expressive.

  • 服務和控制器可以轉成類別。這樣它們就能一步步接近 Angular 的服務和元件類別了,也會讓升級變得簡單一點。

    Services and controllers can be turned into classes. That way they'll be a step closer to becoming Angular service and component classes, which will make life easier after the upgrade.

使用元件型指令

Using Component Directives

在 Angular 中,元件是用來建構使用者介面的主要元素。你把 UI 中的不同部分定義成元件,然後在範本中使用這些元件合成出最終的 UI。

In Angular, components are the main primitive from which user interfaces are built. You define the different portions of the UI as components and compose them into a full user experience.

你在 AngularJS 中也能這麼做。那就是一種定義了自己的範本、控制器和輸入/輸出繫結的指令 —— 跟 Angular 中對元件的定義是一樣的。 要遷移到 Angular,透過元件型指令建構的應用程式會比直接用 ng-controllerng-include 和作用域繼承等底層特性建構的要容易得多。

You can also do this in AngularJS, using component directives. These are directives that define their own templates, controllers, and input/output bindings - the same things that Angular components define. Applications built with component directives are much easier to migrate to Angular than applications built with lower-level features like ng-controller, ng-include, and scope inheritance.

要與 Angular 相容,AngularJS 的元件型指令應該配置下列屬性:

To be Angular compatible, an AngularJS component directive should configure these attributes:

  • restrict: 'E'。元件通常會以元素的方式使用。

    restrict: 'E'. Components are usually used as elements.

  • scope: {} - 一個獨立作用域。在 Angular 中,元件永遠是從它們的環境中被隔離出來的,在 AngularJS 中也同樣如此。

    scope: {} - an isolate scope. In Angular, components are always isolated from their surroundings, and you should do this in AngularJS too.

  • bindToController: {}。元件的輸入和輸出應該繫結到控制器,而不是 $scope

    bindToController: {}. Component inputs and outputs should be bound to the controller instead of using the $scope.

  • controllercontrollerAs。元件要有自己的控制器。

    controller and controllerAs. Components have their own controllers.

  • templatetemplateUrl。元件要有自己的範本。

    template or templateUrl. Components have their own templates.

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

Component directives may also use the following attributes:

  • transclude: true:如果元件需要從其它地方透傳內容,就設定它。

    transclude: true/{}, if the component needs to transclude content from elsewhere.

  • require:如果元件需要和父元件的控制器通訊,就設定它。

    require, if the component needs to communicate with some parent component's controller.

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

Component directives should not use the following attributes:

  • compile。Angular 不再支援它。

    compile. This will not be supported in Angular.

  • replace: true。Angular 永遠不會用元件範本替換一個元件元素。這個特性在 AngularJS 中也同樣不建議使用了。

    replace: true. Angular never replaces a component element with the component template. This attribute is also deprecated in AngularJS.

  • priorityterminal。雖然 AngularJS 的元件可能使用這些,但它們在 Angular 中已經沒用了,並且最好不要再寫依賴它們的程式碼。

    priority and terminal. While AngularJS components may use these, they are not used in Angular and it is better not to write code that relies on them.

AngularJS 中一個完全向 Angular 架構對齊過的元件型指令是這樣的:

An AngularJS component directive that is fully aligned with the Angular architecture may look something like this:

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' }; }
hero-detail.directive.ts
      
      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,它讓定義指令變得更簡單了。 為元件型指令使用這個 API 是一個好主意,因為:

AngularJS 1.5 introduces the component API that makes it easier to define component directives like these. It is a good idea to use this API for component directives for several reasons:

  • 它需要更少的樣板程式碼。

    It requires less boilerplate code.

  • 它強制你遵循元件的最佳實踐,比如 controllerAs

    It enforces the use of component best practices like controllerAs.

  • 指令中像 scoperestrict 這樣的屬性應該有良好的預設值。

    It has good default values for directive attributes like scope and restrict.

如果使用這個元件 API 進行表示,那麼上面看到的元件型指令就變成了這樣:

The component directive example from above looks like this when expressed using the component API:

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); }; } };
hero-detail.component.ts
      
      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 中的等價物,所以,圍繞它們組織元件生命週期的邏輯在升級到 Angular 時會更容易。

Controller lifecycle hook methods $onInit(), $onDestroy(), and $onChanges() are another convenient feature that AngularJS 1.5 introduces. They all have nearly exact equivalents in Angular, so organizing component lifecycle logic around them will ease the eventual Angular upgrade process.

使用升級介面卡進行升級

Upgrading with ngUpgrade

不管要升級什麼,Angular 中的 ngUpgrade 函式庫都會是一個非常有用的工具 —— 除非是小到沒功能的應用。 藉助它,你可以在同一個應用程式中混用並匹配 AngularJS 和 Angular 的元件,並讓它們實現無縫的互操作。 這意味著你不用被迫一次性做完所有的升級工作,因為在整個演進過程中,這兩個框架可以很自然的和睦相處。

The ngUpgrade library in Angular is a very useful tool for upgrading anything but the smallest of applications. With it you can mix and match AngularJS and Angular components in the same application and have them interoperate seamlessly. That means you don't have to do the upgrade work all at once, since there's a natural coexistence between the two frameworks during the transition period.

升級模組工作原理

How ngUpgrade Works

ngUpgrade 提供的主要工具之一被稱為 UpgradeModule。這是一個服務,它可以啟動並管理一個能同時支援 Angular 和 AngularJS 的混合式應用。

One of the primary tools provided by ngUpgrade is called the UpgradeModule. This is a module that contains utilities for bootstrapping and managing hybrid applications that support both Angular and AngularJS code.

當使用 ngUpgrade 時,你實際上在同時執行 AngularJS 和 Angular。所有 Angular 的程式碼執行在 Angular 框架中,而 AngularJS 的程式碼執行在 AngularJS 框架中。所有這些都是真實的、全功能的框架版本。 沒有進行任何模擬,所以你可以認為同時存在著這兩個框架的所有特性和自然行為。

When you use ngUpgrade, what you're really doing is running both AngularJS and Angular at the same time. All Angular code is running in the Angular framework, and AngularJS code in the AngularJS framework. Both of these are the actual, fully featured versions of the frameworks. There is no emulation going on, so you can expect to have all the features and natural behavior of both frameworks.

所有這些事情的背後,本質上是一個框架中管理的元件和服務能和來自另一個框架的進行互操作。 這些主要體現在三個方面:依賴注入、DOM 和變更檢測。

What happens on top of this is that components and services managed by one framework can interoperate with those from the other framework. This happens in three main areas: Dependency injection, the DOM, and change detection.

依賴注入

Dependency Injection

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

Dependency injection is front and center in both AngularJS and Angular, but there are some key differences between the two frameworks in how it actually works.

AngularJS

Angular

依賴注入的令牌(Token)永遠是字串(譯註:指服務名稱)。

Dependency injection tokens are always strings

令牌可能有不同的型別。 通常是類別,也可能是字串。

Tokens can have different types. They are often classes. They may also be strings.

只有一個注入器。即使在多模組的應用程式中,每樣東西也都會被裝入一個巨大的名稱空間中。

There is exactly one injector. Even in multi-module applications, everything is poured into one big namespace.

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

There is a tree hierarchy of injectors, with a root injector and an additional injector for each component.

就算有這麼多不同點,也並不妨礙你在依賴注入時進行互操作。UpgradeModule 解決了這些差異,並讓它們無縫的對接:

Even accounting for these differences you can still have dependency injection interoperability. upgrade/static resolves the differences and makes everything work seamlessly:

  • 透過升級它們,你就能讓那些在 AngularJS 中能被注入的服務也可用於 Angular 的程式碼中。 在框架之間共享的是服務的同一個單例物件。在 Angular 中,這些外來服務總是被放在根注入器中,並可用於所有元件。 它們總是具有字串令牌 —— 跟它們在 AngularJS 中的令牌相同。

    You can make AngularJS services available for injection to Angular code by upgrading them. The same singleton instance of each service is shared between the frameworks. In Angular these services will always be in the root injector and available to all components.

  • 透過降級它們,你也能讓那些在 Angular 中能被注入的服務在 AngularJS 的程式碼中可用。 只有那些來自 Angular 根注入器的服務才能被降級。同樣的,在框架之間共享的是同一個單例物件。 當你註冊一個要降級的服務時,要明確指定一個打算在 AngularJS 中使用的字串令牌

    You can also make Angular services available for injection to AngularJS code by downgrading them. Only services from the Angular root injector can be downgraded. Again, the same singleton instances are shared between the frameworks. When you register a downgraded service, you must explicitly specify a string token that you want to use in AngularJS.

元件與 DOM

Components and the DOM

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

In the DOM of a hybrid ngUpgrade application are components and directives from both AngularJS and Angular. These components communicate with each other by using the input and output bindings of their respective frameworks, which ngUpgrade bridges together. They may also communicate through shared injected dependencies, as described above.

理解混合式應用的關鍵在於,DOM 中的每一個元素都只能屬於這兩個框架之一,而另一個框架則會忽略它。如果一個元素屬於 AngularJS,那麼 Angular 就會當它不存在,反之亦然。

The key thing to understand about a hybrid application is that every element in the DOM is owned by exactly one of the two frameworks. The other framework ignores it. If an element is owned by AngularJS, Angular treats it as if it didn't exist, and vice versa.

所以,混合式應用總是像 AngularJS 程式那樣啟動,處理根範本的也是 AngularJS. 然後,當這個應用的範本中使用到了 Angular 的元件時,Angular 才開始參與。 這個元件的檢視由 Angular 進行管理,而且它還可以使用一系列的 Angular 元件和指令。

So normally a hybrid application begins life as an AngularJS application, and it is AngularJS that processes the root template, e.g. the index.html. Angular then steps into the picture when an Angular component is used somewhere in an AngularJS template. That component's template will then be managed by Angular, and it may contain any number of Angular components and directives.

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

Beyond that, you may interleave the two frameworks. You always cross the boundary between the two frameworks by one of two ways:

  1. 透過使用來自另一個框架的元件:AngularJS 的範本中用到了 Angular 的元件,或者 Angular 的範本中使用了 AngularJS 的元件。

    By using a component from the other framework: An AngularJS template using an Angular component, or an Angular template using an AngularJS component.

  2. 透過透傳(transclude)或投影(project)來自另一個框架的內容。UpgradeModule 牽線搭橋,把 AngularJS 的透傳概念和 Angular 的內容投影概念關聯起來。

    By transcluding or projecting content from the other framework. ngUpgrade bridges the related concepts of AngularJS transclusion and Angular content projection together.

當你使用一個屬於另一個框架的元件時,就會發生一次跨框架邊界的切換。不過,這種切換只發生在該元件元素的子節點上。 考慮一個場景,你從 AngularJS 中使用一個 Angular 元件,就像這樣:

Whenever you use a component that belongs to the other framework, a switch between framework boundaries occurs. However, that switch only happens to the elements in the template of that component. Consider a situation where you use an Angular component from AngularJS like this:

<a-component></a-component>
      
      <a-component></a-component>
    

此時,<a-component> 這個 DOM 元素仍然由 AngularJS 管理,因為它是在 AngularJS 的範本中定義的。 這也意味著你可以往它上面新增別的 AngularJS 指令,卻不能新增 Angular 的指令。 只有在 <a-component> 元件的範本中才是 Angular 的天下。同樣的規則也適用於在 Angular 中使用 AngularJS 元件型指令的情況。

The DOM element <a-component> will remain to be an AngularJS managed element, because it's defined in an AngularJS template. That also means you can apply additional AngularJS directives to it, but not Angular directives. It is only in the template of the <a-component> where Angular steps in. This same rule also applies when you use AngularJS component directives from Angular.

變更檢測

Change Detection

AngularJS 中的變更檢測全是關於 scope.$apply() 的。在每個事件發生之後,scope.$apply() 就會被呼叫。 這或者由框架自動呼叫,或者在某些情況下由你自己的程式碼手動呼叫。

The scope.$apply() is how AngularJS detects changes and updates data bindings. After every event that occurs, scope.$apply() gets called. This is done either automatically by the framework, or manually by you.

在 Angular 中,事情有點不一樣。雖然變更檢測仍然會在每一個事件之後發生,卻不再需要每次呼叫 scope.$apply() 了。 這是因為所有 Angular 程式碼都執行在一個叫做Angular zone的地方。 Angular 總是知道什麼時候程式碼執行完了,也就知道了它什麼時候應該觸發變更檢測。程式碼本身並不需要呼叫 scope.$apply() 或其它類似的東西。

In Angular things are different. While change detection still occurs after every event, no one needs to call scope.$apply() for that to happen. This is because all Angular code runs inside something called the Angular zone. Angular always knows when the code finishes, so it also knows when it should kick off change detection. The code itself doesn't have to call scope.$apply() or anything like it.

在這種混合式應用的案例中,UpgradeModule 在 AngularJS 的方法和 Angular 的方法之間建立了橋樑。發生了什麼呢?

In the case of hybrid applications, the UpgradeModule bridges the AngularJS and Angular approaches. Here's what happens:

  • 應用中發生的每件事都執行在 Angular 的 zone 裡。 無論事件發生在 AngularJS 還是 Angular 的程式碼中,都是如此。 這個 zone 會在每個事件之後觸發 Angular 的變更檢測。

    Everything that happens in the application runs inside the Angular zone. This is true whether the event originated in AngularJS or Angular code. The zone triggers Angular change detection after every event.

  • UpgradeModule 將在每一次離開 Angular zone 時呼叫 AngularJS 的 $rootScope.$apply()。這樣也就同樣會在每個事件之後觸發 AngularJS 的變更檢測。

    The UpgradeModule will invoke the AngularJS $rootScope.$apply() after every turn of the Angular zone. This also triggers AngularJS change detection after every event.

在實踐中,你不用在自己的程式碼中呼叫 $apply(),而不用管這段程式碼是在 AngularJS 還是 Angular 中。 UpgradeModule 都替你做了。你仍然可以呼叫 $apply(),也就是說你不必從現有程式碼中移除此呼叫。 在混合式應用中,這些呼叫只會觸發一次額外的 AngularJS 變更檢測。

In practice, you do not need to call $apply(), regardless of whether it is in AngularJS or Angular. The UpgradeModule does it for us. You can still call $apply() so there is no need to remove such calls from existing code. Those calls just trigger additional AngularJS change detection checks in a hybrid application.

當你降級一個 Angular 元件,然後把它用於 AngularJS 中時,元件的輸入屬性就會被 AngularJS 的變更檢測體系監視起來。 當那些輸入屬性發生變化時,元件中相應的屬性就會被設定。你也能透過實現OnChanges 介面來掛鉤到這些更改,就像它未被降級時一樣。

When you downgrade an Angular component and then use it from AngularJS, the component's inputs will be watched using AngularJS change detection. When those inputs change, the corresponding properties in the component are set. You can also hook into the changes by implementing the OnChanges interface in the component, just like you could if it hadn't been downgraded.

相應的,當你把 AngularJS 的元件升級給 Angular 使用時,在這個元件型指令的 scope(或 bindToController)中定義的所有繫結, 都將被掛鉤到 Angular 的變更檢測體系中。它們將和標準的 Angular 輸入屬性被同等對待,並當它們發生變化時設定回 scope(或控制器)上。

Correspondingly, when you upgrade an AngularJS component and use it from Angular, all the bindings defined for the component directive's scope (or bindToController) will be hooked into Angular change detection. They will be treated as regular Angular inputs. Their values will be written to the upgraded component's scope (or controller) when they change.

透過 Angular 的 NgModule 來使用 UpgradeModule

Using UpgradeModule with Angular NgModules

AngularJS 還是 Angular 都有自己的模組概念,來幫你把應用組織成一些內聚的功能塊。

Both AngularJS and Angular have their own concept of modules to help organize an application into cohesive blocks of functionality.

它們在架構和實現的細節上有著顯著的不同。 在 AngularJS 中,你要把 AngularJS 的資源新增到 angular.module 屬性上。 在 Angular 中,你要建立一個或多個帶有 NgModule 裝飾器的類別,這些裝飾器用來在元資料中描述 Angular 資源。差異主要來自這裡。

Their details are quite different in architecture and implementation. In AngularJS, you add Angular assets to the angular.module property. In Angular, you create one or more classes adorned with an NgModule decorator that describes Angular assets in metadata. The differences blossom from there.

在混合式應用中,你同時運行了兩個版本的 Angular。 這意味著你至少需要 AngularJS 和 Angular 各提供一個模組。 當你使用 AngularJS 的模組進行引導時,就得把 Angular 的模組傳給 UpgradeModule

In a hybrid application you run both versions of Angular at the same time. That means that you need at least one module each from both AngularJS and Angular. You will import UpgradeModule inside the NgModule, and then use it for bootstrapping the AngularJS module.

要了解更多,請參閱NgModules頁。

For more information, see NgModules.

引導混合式應用程式

Bootstrapping hybrid applications

要想引導混合式應用,就必須在應用中分別引導 Angular 和 AngularJS 應用的一部分。你必須先引導 Angular,然後再呼叫 UpgradeModule 來引導 AngularJS。

To bootstrap a hybrid application, you must bootstrap each of the Angular and AngularJS parts of the application. You must bootstrap the Angular bits first and then ask the UpgradeModule to bootstrap the AngularJS bits next.

在 AngularJS 應用中有一個 AngularJS 的根模組,它用於引導 AngularJS 應用。

In an AngularJS application you have a root AngularJS module, which will also be used to bootstrap the AngularJS application.

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

單純的 AngularJS 應用可以在 HTML 頁面中使用 ng-app 指令進行引導,但對於混合式應用你要透過 UpgradeModule 模組進行手動引導。因此,在切換成混合式應用之前,最好先把 AngularJS 改寫成使用 angular.bootstrap進行手動引導的方式。

Pure AngularJS applications can be automatically bootstrapped by using an ng-app directive somewhere on the HTML page. But for hybrid applications, you manually bootstrap via the UpgradeModule. Therefore, it is a good preliminary step to switch AngularJS applications to use the manual JavaScript angular.bootstrapmethod even before switching them to hybrid mode.

比如你現在有這樣一個透過 ng-app 進行引導的應用:

Say you have an ng-app driven bootstrap such as this one:

<!DOCTYPE HTML> <html lang="en"> <head> <base href="/"> <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.3/angular.js"></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>
      
      <!DOCTYPE HTML>
<html lang="en">
  <head>
    <base href="/">
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.3/angular.js"></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-appng-strict-di 指令,改為從 JavaScript 中呼叫 angular.bootstrap,它能達到同樣效果:

You can remove the ng-app and ng-strict-di directives from the HTML and instead switch to calling angular.bootstrap from JavaScript, which will result in the same thing:

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

要想把 AngularJS 應用變成 Hybrid 應用,就要先載入 Angular 框架。 根據準備升級到 AngularJS 中給出的步驟,選擇性的把“快速上手”的 Github 儲存庫中的程式碼複製過來。

To begin converting your AngularJS application to a hybrid, you need to load the Angular framework. You can see how this can be done with SystemJS by following the instructions in Setup for Upgrading to AngularJS for selectively copying code from the QuickStart github repository.

也可以透過 npm install @angular/upgrade --save 命令來安裝 @angular/upgrade 套件,並給它新增一個到 @angular/upgrade/static 套件的對映。

You also need to install the @angular/upgrade package via npm install @angular/upgrade --save and add a mapping for the @angular/upgrade/static package:

'@angular/upgrade/static': 'npm:@angular/upgrade/bundles/upgrade-static.umd.js',
systemjs.config.js (map)
      
      '@angular/upgrade/static': 'npm:@angular/upgrade/bundles/upgrade-static.umd.js',
    

接下來,建立一個 app.module.ts 檔案,並新增下列 NgModule 類別:

Next, create an app.module.ts file and add the following NgModule class:

import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { UpgradeModule } from '@angular/upgrade/static'; @NgModule({ imports: [ BrowserModule, UpgradeModule ] }) export class AppModule { constructor(private upgrade: UpgradeModule) { } ngDoBootstrap() { this.upgrade.bootstrap(document.body, ['heroApp'], { strictDi: true }); } }
app.module.ts
      
      import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { UpgradeModule } from '@angular/upgrade/static';

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

最小化的 NgModule 匯入了 BrowserModule,它是每個基於瀏覽器的 Angular 應用必備的。 它還從 @angular/upgrade/static 中匯入了 UpgradeModule,它匯出了一些服務提供者,這些提供者會用於升級、降級服務和元件。

This bare minimum NgModule imports BrowserModule, the module every Angular browser-based app must have. It also imports UpgradeModule from @angular/upgrade/static, which exports providers that will be used for upgrading and downgrading services and components.

AppModule 的建構函式中,使用依賴注入技術獲取了一個 UpgradeModule 實例,並用它在 AppModule.ngDoBootstrap 方法中啟動 AngularJS 應用。 upgrade.bootstrap 方法接受和 angular.bootstrap 完全相同的引數。

In the constructor of the AppModule, use dependency injection to get a hold of the UpgradeModule instance, and use it to bootstrap the AngularJS app in the AppModule.ngDoBootstrap method. The upgrade.bootstrap method takes the exact same arguments as angular.bootstrap:

注意,你不需要在 @NgModule 中加入 bootstrap 宣告,因為 AngularJS 控制著該應用的根範本。

Note that you do not add a bootstrap declaration to the @NgModule decorator, since AngularJS will own the root template of the application.

現在,你就可以使用 platformBrowserDynamic.bootstrapModule 方法來啟動 AppModule 了。

Now you can bootstrap AppModule using the platformBrowserDynamic.bootstrapModule method.

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

platformBrowserDynamic().bootstrapModule(AppModule);
    

恭喜!你就要開始執行這個混合式應用了!所有現存的 AngularJS 程式碼會像以前一樣正常工作,但是你現在也同樣可以執行 Angular 程式碼了。

Congratulations! You're running a hybrid application! The existing AngularJS code works as before and you're ready to start adding Angular code.

在 AngularJS 的程式碼中使用 Angular 的元件

Using Angular Components from AngularJS Code

Using an Angular component from AngularJS code

一旦你開始執行混合式應用,你就可以開始逐漸升級程式碼了。一種更常見的工作模式就是在 AngularJS 的上下文中使用 Angular 的元件。 該元件可能是全新的,也可能是把原本 AngularJS 的元件用 Angular 重寫而成的。

Once you're running a hybrid app, you can start the gradual process of upgrading code. One of the more common patterns for doing that is to use an Angular component in an AngularJS context. This could be a completely new component or one that was previously AngularJS but has been rewritten for Angular.

假設你有一個簡單的用來顯示英雄資訊的 Angular 元件:

Say you have a simple Angular component that shows information about a hero:

import { Component } from '@angular/core'; @Component({ selector: 'hero-detail', template: ` <h2>Windstorm details!</h2> <div><label>id: </label>1</div> ` }) export class HeroDetailComponent { }
hero-detail.component.ts
      
      import { Component } from '@angular/core';

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

如果你想在 AngularJS 中使用這個元件,就得用 downgradeComponent() 方法把它降級。 其結果是一個 AngularJS 的指令,你可以把它註冊到 AngularJS 的模組中:

If you want to use this component from AngularJS, you need to downgrade it using the downgradeComponent() method. The result is an AngularJS directive, which you can then register in the AngularJS module:

import { HeroDetailComponent } from './hero-detail.component'; /* . . . */ import { downgradeComponent } from '@angular/upgrade/static'; angular.module('heroApp', []) .directive( 'heroDetail', downgradeComponent({ component: HeroDetailComponent }) as angular.IDirectiveFactory );
app.module.ts
      
      import { HeroDetailComponent } from './hero-detail.component';

/* . . . */

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

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

預設情況下,Angular 變更檢測也會在 AngularJS 的每個 $digest 週期中執行。如果你希望只在輸入屬性發生變化時才執行變更檢測,可以在呼叫 downgradeComponent() 時把 propagateDigest 設定為 false

By default, Angular change detection will also run on the component for every AngularJS $digest cycle. If you wish to only have change detection run when the inputs change, you can set propagateDigest to false when calling downgradeComponent().

由於 HeroDetailComponent 是一個 Angular 元件,所以你必須同時把它加入 AppModuledeclarations 欄位中。

Because HeroDetailComponent is an Angular component, you must also add it to the declarations in the AppModule.

並且由於這個元件在 AngularJS 模組中使用,也是你 Angular 應用的一個入口點,你還需要 將它加入到 NgModule 的 entryComponents 列表中。

And because this component is being used from the AngularJS module, and is an entry point into the Angular application, you must add it to the entryComponents for the NgModule.

import { HeroDetailComponent } from './hero-detail.component'; @NgModule({ imports: [ BrowserModule, UpgradeModule ], declarations: [ HeroDetailComponent ], entryComponents: [ HeroDetailComponent ] }) export class AppModule { constructor(private upgrade: UpgradeModule) { } ngDoBootstrap() { this.upgrade.bootstrap(document.body, ['heroApp'], { strictDi: true }); } }
app.module.ts
      
      import { HeroDetailComponent } from './hero-detail.component';

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

所有 Angular 元件、指令和管道都必須宣告在 NgModule 中。

All Angular components, directives and pipes must be declared in an NgModule.

最終的結果是一個叫做 heroDetail 的 AngularJS 指令,你可以像用其它指令一樣把它用在 AngularJS 範本中。

The net result is an AngularJS directive called heroDetail, that you can use like any other directive in AngularJS templates.

<hero-detail></hero-detail>
      
      <hero-detail></hero-detail>
    

注意,它在 AngularJS 中是一個名叫 heroDetail 的元素型指令(restrict: 'E')。 AngularJS 的元素型指令是基於它的名字匹配的。 Angular 元件中的 selector 元資料,在降級後的版本中會被忽略。

Note that this AngularJS is an element directive (restrict: 'E') called heroDetail. An AngularJS element directive is matched based on its name. The selector metadata of the downgraded Angular component is ignored.

當然,大多陣列件都不像這個這麼簡單。它們中很多都有輸入屬性和輸出屬性,來把它們連線到外部世界。 Angular 的英雄詳情元件帶有像這樣的輸入屬性與輸出屬性:

Most components are not quite this simple, of course. Many of them have inputs and outputs that connect them to the outside world. An Angular hero detail component with inputs and outputs might look like this:

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); } }
hero-detail.component.ts
      
      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() 方法負責橋接它們:

These inputs and outputs can be supplied from the AngularJS template, and the downgradeComponent() method takes care of wiring them up:

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

注意,雖然你正在 AngularJS 的範本中,但卻在使用 Angular 的屬性(Attribute)語法來繫結到輸入屬性與輸出屬性。 這是降級的元件本身要求的。而表示式本身仍然是標準的 AngularJS 表示式。

Note that even though you are in an AngularJS template, you're using Angular attribute syntax to bind the inputs and outputs. This is a requirement for downgraded components. The expressions themselves are still regular AngularJS expressions.

在降級過的元件屬性中使用中線命名法
Use kebab-case for downgraded component attributes

為降級過的元件使用 Angular 的屬性(Attribute)語法規則時有一個值得注意的例外。 它適用於由多個單片語成的輸入或輸出屬性。在 Angular 中,你要使用小駝峰命名法繫結這些屬性:

There's one notable exception to the rule of using Angular attribute syntax for downgraded components. It has to do with input or output names that consist of multiple words. In Angular, you would bind these attributes using camelCase:

[myHero]="hero" (heroDeleted)="handleHeroDeleted($event)"
      
      [myHero]="hero"
(heroDeleted)="handleHeroDeleted($event)"
    

但是從 AngularJS 的範本中使用它們時,你得使用中線命名法:

But when using them from AngularJS templates, you must use kebab-case:

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

$event 變數能被用在輸出屬性裡,以訪問這個事件所發出的物件。這個案例中它是 Hero 物件,因為 this.deleted.emit() 函式曾把它傳了出來。

The $event variable can be used in outputs to gain access to the object that was emitted. In this case it will be the Hero object, because that is what was passed to this.deleted.emit().

由於這是一個 AngularJS 範本,雖然它已經有了 Angular 中繫結的屬性(Attribute),你仍可以在這個元素上使用其它 AngularJS 指令。 例如,你可以用 ng-repeat 簡單的製作該元件的多份拷貝:

Since this is an AngularJS template, you can still use other AngularJS directives on the element, even though it has Angular binding attributes on it. For example, you can easily make multiple copies of the component using 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>
      
      <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 元件型指令

Using AngularJS Component Directives from Angular Code

Using an AngularJS component from Angular code

現在,你已經能在 Angular 中寫一個元件,並把它用於 AngularJS 程式碼中了。 當你從低階元件開始移植,並往上走時,這非常有用。但在另外一些情況下,從相反的方向進行移植會更加方便: 從高階元件開始,然後往下走。這也同樣能用 UpgradeModule 完成。 你可以升級AngularJS 元件型指令,然後從 Angular 中用它們。

So, you can write an Angular component and then use it from AngularJS code. This is useful when you start to migrate from lower-level components and work your way up. But in some cases it is more convenient to do things in the opposite order: To start with higher-level components and work your way down. This too can be done using the upgrade/static. You can upgrade AngularJS component directives and then use them from Angular.

不是所有種類別的 AngularJS 指令都能升級。該指令必須是一個嚴格的元件型指令,具有上面的準備指南中描述的那些特徵。 確保相容性的最安全的方式是 AngularJS 1.5 中引入的元件 API

Not all kinds of AngularJS directives can be upgraded. The directive really has to be a component directive, with the characteristics described in the preparation guide above. The safest bet for ensuring compatibility is using the component API introduced in AngularJS 1.5.

可升級元件的簡單例子是只有一個範本和一個控制器的指令:

A simple example of an upgradable component is one that just has a template and a controller:

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

你可以使用 UpgradeComponent 方法來把這個元件升級到 Angular。 具體方法是建立一個 Angular指令,繼承 UpgradeComponent,在其建構函式中進行 super 呼叫, 這樣你就得到一個完全升級的 AngularJS 元件,並且可以 Angular 中使用。 剩下是工作就是把它加入到 AppModuledeclarations 陣列。

You can upgrade this component to Angular using the UpgradeComponent class. By creating a new Angular directive that extends UpgradeComponent and doing a super call inside its constructor, you have a fully upgraded AngularJS component to be used inside Angular. All that is left is to add it to AppModule's declarations array.

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); } }
hero-detail.component.ts
      
      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 { constructor(private upgrade: UpgradeModule) { } ngDoBootstrap() { this.upgrade.bootstrap(document.body, ['heroApp'], { strictDi: true }); } }
app.module.ts
      
      @NgModule({
  imports: [
    BrowserModule,
    UpgradeModule
  ],
  declarations: [
    HeroDetailDirective,
/* . . . */
  ]
})
export class AppModule {
  constructor(private upgrade: UpgradeModule) { }
  ngDoBootstrap() {
    this.upgrade.bootstrap(document.body, ['heroApp'], { strictDi: true });
  }
}
    

升級後的元件是 Angular 的指令,而不是元件,因為 Angular 不知道 AngularJS 將在它下面建立元素。 Angular 所知道的是升級後的元件只是一個指令(一個標籤),Angular 不需要關心元件本身及其子元素。

Upgraded components are Angular directives, instead of components, because Angular is unaware that AngularJS will create elements under it. As far as Angular knows, the upgraded component is just a directive - a tag - and Angular doesn't have to concern itself with its children.

升級後的元件也可能有輸入屬性和輸出屬性,它們是在原 AngularJS 元件型指令的 scope/controller 繫結中定義的。 當你從 Angular 範本中使用該元件時,就要使用Angular 範本語法來提供這些輸入屬性和輸出屬性,但要遵循下列規則:

An upgraded component may also have inputs and outputs, as defined by the scope/controller bindings of the original AngularJS component directive. When you use the component from an Angular template, provide the inputs and outputs using Angular template syntax, observing the following rules:

繫結定義

Binding definition

範本語法

Template syntax

屬性(Attribute)繫結

Attribute binding

myAttribute: '@myAttribute'

<my-component myAttribute="value">

表示式繫結

Expression binding

myOutput: '&myOutput'

<my-component (myOutput)="action()">

單向繫結

One-way binding

myValue: '<myValue'

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

雙向繫結

Two-way binding

myValue: '=myValue'

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

As a two-way binding: <my-component [(myValue)]="anExpression">. Since most AngularJS two-way bindings actually only need a one-way binding in practice, <my-component [myValue]="anExpression"> is often enough.

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

For example, imagine a hero detail AngularJS component directive with one input and one output:

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); }; } };
hero-detail.component.ts
      
      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);
    };
  }
};
    

你可以把這個元件升級到 Angular,然後使用 Angular 的範本語法提供這個輸入屬性和輸出屬性:

You can upgrade this component to Angular, annotate inputs and outputs in the upgrade directive, and then provide the input and output using Angular template syntax:

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); } }
hero-detail.component.ts
      
      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; } }
container.component.ts
      
      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 的內容投影到 Angular 元件中

Projecting AngularJS Content into Angular Components

Projecting AngularJS content into Angular

如果你在 AngularJS 範本中使用降級後的 Angular 元件時,可能會需要把範本中的一些內容投影進那個元件。 這也是可能的,雖然在 Angular 中並沒有透傳(transclude)這樣的東西,但它有一個非常相似的概念,叫做內容投影UpgradeModule 也能讓這兩個特性實現互操作。

When you are using a downgraded Angular component from an AngularJS template, the need may arise to transclude some content into it. This is also possible. While there is no such thing as transclusion in Angular, there is a very similar concept called content projection. upgrade/static is able to make these two features interoperate.

Angular 的元件透過使用 <ng-content> 標籤來支援內容投影。下面是這類別元件的一個例子:

Angular components that support content projection make use of an <ng-content> tag within them. Here's an example of such a component:

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; }
hero-detail.component.ts
      
      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;
}
    

當從 AngularJS 中使用該元件時,你可以為它提供內容。正如它們將在 AngularJS 中被透傳一樣, 它們也在 Angular 中被投影到了 <ng-content> 標籤所在的位置:

When using the component from AngularJS, you can supply contents for it. Just like they would be transcluded in AngularJS, they get projected to the location of the <ng-content> tag in Angular:

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

當 AngularJS 的內容被投影到 Angular 元件中時,它仍然留在“AngularJS 王國”中,並被 AngularJS 框架管理著。

When AngularJS content gets projected inside an Angular component, it still remains in "AngularJS land" and is managed by the AngularJS framework.

把 Angular 的內容透傳進 AngularJS 的元件型指令

Transcluding Angular Content into AngularJS Component Directives

Projecting Angular content into AngularJS

就像可以把 AngularJS 的內容投影進 Angular 元件一樣,你也能把 Angular 的內容透傳進 AngularJS 的元件, 但不管怎樣,你都要使用它們升級過的版本。

Just as you can project AngularJS content into Angular components, you can transclude Angular content into AngularJS components, whenever you are using upgraded versions from them.

如果一個 AngularJS 元件型指令支援透傳,它就會在自己的範本中使用 ng-transclude 指令標記出透傳到的位置:

When an AngularJS component directive supports transclusion, it may use the ng-transclude directive in its template to mark the transclusion point:

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

如果你升級這個元件,並把它用在 Angular 中,你就能把準備透傳的內容放進這個元件的標籤中。

If you upgrade this component and use it from Angular, you can populate the component tag with contents that will then get transcluded:

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'); }
container.component.ts
      
      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

Making AngularJS Dependencies Injectable to Angular

當執行一個混合式應用時,可能會遇到這種情況:你需要把某些 AngularJS 的依賴注入到 Angular 程式碼中。 這可能是因為某些業務邏輯仍然在 AngularJS 服務中,或者需要某些 AngularJS 的內建服務,比如 $location$timeout

When running a hybrid app, you may encounter situations where you need to inject some AngularJS dependencies into your Angular code. Maybe you have some business logic still in AngularJS services. Maybe you want access to AngularJS's built-in services like $location or $timeout.

在這些情況下,把一個 AngularJS 提供者升級到Angular 也是有可能的。這就讓它將來有可能被注入到 Angular 程式碼中的某些地方。 比如,你可能在 AngularJS 中有一個名叫 HeroesService 的服務:

In these situations, it is possible to upgrade an AngularJS provider to Angular. This makes it possible to then inject it somewhere in Angular code. For example, you might have a service called HeroesService in AngularJS:

import { Hero } from '../hero'; export class HeroesService { get() { return [ new Hero(1, 'Windstorm'), new Hero(2, 'Spiderman') ]; } }
heroes.service.ts
      
      import { Hero } from '../hero';

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

你可以用 Angular 的工廠提供者升級該服務, 它從 AngularJS 的 $injector 請求服務。

You can upgrade the service using a Angular factory provider that requests the service from the AngularJS $injector.

很多開發者都喜歡在一個獨立的 ajs-upgraded-providers.ts 中宣告這個工廠提供者,以便把它們都放在一起,這樣便於參考、建立新的以及在升級完畢時刪除它們。

Many developers prefer to declare the factory provider in a separate ajs-upgraded-providers.ts file so that they are all together, making it easier to reference them, create new ones and delete them once the upgrade is over.

同時,建議匯出 heroesServiceFactory 函式,以便 AOT 編譯器可以拿到它們。

It's also recommended to export the heroesServiceFactory function so that Ahead-of-Time compilation can pick it up.

注意:這個工廠中的字串 'heroes' 指向的是 AngularJS 的 HeroesService。 AngularJS 應用中通常使用服務名作為令牌,比如 'heroes',並為其追加 'Service' 字尾來建立其類別名稱。

Note: The 'heroes' string inside the factory refers to the AngularJS HeroesService. It is common in AngularJS apps to choose a service name for the token, for example "heroes", and append the "Service" suffix to create the class name.

import { HeroesService } from './heroes.service'; export function heroesServiceFactory(i: any) { return i.get('heroes'); } export const heroesServiceProvider = { provide: HeroesService, useFactory: heroesServiceFactory, deps: ['$injector'] };
ajs-upgraded-providers.ts
      
      import { HeroesService } from './heroes.service';

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

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

然後,你就可以把這個服務新增到 @NgModule 中來把它暴露給 Angular:

You can then provide the service to Angular by adding it to the @NgModule:

import { heroesServiceProvider } from './ajs-upgraded-providers'; @NgModule({ imports: [ BrowserModule, UpgradeModule ], providers: [ heroesServiceProvider ], /* . . . */ }) export class AppModule { constructor(private upgrade: UpgradeModule) { } ngDoBootstrap() { this.upgrade.bootstrap(document.body, ['heroApp'], { strictDi: true }); } }
app.module.ts
      
      import { heroesServiceProvider } from './ajs-upgraded-providers';

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

然後在元件的建構函式中使用該服務的類別名稱作為型別註解注入到元件中,從而在元件中使用它:

Then use the service inside your component by injecting it in the component constructor using its class as a type annotation:

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]; } }
hero-detail.component.ts
      
      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];
  }
}
    

在這個例子中,你升級了服務類別。當注入它時,你可以使用 TypeScript 型別註解來獲得這些額外的好處。 它沒有影響該依賴的處理過程,同時還得到了啟用靜態型別檢查的好處。 任何 AngularJS 中的服務、工廠和提供者都能被升級 —— 儘管這不是必須的。

In this example you upgraded a service class. You can use a TypeScript type annotation when you inject it. While it doesn't affect how the dependency is handled, it enables the benefits of static type checking. This is not required though, and any AngularJS service, factory, or provider can be upgraded.

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

Making Angular Dependencies Injectable to AngularJS

除了能升級 AngularJS 依賴之外,你還能降級Angular 的依賴,以便在 AngularJS 中使用它們。 當你已經開始把服務移植到 Angular 或在 Angular 中建立新服務,但同時還有一些用 AngularJS 寫成的元件時,這會非常有用。

In addition to upgrading AngularJS dependencies, you can also downgrade Angular dependencies, so that you can use them from AngularJS. This can be useful when you start migrating services to Angular or creating new services in Angular while retaining components written in AngularJS.

例如,你可能有一個 Angular 的 Heroes 服務:

For example, you might have an Angular service called Heroes:

import { Injectable } from '@angular/core'; import { Hero } from '../hero'; @Injectable() export class Heroes { get() { return [ new Hero(1, 'Windstorm'), new Hero(2, 'Spiderman') ]; } }
heroes.ts
      
      import { Injectable } from '@angular/core';
import { Hero } from '../hero';

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

仿照 Angular 元件,把該提供者加入 NgModuleproviders 列表中,以註冊它。

Again, as with Angular components, register the provider with the NgModule by adding it to the module's providers list.

import { Heroes } from './heroes'; @NgModule({ imports: [ BrowserModule, UpgradeModule ], providers: [ Heroes ] }) export class AppModule { constructor(private upgrade: UpgradeModule) { } ngDoBootstrap() { this.upgrade.bootstrap(document.body, ['heroApp'], { strictDi: true }); } }
app.module.ts
      
      import { Heroes } from './heroes';

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

現在,用 downgradeInjectable() 來把 Angular 的 Heroes 包裝成AngularJS 的工廠函式,並把這個工廠註冊進 AngularJS 的模組中。 依賴在 AngularJS 中的名字你可以自己定:

Now wrap the Angular Heroes in an AngularJS factory function using downgradeInjectable() and plug the factory into an AngularJS module. The name of the AngularJS dependency is up to you:

import { Heroes } from './heroes'; /* . . . */ import { downgradeInjectable } from '@angular/upgrade/static'; angular.module('heroApp', []) .factory('heroes', downgradeInjectable(Heroes)) .component('heroDetail', heroDetailComponent);
app.module.ts
      
      import { Heroes } from './heroes';
/* . . . */
import { downgradeInjectable } from '@angular/upgrade/static';

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

此後,該服務就能被注入到 AngularJS 程式碼中的任何地方了:

After this, the service is injectable anywhere in AngularJS code:

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

延遲載入 AngularJS

Lazy Loading AngularJS

在建構應用時,你需要確保只在必要的時候才載入所需的資源,無論是載入靜態資產(Asset)還是程式碼。要確保任何事都儘量推遲到必要時才去做,以便讓應用更高效的執行。當要在同一個應用中執行不同的框架時,更是如此。

When building applications, you want to ensure that only the required resources are loaded when necessary. Whether that be loading of assets or code, making sure everything that can be deferred until needed keeps your application running efficiently. This is especially true when running different frameworks in the same application.

延遲載入是一項技術,它會推遲到使用時才載入所需靜態資產和程式碼資源。這可以減少啟動時間、提高效率,特別是要在同一個應用中執行不同的框架時。

Lazy loading is a technique that defers the loading of required assets and code resources until they are actually used. This reduces startup time and increases efficiency, especially when running different frameworks in the same application.

當你採用混合式應用的方式將大型應用從 AngularJS 遷移到 Angular 時,你首先要遷移一些最常用的特性,並且只在必要的時候才使用那些不太常用的特性。這樣做有助於確保應用程式在遷移過程中仍然能為使用者提供無縫的體驗。

When migrating large applications from AngularJS to Angular using a hybrid approach, you want to migrate some of the most commonly used features first, and only use the less commonly used features if needed. Doing so helps you ensure that the application is still providing a seamless experience for your users while you are migrating.

在大多數需要同時用 Angular 和 AngularJS 渲染應用的環境中,這兩個框架都會包含在傳送給客戶端的初始發佈套件中。這會導致發佈套件的體積增大、效能降低。

In most environments where both Angular and AngularJS are used to render the application, both frameworks are loaded in the initial bundle being sent to the client. This results in both increased bundle size and possible reduced performance.

當用戶停留在由 Angular 渲染的頁面上時,應用的整體效能也會受到影響。這是因為 AngularJS 的框架和應用仍然被載入並運行了 —— 即使它們從未被訪問過。

Overall application performance is affected in cases where the user stays on Angular-rendered pages because the AngularJS framework and application are still loaded and running, even if they are never accessed.

你可以採取一些措施來緩解這些套件的大小和效能問題。透過把 AngularJS 應用程式分離到一個單獨的發佈套件中,你就可以利用延遲載入技術來只在必要的時候才載入、引導和渲染這個 AngularJS 應用。這種策略減少了你的初始發佈套件大小,推遲了同時載入兩個框架的潛在影響 —— 直到絕對必要時才載入,以便讓你的應用盡可能高效地執行。

You can take steps to mitigate both bundle size and performance issues. By isolating your AngularJS app to a separate bundle, you can take advantage of lazy loading to load, bootstrap, and render the AngularJS application only when needed. This strategy reduces your initial bundle size, defers any potential impact from loading both frameworks until absolutely necessary, and keeps your application running as efficiently as possible.

下面的步驟介紹了應該如何去做:

The steps below show you how to do the following:

  • 為 AngularJS 發佈套件設定一個回呼(Callback)函式。

    Setup a callback function for your AngularJS bundle.

  • 建立一個服務,以便延遲載入並引導你的 AngularJS 應用。

    Create a service that lazy loads and bootstraps your AngularJS app.

  • 為 AngularJS 的內容建立一個可路由的元件

    Create a routable component for AngularJS content

  • 為 AngularJS 特有的 URL 建立自訂的 matcher 函式,並為 AngularJS 的各個路由配上帶有自訂匹配器的 Angular 路由器。

    Create a custom matcher function for AngularJS-specific URLs and configure the Angular Router with the custom matcher for AngularJS routes.

為延遲載入 AngularJS 建立一個服務

Create a service to lazy load AngularJS

在 Angular 的版本 8 中,延遲載入程式碼只需使用動態匯入語法 import('...') 即可。在這個應用中,你建立了一個新服務,它使用動態匯入技術來延遲載入 AngularJS。

As of Angular version 8, lazy loading code can be accomplished simply by using the dynamic import syntax import('...'). In your application, you create a new service that uses dynamic imports to lazy load AngularJS.

import { Injectable } from '@angular/core'; import * as angular from 'angular'; @Injectable({ providedIn: 'root' }) export class LazyLoaderService { private app: angular.auto.IInjectorService; 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(); } } }
src/app/lazy-loader.service.ts
      
      import { Injectable } from '@angular/core';
import * as angular from 'angular';

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

  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();
    }
  }
}
    

該服務使用 import() 方法延遲載入打包好的 AngularJS 應用。這會減少應用初始套件的大小,因為你尚未載入使用者目前不需要的程式碼。你還要提供一種方法,在載入完畢後手動啟動它。AngularJS 提供了一種使用 angular.bootstrap() 方法並傳入一個 HTML 元素來手動引導應用的方法。你的 AngularJS 應用也應該公開一個用來引導 AngularJS 應用的 bootstrap 方法。

The service uses the import() method to load your bundled AngularJS application lazily. This decreases the initial bundle size of your application as you're not loading code your user doesn't need yet. You also need to provide a way to bootstrap the application manually after it has been loaded. AngularJS provides a way to manually bootstrap an application using the angular.bootstrap() method with a provided HTML element. Your AngularJS app should also expose a bootstrap method that bootstraps the AngularJS app.

要確保 AngularJS 應用中的任何清理工作都觸發過(比如移除全域性監聽器),你還可以實現一個方法來呼叫 $rootScope.destroy() 方法。

To ensure any necessary teardown is triggered in the AngularJS app, such as removal of global listeners, you also implement a method to call the $rootScope.destroy() method.

import * as angular from 'angular'; import 'angular-route'; const appModule = angular.module('myApp', [ 'ngRoute' ]) .config(['$routeProvider', '$locationProvider', function config($routeProvider, $locationProvider) { $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-app
      
      import * as angular from 'angular';
import 'angular-route';

const appModule = angular.module('myApp', [
  'ngRoute'
])
.config(['$routeProvider', '$locationProvider',
  function config($routeProvider, $locationProvider) {
    $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 應用只配置了渲染內容所需的那部分路由。而 Angular 路由器會處理應用中其餘的路由。你的 Angular 應用中會呼叫公開的 bootstrap 方法,讓它在載入完發佈套件之後引導 AngularJS 應用。

Your AngularJS application is configured with only the routes it needs to render content. The remaining routes in your application are handled by the Angular Router. The exposed bootstrap method is called in your Angular app to bootstrap the AngularJS application after the bundle is loaded.

注意:當 AngularJS 載入並引導完畢後,監聽器(比如路由配置中的那些監聽器)會繼續監聽路由的變化。為了確保當 AngularJS 尚未顯示時先關閉監聽器,請在 $routeProvider 中配置一個渲染空範本 otherwise 選項。這裡假設 Angular 將處理所有其它路由。

Note: After AngularJS is loaded and bootstrapped, listeners such as those wired up in your route configuration will continue to listen for route changes. To ensure listeners are shut down when AngularJS isn't being displayed, configure an otherwise option with the $routeProvider that renders an empty template. This assumes all other routes will be handled by Angular.

建立一個用來渲染 AngularJS 內容的元件

Create a component to render AngularJS content

在 Angular 應用中,你需要一個元件作為 AngularJS 內容的佔位符。該元件使用你建立的服務,並在元件初始化完成後載入並引導你的 AngularJS 應用。

In your Angular application, you need a component as a placeholder for your AngularJS content. This component uses the service you create to load and bootstrap your AngularJS app after the component is initialized.

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(); } }
src/app/angular-js/angular-js.component.ts
      
      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();
  }
}
    

當 Angular 的路由器匹配到使用 AngularJS 的路由時,會渲染 AngularJSComponent,並在 AngularJS 的 ng-view指令中渲染內容。當用戶導航離開本路由時,$rootScope 會在 AngularJS 應用中被銷燬。

When the Angular Router matches a route that uses AngularJS, the AngularJSComponent is rendered, and the content is rendered within the AngularJS ng-viewdirective. When the user navigates away from the route, the $rootScope is destroyed on the AngularJS application.

為那些 AngularJS 路由配置自訂路由匹配器

Configure a custom route matcher for AngularJS routes

為了配置 Angular 的路由器,你必須為 AngularJS 的 URL 定義路由。要匹配這些 URL,你需要新增一個使用 matcher 屬性的路由配置。這個 matcher 允許你使用自訂模式來匹配這些 URL 路徑。Angular 的路由器會首先嚐試匹配更具體的路由,比如靜態路由和可變路由。當它找不到匹配項時,就會求助於路由配置中的自訂匹配器。如果自訂匹配器與某個路由不匹配,它就會轉到用於 "捕獲所有"(catch-all)的路由,比如 404 頁面。

To configure the Angular Router, you must define a route for AngularJS URLs. To match those URLs, you add a route configuration that uses the matcher property. The matcher allows you to use custom pattern matching for URL paths. The Angular Router tries to match on more specific routes such as static and variable routes first. When it doesn't find a match, it then looks at custom matchers defined in your route configuration. If the custom matchers don't match a route, it then goes to catch-all routes, such as a 404 page.

下面的例子給 AngularJS 路由定義了一個自訂匹配器函式。

The following example defines a custom matcher function for AngularJS routes.

export function isAngularJSUrl(url: UrlSegment[]) { return url.length > 0 && url[0].path.startsWith('users') ? ({consumed: url}) : null; }
src/app/app-routing.module.ts
      
      export function isAngularJSUrl(url: UrlSegment[]) {
  return url.length > 0 && url[0].path.startsWith('users') ? ({consumed: url}) : null;
}
    

下列程式碼往你的路由配置中添加了一個路由物件,其 matcher 屬性是這個自訂匹配器,而 component 屬性為 AngularJSComponent

The following code adds a route object to your routing configuration using the matcher property and custom matcher, and the component property with 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 { }
src/app/app-routing.module.ts
      
      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 { }
    

當你的應用匹配上需要 AngularJS 的路由時,AngularJS 應用就會被載入並引導。AngularJS 路由會匹配必要的 URL 以渲染它們的內容,而接下來你的應用就會同時執行 AngularJS 和 Angular 框架。

When your application matches a route that needs AngularJS, the AngularJS app is loaded and bootstrapped, the AngularJS routes match the necessary URL to render their content, and your application continues to run with both AngularJS and Angular frameworks.

使用統一的 Angular 位置服務(Location)

Using the Unified Angular Location Service

在 AngularJS 中,$location 服務會處理所有路由配置和導航工作,並對各個 URL 進行編碼和解碼、重新導向、以及與瀏覽器 API 互動。Angular 在所有這些任務中都使用了自己的底層服務 Location

In AngularJS, the $location service handles all routing configuration and navigation, encoding and decoding of URLS, redirects, and interactions with browser APIs. Angular uses its own underlying Location service for all of these tasks.

當你從 AngularJS 遷移到 Angular 時,你會希望把儘可能多的責任移交給 Angular,以便利用新的 API。為了幫你完成這種轉換,Angular 提供了 LocationUpgradeModule。該模組支援統一位置服務,可以把 AngularJS 中 $location 服務的職責轉給 Angular 的 Location 服務。

When you migrate from AngularJS to Angular you will want to move as much responsibility as possible to Angular, so that you can take advantage of new APIs. To help with the transition, Angular provides the LocationUpgradeModule. This module enables a unified location service that shifts responsibilities from the AngularJS $location service to the Angular Location service.

要使用 LocationUpgradeModule,就會從 @angular/common/upgrade 中匯入此符號,並使用靜態方法 LocationUpgradeModule.config() 把它新增到你的 AppModule 匯入表(imports)中。

To use the LocationUpgradeModule, import the symbol from @angular/common/upgrade and add it to your AppModule imports using the static LocationUpgradeModule.config() method.

// Other imports ... import { LocationUpgradeModule } from '@angular/common/upgrade'; @NgModule({ imports: [ // Other NgModule imports... LocationUpgradeModule.config() ] }) export class AppModule {}
      
      // Other imports ...
import { LocationUpgradeModule } from '@angular/common/upgrade';

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

LocationUpgradeModule.config() 方法接受一個配置物件,該物件的 useHashLocationStrategyhashPrefix 為 URL 字首。

The LocationUpgradeModule.config() method accepts a configuration object that allows you to configure options including the LocationStrategy with the useHash property, and the URL prefix with the hashPrefix property.

useHash 屬性預設為 false,而 hashPrefix 預設為空 string。傳遞配置物件可以覆蓋預設值。

The useHash property defaults to false, and the hashPrefix defaults to an empty string. Pass the configuration object to override the defaults.

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

注意:關於 LocationUpgradeModule.config() 方法的更多可用配置項,請參閱 LocationUpgradeConfig

Note: See the LocationUpgradeConfig for more configuration options available to the LocationUpgradeModule.config() method.

這會為 AngularJS 中的 $location 提供者註冊一個替代品。一旦註冊成功,導航過程中所有由 AngularJS 觸發的導航、路由廣播訊息以及任何必需的變更檢測週期都會改由 Angular 進行處理。這樣,你就可以透過這個唯一的途徑在此混合應用的兩個框架間進行導航了。

This registers a drop-in replacement for the $location provider in AngularJS. Once registered, all navigation, routing broadcast messages, and any necessary digest cycles in AngularJS triggered during navigation are handled by Angular. This gives you a single way to navigate within both sides of your hybrid application consistently.

要想在 AngularJS 中使用 $location 服務作為提供者,你需要使用一個工廠提供者來降級 $locationShim

For usage of the $location service as a provider in AngularJS, you need to downgrade the $locationShim using a factory provider.

// Other imports ... import { $locationShim } from '@angular/common/upgrade'; import { downgradeInjectable } from '@angular/upgrade/static'; angular.module('myHybridApp', [...]) .factory('$location', downgradeInjectable($locationShim));
      
      // Other imports ...
import { $locationShim } from '@angular/common/upgrade';
import { downgradeInjectable } from '@angular/upgrade/static';

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

一旦引入了 Angular 路由器,你只要使用 Angular 路由器就可以透過統一位置服務來觸發導航了,同時,你仍然可以透過 AngularJS 和 Angular 進行導航。

Once you introduce the Angular Router, using the Angular Router triggers navigations through the unified location service, still providing a single source for navigating with AngularJS and Angular.

PhoneCat 升級課程

PhoneCat Upgrade Tutorial

在本節和下節中,你將看一個完整的例子,它使用 upgrade 模組準備和升級了一個應用程式。 該應用就是來自原 AngularJS 課程中的Angular PhoneCat。 那是我們很多人當初開始 Angular 探險之旅的地方。 現在,你會看到如何把該應用帶入 Angular 的美麗新世界。

In this section, you'll learn to prepare and upgrade an application with ngUpgrade. The example app is Angular PhoneCat from the original AngularJS tutorial, which is where many of us began our Angular adventures. Now you'll see how to bring that application to the brave new world of Angular.

這期間,你將學到如何在實踐中應用準備指南中列出的那些重點步驟: 你先讓該應用向 Angular 看齊,然後為它引入 SystemJS 模組載入器和 TypeScript。

During the process you'll learn how to apply the steps outlined in the preparation guide. You'll align the application with Angular and also start writing in TypeScript.

要跟隨本課程,請先把angular-phonecat儲存庫複製到本地,並應用這些步驟。

To follow along with the tutorial, clone the angular-phonecat repository and apply the steps as you go.

在專案結構方面,工作的起點是這樣的:

In terms of project structure, this is where the work begins:

angular-phonecat

bower.json

karma.conf.js

package.json

app

core

checkmark

checkmark.filter.js

checkmark.filter.spec.js

phone

phone.module.js

phone.service.js

phone.service.spec.js

core.module.js

phone-detail

phone-detail.component.js

phone-detail.component.spec.js

phone-detail.module.js

phone-detail.template.html

phone-list

phone-list.component.js

phone-list.component.spec.js

phone-list.module.js

phone-list.template.html

img

...

phones

...

app.animations.js

app.config.js

app.css

app.module.js

index.html

e2e-tests

protractor-conf.js

scenarios.js

這確實是一個很好地起點。這些程式碼使用了 AngularJS 1.5 的元件 API,並遵循了 AngularJS 風格指南進行組織, 在成功升級之前,這是一個很重要的準備步驟

This is actually a pretty good starting point. The code uses the AngularJS 1.5 component API and the organization follows the AngularJS Style Guide, which is an important preparation step before a successful upgrade.

  • 每個元件、服務和過濾器都在它自己的原始檔中 —— 就像單一規則所要求的。

    Each component, service, and filter is in its own source file, as per the Rule of 1.

  • corephone-detailphone-list 模組都在它們自己的子目錄中。那些子目錄除了包含 HTML 範本之外,還包含 JavaScript 程式碼,它們共同完成一個特性。 這是按特性分目錄的結構模組化規則所要求的。

    The core, phone-detail, and phone-list modules are each in their own subdirectory. Those subdirectories contain the JavaScript code as well as the HTML templates that go with each particular feature. This is in line with the Folders-by-Feature Structure and Modularity rules.

  • 單元測試都和應用程式碼在一起,它們很容易找到。就像規則 組織測試檔案中要求的那樣。

    Unit tests are located side-by-side with application code where they are easily found, as described in the rules for Organizing Tests.

切換到 TypeScript

Switching to TypeScript

因為你將使用 TypeScript 編寫 Angular 的程式碼,所以在開始升級之前,先要把 TypeScript 的編譯器設定好。

Since you're going to be writing Angular code in TypeScript, it makes sense to bring in the TypeScript compiler even before you begin upgrading.

你還將開始逐步淘汰 Bower 套件管理器,換成 NPM。後面你將使用 NPM 來安裝新的相依套件,並最終從專案中移除 Bower。

You'll also start to gradually phase out the Bower package manager in favor of NPM, installing all new dependencies using NPM, and eventually removing Bower from the project.

先把 TypeScript 套件安裝到專案中。

Begin by installing TypeScript to the project.

npm i typescript --save-dev
      
      npm i typescript --save-dev
    

還要為那些沒有自帶型別資訊的函式庫(比如 AngularJS、AngularJS Material 和 Jasmine)安裝型別定義檔案。

Install type definitions for the existing libraries that you're using but that don't come with prepackaged types: AngularJS, AngularJS Material, and the Jasmine unit test framework.

對於 PhoneCat 應用,我們可以執行下列命令來安裝必要的型別定義檔案:

For the PhoneCat app, we can install the necessary type definitions by running the following command:

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
      
      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,你可以透過下列命令安裝其型別定義:

If you are using AngularJS Material, you can install the type definitions via:

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

你還應該要往專案目錄下新增一個 tsconfig.json 檔案, 就像在 TypeScript 配置中講過的那樣。 tsconfig.json 檔案會告訴 TypeScript 編譯器如何把 TypeScript 檔案轉成 ES5 程式碼,並打包進 CommonJS 模組中。

You should also configure the TypeScript compiler with a tsconfig.json in the project directory as described in the TypeScript Configuration guide. The tsconfig.json file tells the TypeScript compiler how to turn your TypeScript files into ES5 code bundled into CommonJS modules.

最後,你應該把下列 npm 指令碼新增到 package.json 中,用於把 TypeScript 檔案編譯成 JavaScript (根據 tsconfig.json 的配置):

Finally, you should add some npm scripts in package.json to compile the TypeScript files to JavaScript (based on the tsconfig.json configuration file):

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

現在,從命令列中用監視模式啟動 TypeScript 編譯器:

Now launch the TypeScript compiler from the command line in watch mode:

npm run tsc:w
      
      npm run tsc:w
    

讓這個程序一直在後臺執行,監聽任何變化並自動重新編譯。

Keep this process running in the background, watching and recompiling as you make changes.

接下來,把 JavaScript 檔案轉換成 TypeScript 檔案。 由於 TypeScript 是 ECMAScript 2015 的一個超集,而 ES2015 又是 ECMAScript 5 的超集,所以你可以簡單的把檔案的副檔名從 .js 換成 .ts, 它們還是會像以前一樣工作。由於 TypeScript 編譯器仍在執行,它會為每一個 .ts 檔案產生對應的 .js 檔案,而真正執行的是編譯後的 .js 檔案。 如果你用 npm start 開啟了本專案的 HTTP 伺服器,你會在瀏覽器中看到一個功能完好的應用。

Next, convert your current JavaScript files into TypeScript. Since TypeScript is a super-set of ECMAScript 2015, which in turn is a super-set of ECMAScript 5, you can simply switch the file extensions from .js to .ts and everything will work just like it did before. As the TypeScript compiler runs, it emits the corresponding .js file for every .ts file and the compiled JavaScript is what actually gets executed. If you start the project HTTP server with npm start, you should see the fully functional application in your browser.

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

Now that you have TypeScript though, you can start benefiting from some of its features. There's a lot of value the language can provide to AngularJS applications.

首先,TypeScript 是一個 ES2015 的超集。任何以前用 ES5 寫的程式(就像 PhoneCat 範例)都可以開始透過 TypeScript 納入那些新增到 ES2015 中的新特性。 這包括 letconst、箭頭函式、函式預設引數以及解構(destructure)賦值。

For one thing, TypeScript is a superset of ES2015. Any app that has previously been written in ES5 - like the PhoneCat example has - can with TypeScript start incorporating all of the JavaScript features that are new to ES2015. These include things like lets and consts, arrow functions, default function parameters, and destructuring assignments.

你能做的另一件事就是把型別安全新增到程式碼中。這實際上已經部分完成了,因為你已經安裝了 AngularJS 的型別定義。 TypeScript 會幫你檢查是否正確呼叫了 AngularJS 的 API,—— 比如往 Angular 模組中註冊元件。

Another thing you can do is start adding type safety to your code. This has actually partially already happened because of the AngularJS typings you installed. TypeScript are checking that you are calling AngularJS APIs correctly when you do things like register components to Angular modules.

你還能開始把型別註解新增到自己的程式碼中,來從 TypeScript 的型別系統中獲得更多幫助。 比如,你可以給 checkmark 過濾器加上註解,表明它期待一個 boolean 型別的引數。 這可以更清楚的表明此過濾器打算做什麼

But you can also start adding type annotations to get even more out of TypeScript's type system. For instance, you can annotate the checkmark filter so that it explicitly expects booleans as arguments. This makes it clearer what the filter is supposed to do.

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

Phone 服務中,你可以明確的把 $resource 服務宣告為 angular.resource.IResourceService,一個 AngularJS 型別定義提供的型別。

In the Phone service, you can explicitly annotate the $resource service dependency as an angular.resource.IResourceService - a type defined by the AngularJS typings.

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

你可以在應用的路由配置中使用同樣的技巧,那裡你用到了 location 和 route 服務。 一旦為它們提供了型別資訊,TypeScript 就能檢查你是否在用型別的正確引數來呼叫它們了。

You can apply the same trick to the application's route configuration file in app.config.ts, where you are using the location and route services. By annotating them accordingly TypeScript can verify you're calling their APIs with the correct kinds of arguments.

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'); } ]);
app/app.config.ts
      
      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');
    }
  ]);
    

你用安裝的這個AngularJS.x 型別定義檔案 並不是由 Angular 開發組維護的,但它也已經足夠全面了。藉助這些型別定義的幫助,它可以為 AngularJS.x 程式加上全面的型別註解。

The AngularJS 1.x type definitions you installed are not officially maintained by the Angular team, but are quite comprehensive. It is possible to make an AngularJS 1.x application fully type-annotated with the help of these definitions.

如果你想這麼做,就在 tsconfig.json 中啟用 noImplicitAny 配置項。 這樣,如果遇到什麼還沒有型別註解的程式碼,TypeScript 編譯器就會顯示一個警告。 你可以用它作為指南,告訴你現在與一個完全型別化的專案距離還有多遠。

If this is something you wanted to do, it would be a good idea to enable the noImplicitAny configuration option in tsconfig.json. This would cause the TypeScript compiler to display a warning when there's any code that does not yet have type annotations. You could use it as a guide to inform us about how close you are to having a fully annotated project.

你能用的另一個 TypeScript 特性是類別。具體來講,你可以把控制器轉換成類別。 這種方式下,你離成為 Angular 元件類別就又近了一步,它會令你的升級之路變得更簡單。

Another TypeScript feature you can make use of is classes. In particular, you can turn component controllers into classes. That way they'll be a step closer to becoming Angular component classes, which will make life easier once you upgrade.

AngularJS 期望控制器是一個建構函式。這實際上就是 ES2015/TypeScript 中的類別, 這也就意味著只要你把一個類別註冊為元件控制器,AngularJS 就會愉快的使用它。

AngularJS expects controllers to be constructor functions. That's exactly what ES2015/TypeScript classes are under the hood, so that means you can just plug in a class as a component controller and AngularJS will happily use it.

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

Here's what the new class for the phone list component controller looks like:

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 });
app/phone-list/phone-list.component.ts
      
      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
  });
    

以前在控制器函式中實現的一切,現在都改由類別的建構函式來實現了。型別注入註解透過靜態屬性 $inject 被附加到了類別上。在執行時,它們變成了 PhoneListController.$inject

What was previously done in the controller function is now done in the class constructor function. The dependency injection annotations are attached to the class using a static property $inject. At runtime this becomes the PhoneListController.$inject property.

該類別還聲明瞭另外三個成員:電話列表、當前排序鍵的名字和搜尋條件。 這些東西你以前就加到了控制器上,只是從來沒有在任何地方顯式定義過它們。最後一個成員從未真正在 TypeScript 程式碼中用過, 因為它只是在範本中被參考過。但為了清晰起見,你還是應該定義出此控制器應有的所有成員。

The class additionally declares three members: The array of phones, the name of the current sort key, and the search query. These are all things you have already been attaching to the controller but that weren't explicitly declared anywhere. The last one of these isn't actually used in the TypeScript code since it's only referred to in the template, but for the sake of clarity you should define all of the controller members.

在電話詳情控制器中,你有兩個成員:一個是使用者正在檢視的電話,另一個是正在顯示的影象:

In the Phone detail controller, you'll have two members: One for the phone that the user is looking at and another for the URL of the currently displayed image:

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 });
app/phone-detail/phone-detail.component.ts
      
      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
  });
    

這已經讓你的控制器程式碼看起來更像 Angular 了。你的準備工作做好了,可以引進 Angular 到專案中了。

This makes the controller code look a lot more like Angular already. You're all set to actually introduce Angular into the project.

如果專案中有任何 AngularJS 的服務,它們也是轉換成類別的優秀候選人,像控制器一樣,它們也是建構函式。 但是在本專案中,你只有一個 Phone 工廠,這有點特別,因為它是一個 ngResource 工廠。 所以你不會在準備階段中處理它,而是在下一節中直接把它轉換成 Angular 服務。

If you had any AngularJS services in the project, those would also be a good candidate for converting to classes, since like controllers, they're also constructor functions. But you only have the Phone factory in this project, and that's a bit special since it's an ngResource factory. So you won't be doing anything to it in the preparation stage. You'll instead turn it directly into an Angular service.

安裝 Angular

Installing Angular

準備工作做完了,接下來就開始把 PhoneCat 升級到 Angular。 你將在 Angular升級模組的幫助下增量式的完成此項工作。 做完這些之後,就能把 AngularJS 從專案中完全移除了,但其中的關鍵是在不破壞此程式的前提下一小塊一小塊的完成它。

Having completed the preparation work, get going with the Angular upgrade of PhoneCat. You'll do this incrementally with the help of ngUpgrade that comes with Angular. By the time you're done, you'll be able to remove AngularJS from the project completely, but the key is to do this piece by piece without breaking the application.

該專案還包含一些動畫,在此指南的當前版本你先不升級它,請到 Angular 動畫中進一步學習。

The project also contains some animations. You won't upgrade them in this version of the guide. Turn to the Angular animations guide to learn about that.

用 SystemJS 模組載入器把 Angular 安裝到專案中。 看看升級的準備工作中的指南,並從那裡獲得如下配置:

Install Angular into the project, along with the SystemJS module loader. Take a look at the results of the upgrade setup instructions and get the following configurations from there:

  • 把 Angular 和其它新依賴新增到 package.json

    Add Angular and the other new dependencies to package.json

  • 把 SystemJS 的配置檔案 systemjs.config.js 新增到專案的根目錄。

    The SystemJS configuration file systemjs.config.js to the project root directory.

這些完成之後,就執行:

Once these are done, run:

npm install
      
      npm install
    

很快你就可以透過 index.html 來把 Angular 的依賴快速載入到應用中, 但首先,你得做一些目錄結構調整。這是因為你正準備從 node_modules 中載入檔案,然而目前專案中的每一個檔案都是從 /app 目錄下載入的。

Soon you can load Angular dependencies into the application via index.html, but first you need to do some directory path adjustments. You'll need to load files from node_modules and the project root instead of from the /app directory as you've been doing to this point.

app/index.html 移入專案的根目錄,然後把 package.json 中的開發伺服器根目錄也指向專案的根目錄,而不再是 app 目錄:

Move the app/index.html file to the project root directory. Then change the development server root path in package.json to also point to the project root instead of app:

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

現在,你就能把專案根目錄下的每一樣東西發給瀏覽器了。但你不想為了適應開發環境中的設定,被迫修改應用程式碼中用到的所有圖片和資料的路徑。因此,你要往 index.html 中新增一個 <base> 標籤,它將導致各種相對路徑被解析回 /app 目錄:

Now you're able to serve everything from the project root to the web browser. But you do not want to have to change all the image and data paths used in the application code to match the development setup. For that reason, you'll add a <base> tag to index.html, which will cause relative URLs to be resolved back to the /app directory:

<base href="/app/">
index.html
      
      <base href="/app/">
    

現在你可以透過 SystemJS 載入 Angular 了。你還要把 Angular 的Polyfill指令碼(polyfills) 和 SystemJS 的配置加到 <head> 區的末尾,然後,你能就用 System.import 來載入實際的應用了:

Now you can load Angular via SystemJS. You'll add the Angular polyfills and the SystemJS config to the end of the <head> section, and then you'll use System.import to load the actual application:

<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>
index.html
      
      <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>
    

你還需要對升級的準備工作期間安裝的 systemjs.config.js 檔案做一些調整。

You also need to make a couple of adjustments to the systemjs.config.js file installed during upgrade setup.

在 SystemJS 載入期間為瀏覽器指出專案的根在哪裡,而不再使用 <base> URL。

Point the browser to the project root when loading things through SystemJS, instead of using the <base> URL.

再透過 npm install @angular/upgrade --save 安裝 upgrade 套件,並為 @angular/upgrade/static 包新增一個對映。

Install the upgrade package via npm install @angular/upgrade --save and add a mapping for the @angular/upgrade/static package.

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/bundles/upgrade-static.umd.js', /* . . . */ },
systemjs.config.js
      
      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/bundles/upgrade-static.umd.js',
/* . . . */
    },
    

建立 AppModule

Creating the AppModule

現在,建立一個名叫 AppModule 的根 NgModule 類別。 這裡已經有了一個名叫 app.module.ts 的檔案,其中存放著 AngularJS 的模組。 把它改名為 app.module.ng1.ts,同時也要在 index.html 中修改對應的指令碼名。 檔案的內容保留:

Now create the root NgModule class called AppModule. There is already a file named app.module.ts that holds the AngularJS module. Rename it to app.module.ajs.ts and update the corresponding script name in the index.html as well. The file contents remain:

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

然後建立一個新的 app.module.ts 檔案,其中是一個最小化的 NgModule 類別:

Now create a new app.module.ts with the minimum NgModule class:

import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; @NgModule({ imports: [ BrowserModule, ], }) export class AppModule { }
app.module.ts
      
      import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

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

引導 PhoneCat 的混合式應用

Bootstrapping a hybrid PhoneCat

接下來,你把該應用程式引導改裝為一個同時支援 AngularJS 和 Angular 的混合式應用。 然後,就能開始把這些不可分割的小塊轉換到 Angular 了。

Next, you'll bootstrap the application as a hybrid application that supports both AngularJS and Angular components. After that, you can start converting the individual pieces to Angular.

本應用現在是使用宿主頁面中附加到 <html> 元素上的 AngularJS 指令 ng-app 引導的。 但在混合式應用中,不能再這麼用了。你得用ngUpgrade bootstrap方法代替。

The application is currently bootstrapped using the AngularJS ng-app directive attached to the <html> element of the host page. This will no longer work in the hybrid app. Switch to the ngUpgrade bootstrap method instead.

首先,從 index.html 中移除 ng-app。然後在 AppModule 中匯入 UpgradeModule,並改寫它的 ngDoBootstrap 方法:

First, remove the ng-app attribute from index.html. Then import UpgradeModule in the AppModule, and override its ngDoBootstrap method:

import { UpgradeModule } from '@angular/upgrade/static'; @NgModule({ imports: [ BrowserModule, UpgradeModule, ], }) export class AppModule { constructor(private upgrade: UpgradeModule) { } ngDoBootstrap() { this.upgrade.bootstrap(document.documentElement, ['phonecatApp']); } }
app/app.module.ts
      
      import { UpgradeModule } from '@angular/upgrade/static';

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

注意,你正在從內部的 ngDoBootstrap 中引導 AngularJS 模組。 它的引數和你在手動引導 AngularJS 時傳給 angular.bootstrap 的是一樣的:應用的根元素,和所要載入的 AngularJS 1.x 模組的陣列。

Note that you are bootstrapping the AngularJS module from inside ngDoBootstrap. The arguments are the same as you would pass to angular.bootstrap if you were manually bootstrapping AngularJS: the root element of the application; and an array of the AngularJS 1.x modules that you want to load.

最後,在 app/main.ts 中引導這個 AppModule。該檔案在 systemjs.config.js 中被配置為了應用的入口,所以它已經被載入進了瀏覽器中。

Finally, bootstrap the AppModule in app/main.ts. This file has been configured as the application entrypoint in systemjs.config.js, so it is already being loaded by the browser.

import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { AppModule } from './app.module'; platformBrowserDynamic().bootstrapModule(AppModule);
app/main.ts
      
      import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app.module';

platformBrowserDynamic().bootstrapModule(AppModule);
    

現在,你同時執行著 AngularJS 和 Angular。漂亮!不過你還沒有執行什麼實際的 Angular 元件,這就是接下來要做的。

Now you're running both AngularJS and Angular at the same time. That's pretty exciting! You're not running any actual Angular components yet. That's next.

為何要宣告 angularangular.IAngularStatic

Why declare angular as angular.IAngularStatic?

@types/angular 宣告為 UMD 模組,根據UMD 型別 的工作方式,一旦你在檔案中有一條 ES6 的 import 語句,所有的 UMD 型別化的模型必須都透過 import 語句匯入, 而是不是全域性可用。

@types/angular is declared as a UMD module, and due to the way UMD typings work, once you have an ES6 import statement in a file all UMD typed modules must also be imported via import statements instead of being globally available.

AngularJS 是日前是透過 index.html 中的 script 標籤載入,這意味著整個應用是作為一個全域性變數進行訪問的, 使用同一個 angular 變數的實例。 但如果你使用 import * as angular from 'angular',我還需要徹底修改 AngularJS 應用中載入每個檔案的方式, 確保 AngularJS 應用被正確載入。

AngularJS is currently loaded by a script tag in index.html, which means that the whole app has access to it as a global and uses the same instance of the angular variable. If you used import * as angular from 'angular' instead, you'd also have to load every file in the AngularJS app to use ES2015 modules in order to ensure AngularJS was being loaded correctly.

這需要相當多的努力,通常也不值得去做,特別是當你正在朝著 Angular 前進時。 但如果你把 angular 宣告為 angular.IAngularStatic,指明它是一個全域性變數, 仍然可以獲得全面的型別支援。

This is a considerable effort and it often isn't worth it, especially since you are in the process of moving your code to Angular. Instead, declare angular as angular.IAngularStatic to indicate it is a global variable and still have full typing support.

升級 Phone 服務

Upgrading the Phone service

你要移植到 Angular 的第一個片段是 Phone 工廠(位於 app/js/core/phones.factory.ts), 並且讓它能幫助控制器從伺服器上載入電話資訊。目前,它是用 ngResource 實現的,你用它做兩件事:

The first piece you'll port over to Angular is the Phone service, which resides in app/core/phone/phone.service.ts and makes it possible for components to load phone information from the server. Right now it's implemented with ngResource and you're using it for two things:

  • 把所有電話的列表載入到電話列表元件中。

    For loading the list of all phones into the phone list component.

  • 把一臺電話的詳情載入到電話詳情元件中。

    For loading the details of a single phone into the phone detail component.

你可以用 Angular 的服務類別來替換這個實現,而把控制器繼續留在 AngularJS 的地盤上。

You can replace this implementation with an Angular service class, while keeping the controllers in AngularJS land.

在這個新版本中,你匯入了 Angular 的 HTTP 模組,並且用它的 HttpClient 服務替換掉 ngResource

In the new version, you import the Angular HTTP module and call its HttpClient service instead of ngResource.

再次開啟 app.module.ts 檔案,匯入並把 HttpClientModule 新增到 AppModuleimports 陣列中:

Re-open the app.module.ts file, import and add HttpClientModule to the imports array of the AppModule:

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']); } }
app.module.ts
      
      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']);
  }
}
    

現在,你已經準備好了升級 Phones 服務本身。你將為 phone.service.ts 檔案中基於 ngResource 的服務加上 @Injectable 裝飾器:

Now you're ready to upgrade the Phone service itself. Replace the ngResource-based service in phone.service.ts with a TypeScript class decorated as @Injectable:

@Injectable() export class Phone { /* . . . */ }
app/core/phone/phone.service.ts (skeleton)
      
      @Injectable()
export class Phone {
/* . . . */
}
    

@Injectable 裝飾器將把一些依賴注入相關的元資料附加到該類別上,讓 Angular 知道它的依賴資訊。 就像在依賴注入指南中描述過的那樣, 這是一個標記裝飾器,你要把它用在那些沒有其它 Angular 裝飾器,並且自己有依賴注入的類別上。

The @Injectable decorator will attach some dependency injection metadata to the class, letting Angular know about its dependencies. As described by the Dependency Injection Guide, this is a marker decorator you need to use for classes that have no other Angular decorators but still need to have their dependencies injected.

在它的建構函式中,該類別期待一個 HttpClient 服務。HttpClient 服務將被注入進來並存入一個私有欄位。 然後該服務在兩個實例方法中被使用到,一個載入所有電話的列表,另一個載入一臺指定電話的詳情:

In its constructor the class expects to get the HttpClient service. It will be injected to it and it is stored as a private field. The service is then used in the two instance methods, one of which loads the list of all phones, and the other loads the details of a specified phone:

@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`); } }
app/core/phone/phone.service.ts
      
      @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`);
  }
}
    

該方法現在返回一個 Phone 型別或 Phone[] 型別的可觀察物件(Observable)。 這是一個你從未用過的型別,因此你得為它新增一個簡單的介面:

The methods now return observables of type PhoneData and PhoneData[]. This is a type you don't have yet. Add a simple interface for it:

export interface PhoneData { name: string; snippet: string; images: string[]; }
app/core/phone/phone.service.ts (interface)
      
      export interface PhoneData {
  name: string;
  snippet: string;
  images: string[];
}
    

@angular/upgrade/static 有一個 downgradeInjectable 方法,可以使 Angular 服務在 AngularJS 的程式碼中可用。 使用它來插入 Phone 服務:

@angular/upgrade/static has a downgradeInjectable method for the purpose of making Angular services available to AngularJS code. Use it to plug in the Phone service:

declare var angular: angular.IAngularStatic; import { downgradeInjectable } from '@angular/upgrade/static'; /* . . . */ @Injectable() export class Phone { /* . . . */ } angular.module('core.phone') .factory('phone', downgradeInjectable(Phone));
app/core/phone/phone.service.ts (downgrade)
      
      declare var angular: angular.IAngularStatic;
import { downgradeInjectable } from '@angular/upgrade/static';
/* . . . */
@Injectable()
export class Phone {
/* . . . */
}

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

最終,該類別的全部程式碼如下:

Here's the full, final code for the service:

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));
app/core/phone/phone.service.ts
      
      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));
    

注意,你要單獨匯入了 RxJS Observable 中的 map 運算子。 對每個 RxJS 運算子都要這麼做。

Notice that you're importing the map operator of the RxJS Observable separately. Do this for every RxJS operator.

這個新的 Phone 服務具有和老的基於 ngResource 的服務相同的特性。 因為它是 Angular 服務,你透過 NgModuleproviders 陣列來註冊它:

The new Phone service has the same features as the original, ngResource-based service. Because it's an Angular service, you register it with the NgModule providers:

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']); } }
app.module.ts
      
      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']);
  }
}
    

現在,你正在用 SystemJS 載入 phone.service.ts,你應該從 index.html移除該服務的 <script> 標籤。 這也是你在升級所有元件時將會做的事。在從 AngularJS 向 Angular 升級的同時,你也把程式碼從指令碼移植為模組。

Now that you are loading phone.service.ts through an import that is resolved by SystemJS, you should remove the <script> tag for the service from index.html. This is something you'll do to all components as you upgrade them. Simultaneously with the AngularJS to Angular upgrade you're also migrating code from scripts to modules.

這時,你可以把兩個控制器從使用老的服務切換成使用新的。你像降級過的 phones 工廠一樣 $inject 它, 但它實際上是一個 Phones 類別的實例,並且你可以據此註解它的型別:

At this point, you can switch the two components to use the new service instead of the old one. While you $inject it as the downgraded phone factory, it's really an instance of the Phone class and you annotate its type accordingly:

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 });
app/phone-list/phone-list.component.ts
      
      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 });
app/phone-detail/phone-detail.component.ts
      
      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
  });
    

這裡的兩個 AngularJS 控制器在使用 Angular 的服務!控制器不需要關心這一點,儘管實際上該服務返回的是可觀察物件(Observable),而不是承諾(Promise)。 無論如何,你達到的效果都是把服務移植到 Angular,而不用被迫移植元件來使用它。

Now there are two AngularJS components using an Angular service! The components don't need to be aware of this, though the fact that the service returns observables and not promises is a bit of a giveaway. In any case, what you've achieved is a migration of a service to Angular without having to yet migrate the components that use it.

你也能使用 ObservabletoPromise 方法來在服務中把這些可觀察物件轉變成承諾,以進一步減小元件控制器中需要修改的程式碼量。

You could use the toPromise method of Observable to turn those observables into promises in the service. In many cases that reduce the number of changes to the component controllers.

升級元件

Upgrading Components

接下來,把 AngularJS 的控制器升級成 Angular 的元件。每次升級一個,同時仍然保持應用執行在混合模式下。 在做轉換的同時,你還將自訂首個 Angular管道

Upgrade the AngularJS components to Angular components next. Do it one component at a time while still keeping the application in hybrid mode. As you make these conversions, you'll also define your first Angular pipes.

先看看電話列表元件。它目前包含一個 TypeScript 控制器類別和一個元件定義物件。重新命名控制器類別, 並把 AngularJS 的元件定義物件更換為 Angular @Component 裝飾器,這樣你就把它變形為 Angular 的元件了。然後,你還要從類別中移除靜態 $inject 屬性。

Look at the phone list component first. Right now it contains a TypeScript controller class and a component definition object. You can morph this into an Angular component by just renaming the controller class and turning the AngularJS component definition object into an Angular @Component decorator. You can then also remove the static $inject property from the class:

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'; } /* . . . */ }
app/phone-list/phone-list.component.ts
      
      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 屬性是一個 CSS 選擇器,用來定義元件應該被放在頁面的哪。在 AngularJS 中,你會基於元件名字來匹配, 但是在 Angular 中,你要顯式指定這些選擇器。本元件將會對應元素名字 phone-list,和 AngularJS 版本一樣。

The selector attribute is a CSS selector that defines where on the page the component should go. In AngularJS you do matching based on component names, but in Angular you have these explicit selectors. This one will match elements with the name phone-list, just like the AngularJS version did.

現在,將元件的模版也轉換為 Angular 語法。在搜尋控制元件中,把 AngularJS 的 $ctrl 表示式替換成 Angular 的雙向繫結語法 [(ngModel)]

Now convert the template of this component into Angular syntax. The search controls replace the AngularJS $ctrl expressions with Angular's two-way [(ngModel)] binding syntax:

<p> Search: <input [(ngModel)]="query" /> </p> <p> Sort by: <select [(ngModel)]="orderProp"> <option value="name">Alphabetical</option> <option value="age">Newest</option> </select> </p>
app/phone-list/phone-list.template.html (search controls)
      
      <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 標籤的 ng-src 替換為一個標準的 src 屬性(property)繫結。

Replace the list's ng-repeat with an *ngFor as described in the Template Syntax page. Replace the image tag's ng-src with a binding to the native 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>
app/phone-list/phone-list.template.html (phones)
      
      <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 中沒有 filterorderBy 過濾器

No Angular filter or orderBy filters

Angular 中並不存在 AngularJS 中內建的 filterorderBy 過濾器。 所以你得自己實現進行過濾和排序。

The built-in AngularJS filter and orderBy filters do not exist in Angular, so you need to do the filtering and sorting yourself.

你把 filterorderBy 過濾器改成繫結到控制器中的 getPhones() 方法,透過該方法,元件本身實現了過濾和排序邏輯。

You replaced the filter and orderBy filters with bindings to the getPhones() controller method, which implements the filtering and ordering logic inside the component itself.

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; }
app/phone-list/phone-list.component.ts
      
      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;
}
    

現在你需要降級你的 Angular 元件,這樣你就可以在 AngularJS 中使用它了。 你要註冊一個 phoneList指令,而不是註冊一個元件,它是一個降級版的 Angular 元件。

Now you need to downgrade the Angular component so you can use it in AngularJS. Instead of registering a component, you register a phoneList directive, a downgraded version of the Angular component.

強制型別轉換 as angular.IDirectiveFactory 告訴 TypeScript 編譯器 downgradeComponent 方法 的返回值是一個指令工廠。

The as angular.IDirectiveFactory cast tells the TypeScript compiler that the return value of the downgradeComponent method is a directive factory.

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 );
app/phone-list/phone-list.component.ts
      
      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 新增到 NgModuleimports 中,並宣告新的 PhoneListComponent 元件, 最後,把降級的結果新增到 entryComponents 中:

The new PhoneListComponent uses the Angular ngModel directive, located in the FormsModule. Add the FormsModule to NgModule imports, declare the new PhoneListComponent and finally add it to entryComponents since you downgraded it:

import { FormsModule } from '@angular/forms'; import { PhoneListComponent } from './phone-list/phone-list.component'; @NgModule({ imports: [ BrowserModule, UpgradeModule, HttpClientModule, FormsModule, ], declarations: [ PhoneListComponent, ], entryComponents: [ PhoneListComponent, }) export class AppModule { constructor(private upgrade: UpgradeModule) { } ngDoBootstrap() { this.upgrade.bootstrap(document.documentElement, ['phonecatApp']); } }
app.module.ts
      
      import { FormsModule } from '@angular/forms';
import { PhoneListComponent } from './phone-list/phone-list.component';

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

index.html 中移除電話列表元件的<script>標籤。

Remove the <script> tag for the phone list component from index.html.

現在,剩下的 phone-detail.component.ts 檔案變成了這樣:

Now set the remaining phone-detail.component.ts as follows:

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 );
app/phone-detail/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 裝飾器,它標記出了 $routeParams 依賴。

This is similar to the phone list component. The new wrinkle is the RouteParams type annotation that identifies the routeParams dependency.

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

The AngularJS injector has an AngularJS router dependency called $routeParams, which was injected into PhoneDetails when it was still an AngularJS controller. You intend to inject it into the new PhoneDetailsComponent.

不幸的是,AngularJS 的依賴不會自動在 Angular 的元件中可用。 你必須使用工廠提供者(factory provider) 來把 $routeParams 包裝成 Angular 的服務提供者。 新建一個名叫 ajs-upgraded-providers.ts 的檔案,並且在 app.module.ts 中匯入它:

Unfortunately, AngularJS dependencies are not automatically available to Angular components. You must upgrade this service via a factory provider to make $routeParams an Angular injectable. Do that in a new file called ajs-upgraded-providers.ts and import it in app.module.ts:

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'] };
app/ajs-upgraded-providers.ts
      
      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 ]
app/app.module.ts ($routeParams)
      
      import { routeParamsProvider } from './ajs-upgraded-providers';
  providers: [
    Phone,
    routeParamsProvider
  ]
    

把該元件的範本轉變成 Angular 的語法,程式碼如下:

Convert the phone detail component template into Angular syntax as follows:

<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>
app/phone-detail/phone-detail.template.html
      
      <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>
    

這裡有幾個值得注意的改動:

There are several notable changes here:

  • 你從所有表示式中移除了 $ctrl. 字首。

    You've removed the $ctrl. prefix from all expressions.

  • 正如你在電話列表中做過的那樣,你把 ng-src 替換成了標準的 src 屬性繫結。

    You've replaced ng-src with property bindings for the standard src property.

  • 你在 ng-class 周圍使用了屬性繫結語法。雖然 Angular 中有一個 和 AngularJS 中非常相似的 ngClass指令, 但是它的值不會神奇的作為表示式進行計算。在 Angular 中,範本中的屬性(Attribute)值總是被作為 屬性(Property)表示式計算,而不是作為字串字面量。

    You're using the property binding syntax around ng-class. Though Angular does have a very similar ngClassas AngularJS does, its value is not magically evaluated as an expression. In Angular, you always specify in the template when an attribute's value is a property expression, as opposed to a literal string.

  • 你把 ng-repeat 替換成了 *ngFor

    You've replaced ng-repeats with *ngFors.

  • 你把 ng-click 替換成了一個到標準 click 事件的繫結。

    You've replaced ng-click with an event binding for the standard click.

  • 你把整個範本都包裹進了一個 ngIf 中,這導致只有當存在一個電話時它才會渲染。你必須這麼做, 是因為元件首次載入時你還沒有 phone 變數,這些表示式就會參考到一個不存在的值。 和 AngularJS 不同,當你嘗試參考未定義物件上的屬性時,Angular 中的表示式不會默默失敗。 你必須明確指出這種情況是你所期望的。

    You've wrapped the whole template in an ngIf that causes it only to be rendered when there is a phone present. You need this because when the component first loads, you don't have phone yet and the expressions will refer to a non-existing value. Unlike in AngularJS, Angular expressions do not fail silently when you try to refer to properties on undefined objects. You need to be explicit about cases where this is expected.

PhoneDetailComponent 元件新增到 NgModuledeclarationsentryComponents 中:

Add PhoneDetailComponent component to the NgModule declarations and entryComponents:

import { PhoneDetailComponent } from './phone-detail/phone-detail.component'; @NgModule({ imports: [ BrowserModule, UpgradeModule, HttpClientModule, FormsModule, ], declarations: [ PhoneListComponent, PhoneDetailComponent, ], entryComponents: [ PhoneListComponent, PhoneDetailComponent ], providers: [ Phone, routeParamsProvider ] }) export class AppModule { constructor(private upgrade: UpgradeModule) { } ngDoBootstrap() { this.upgrade.bootstrap(document.documentElement, ['phonecatApp']); } }
app.module.ts
      
      import { PhoneDetailComponent } from './phone-detail/phone-detail.component';

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

你現在應該從 index.html 中移除電話詳情元件的<script>。

You should now also remove the phone detail component <script> tag from index.html.

新增 CheckmarkPipe

Add the CheckmarkPipe

AngularJS 指令中有一個 checkmark過濾器,把它轉換成 Angular 的管道

The AngularJS directive had a checkmark filter. Turn that into an Angular pipe.

沒有什麼升級方法能把過濾器轉換成管道。 但你也並不需要它。 把過濾器函式轉換成等價的 Pipe 類別非常簡單。 實現方式和以前一樣,但把它們包裝進 transform 方法中就可以了。 把該檔案改名成 checkmark.pipe.ts,以符合 Angular 中的命名約定:

There is no upgrade method to convert filters into pipes. You won't miss it. It's easy to turn the filter function into an equivalent Pipe class. The implementation is the same as before, repackaged in the transform method. Rename the file to checkmark.pipe.ts to conform with Angular conventions:

import { Pipe, PipeTransform } from '@angular/core'; @Pipe({name: 'checkmark'}) export class CheckmarkPipe implements PipeTransform { transform(input: boolean) { return input ? '\u2713' : '\u2718'; } }
app/core/checkmark/checkmark.pipe.ts
      
      import { Pipe, PipeTransform } from '@angular/core';

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

現在,匯入並宣告這個新建立的管道,同時從 index.html 檔案中移除該過濾器的 <script> 標籤:

Now import and declare the newly created pipe and remove the filter <script> tag from index.html:

import { CheckmarkPipe } from './core/checkmark/checkmark.pipe'; @NgModule({ imports: [ BrowserModule, UpgradeModule, HttpClientModule, FormsModule, ], declarations: [ PhoneListComponent, PhoneDetailComponent, CheckmarkPipe ], entryComponents: [ PhoneListComponent, PhoneDetailComponent ], providers: [ Phone, routeParamsProvider ] }) export class AppModule { constructor(private upgrade: UpgradeModule) { } ngDoBootstrap() { this.upgrade.bootstrap(document.documentElement, ['phonecatApp']); } }
app.module.ts
      
      import { CheckmarkPipe } from './core/checkmark/checkmark.pipe';

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

對混合式應用做 AOT 編譯

AOT compile the hybrid app

要在混合式應用中使用 AOT 編譯,你首先要像其它 Angular 應用一樣設定它,就像AOT 編譯一章所講的那樣。

To use AOT with a hybrid app, you have to first set it up like any other Angular application, as shown in the Ahead-of-time Compilation chapter.

然後修改 main-aot.ts 的引導程式碼,來引導 AOT 編譯器所產生的 AppComponentFactory

Then change main-aot.ts to bootstrap the AppComponentFactory that was generated by the AOT compiler:

import { platformBrowser } from '@angular/platform-browser'; import { AppModuleNgFactory } from './app.module.ngfactory'; platformBrowser().bootstrapModuleFactory(AppModuleNgFactory);
app/main-aot.ts
      
      import { platformBrowser } from '@angular/platform-browser';

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

platformBrowser().bootstrapModuleFactory(AppModuleNgFactory);
    

你還要把在 index.html 中已經用到的所有 AngularJS 檔案載入到 aot/index.html 中:

You need to load all the AngularJS files you already use in index.html in aot/index.html as well:

<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <base href="/app/"> <title>Google Phone Gallery</title> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" /> <link rel="stylesheet" href="app.css" /> <link rel="stylesheet" href="app.animations.css" /> <script src="https://code.jquery.com/jquery-2.2.4.js"></script> <script src="https://code.angularjs.org/1.5.5/angular.js"></script> <script src="https://code.angularjs.org/1.5.5/angular-animate.js"></script> <script src="https://code.angularjs.org/1.5.5/angular-resource.js"></script> <script src="https://code.angularjs.org/1.5.5/angular-route.js"></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>
aot/index.html
      
      <!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">

    <base href="/app/">

    <title>Google Phone Gallery</title>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" />
    <link rel="stylesheet" href="app.css" />
    <link rel="stylesheet" href="app.animations.css" />

    <script src="https://code.jquery.com/jquery-2.2.4.js"></script>
    <script src="https://code.angularjs.org/1.5.5/angular.js"></script>
    <script src="https://code.angularjs.org/1.5.5/angular-animate.js"></script>
    <script src="https://code.angularjs.org/1.5.5/angular-resource.js"></script>
    <script src="https://code.angularjs.org/1.5.5/angular-route.js"></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>
    

這些檔案要帶著相應的Polyfill指令碼複製到一起。應用執行時需要的檔案,比如電話列表 .json 和圖片,也需要複製過去。

These files need to be copied together with the polyfills. The files the application needs at runtime, like the .json phone lists and images, also need to be copied.

透過 npm install fs-extra --save-dev 安裝 fs-extra 可以更好的複製檔案,並且把 copy-dist-files.js 檔案改成這樣:

Install fs-extra via npm install fs-extra --save-dev for better file copying, and change copy-dist-files.js to the following:

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); });
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);
});
    

這就是想要在升級應用期間 AOT 編譯所需的一切!

And that's all you need to use AOT while upgrading your app!

新增 Angular 路由器和載入程式

Adding The Angular Router And Bootstrap

此刻,你已經把所有 AngularJS 的元件替換成了它們在 Angular 中的等價物,不過你仍然在 AngularJS 路由器中使用它們。

At this point, you've replaced all AngularJS application components with their Angular counterparts, even though you're still serving them from the AngularJS router.

新增 Angular 路由器

Add the Angular router

Angular 有一個全新的路由器

Angular has an all-new router.

像所有的路由器一樣,它需要在 UI 中指定一個位置來顯示路由的檢視。 在 Angular 中,它是 <router-outlet>,並位於應用元件樹頂部的根元件中。

Like all routers, it needs a place in the UI to display routed views. For Angular that's the <router-outlet> and it belongs in a root component at the top of the applications component tree.

你還沒有這樣一個根元件,因為該應用仍然是像一個 AngularJS 應用那樣被管理的。 建立新的 app.component.ts 檔案,放入像這樣的 AppComponent 類別:

You don't yet have such a root component, because the app is still managed as an AngularJS app. Create a new app.component.ts file with the following AppComponent class:

import { Component } from '@angular/core'; @Component({ selector: 'phonecat-app', template: '<router-outlet></router-outlet>' }) export class AppComponent { }
app/app.component.ts
      
      import { Component } from '@angular/core';

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

它有一個很簡單的範本,只包含 Angular 路由的 <router-outlet>。 該元件只負責渲染活動路由的內容,此外啥也不幹。

It has a simple template that only includes the <router-outlet>. This component just renders the contents of the active route and nothing else.

該選擇器告訴 Angular:當應用啟動時就把這個根元件插入到宿主頁面的 <phonecat-app> 元素中。

The selector tells Angular to plug this root component into the <phonecat-app> element on the host web page when the application launches.

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

Add this <phonecat-app> element to the index.html. It replaces the old AngularJS ng-view directive:

<body> <phonecat-app></phonecat-app> </body>
index.html (body)
      
      <body>
  <phonecat-app></phonecat-app>
</body>
    

建立路由模組

Create the Routing Module

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

A router needs configuration whether it's the AngularJS or Angular or any other router.

Angular 路由器配置的詳情最好去查閱下路由與導航文件。 它建議你建立一個專們用於路由器配置的 NgModule(名叫路由模組)。

The details of Angular router configuration are best left to the Routing documentation which recommends that you create a NgModule dedicated to router configuration (called a Routing Module).

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 { }
app/app-routing.module.ts
      
      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 { }
    

該模組定義了一個 routes 物件,它帶有兩個路由,分別指向兩個電話元件,以及為空路徑指定的預設路由。 它把 routes 傳給 RouterModule.forRoot 方法,該方法會完成剩下的事。

This module defines a routes object with two routes to the two phone components and a default route for the empty path. It passes the routes to the RouterModule.forRoot method which does the rest.

一些額外的提供者讓路由器使用“hash”策略解析 URL,比如 #!/phones,而不是預設的“Push State”策略。

A couple of extra providers enable routing with "hash" URLs such as #!/phones instead of the default "push state" strategy.

現在,修改 AppModule,讓它匯入這個 AppRoutingModule,並同時宣告根元件 AppComponent。 這會告訴 Angular,它應該使用根元件 AppComponent 引導應用,並把它的檢視插入到宿主頁面中。

Now update the AppModule to import this AppRoutingModule and also the declare the root AppComponent as the bootstrap component. That tells Angular that it should bootstrap the app with the root AppComponent and insert its view into the host web page.

你還要從 app.module.ts 中移除呼叫 ngDoBootstrap() 來引導 AngularJS 模組的程式碼,以及對 UpgradeModule 的匯入程式碼。

You must also remove the bootstrap of the AngularJS module from ngDoBootstrap() in app.module.ts and the UpgradeModule import.

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 {}
app/app.module.ts
      
      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 {}
    

而且,由於你現在直接路由到 PhoneListComponentPhoneDetailComponent,而不再使用帶 <phone-list><phone-detail> 標籤的路由範本,因此你同樣不再需要它們的 Angular 選擇器。

And since you are routing to PhoneListComponent and PhoneDetailComponent directly rather than using a route template with a <phone-list> or <phone-detail> tag, you can do away with their Angular selectors as well.

在電話列表中,你不用再被迫硬編碼電話詳情的連結了。 你可以透過把每個電話的 id 繫結到 routerLink 指令來產生它們了,該指令的建構函式會為 PhoneDetailComponent 產生正確的 URL:

You no longer have to hardcode the links to phone details in the phone list. You can generate data bindings for each phone's id to the routerLink directive and let that directive construct the appropriate URL to the PhoneDetailComponent:

<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>
app/phone-list/phone-list.template.html (list with links)
      
      <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>
    

要了解詳情,請檢視路由與導航頁。

See the Routing page for details.


使用路由引數

Use route parameters

Angular 路由器會傳入不同的路由引數。 改正 PhoneDetail 元件的建構函式,讓它改用注入進來的 ActivatedRoute 物件。 從 ActivatedRoute.snapshot.params 中提取出 phoneId,並像以前一樣獲取手機的資料:

The Angular router passes route parameters differently. Correct the PhoneDetail component constructor to expect an injected ActivatedRoute object. Extract the phoneId from the ActivatedRoute.snapshot.params and fetch the phone data as before:

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; } }
app/phone-detail/phone-detail.component.ts
      
      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;
  }
}
    

你現在執行的就是純正的 Angular 應用了!

You are now running a pure Angular application!

再見,AngularJS!

Say Goodbye to AngularJS

終於可以把輔助訓練的輪子摘下來了!讓你的應用作為一個純粹、閃亮的 Angular 程式開始它的新生命吧。 剩下的所有任務就是移除程式碼 —— 這當然是每個程式設計師最喜歡的任務!

It is time to take off the training wheels and let the application begin its new life as a pure, shiny Angular app. The remaining tasks all have to do with removing code - which of course is every programmer's favorite task!

應用仍然以混合式應用的方式啟動,然而這再也沒有必要了。

The application is still bootstrapped as a hybrid app. There's no need for that anymore.

把應用的引導(bootstrap)方法從 UpgradeAdapter 的改為 Angular 的。

Switch the bootstrap method of the application from the UpgradeModule to the Angular way.

import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { AppModule } from './app.module'; platformBrowserDynamic().bootstrapModule(AppModule);
main.ts
      
      import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

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

platformBrowserDynamic().bootstrapModule(AppModule);
    

如果你還沒有這麼做,請從 app.module.ts 刪除所有UpgradeModule 的參考, 以及所有用於 AngularJS 服務的工廠提供者(factory provider)app/ajs-upgraded-providers.ts 檔案。

If you haven't already, remove all references to the UpgradeModule from app.module.ts, as well as any factory provider for AngularJS services, and the app/ajs-upgraded-providers.ts file.

還要刪除所有的 downgradeInjectable()downgradeComponent() 以及與 AngularJS 相關的工廠或指令宣告。 因為你不再需要降級任何元件了,也不再需要把它們列在 entryComponents 中。

Also remove any downgradeInjectable() or downgradeComponent() you find, together with the associated AngularJS factory or directive declarations. Since you no longer have downgraded components, you no longer list them in 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 {}
app.module.ts
      
      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 中不需要了:

You may also completely remove the following files. They are AngularJS module configuration files and not needed in 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 的外部型別定義檔案。你現在只需要留下 Jasmine 和 Angular 所需的Polyfill指令碼。 systemjs.config.js 中的 @angular/upgrade 套件及其對映也可以移除了。

The external typings for AngularJS may be uninstalled as well. The only ones you still need are for Jasmine and Angular polyfills. The @angular/upgrade package and its mapping in systemjs.config.js can also go.

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
      
      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.htmlkarma.conf.js 中,移除所有對 AngularJS 和 jQuery 指令碼的參考。 當這些全部做完時,index.html 應該是這樣的:

Finally, from index.html, remove all references to AngularJS scripts and jQuery. When you're done, this is what it should look like:

<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <base href="/app/"> <title>Google Phone Gallery</title> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" /> <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>
index.html
      
      <!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <base href="/app/">
    <title>Google Phone Gallery</title>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" />
    <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 了!它曾經帶給你很多幫助,不過現在,該說再見了。

That is the last you'll see of AngularJS! It has served us well but now it's time to say goodbye.

附錄:升級 PhoneCat 的測試

Appendix: Upgrading PhoneCat Tests

測試不僅要在升級過程中被保留,它還是確保應用在升級過程中不會被破壞的一個安全指示器。 要達到這個目的,E2E 測試尤其有用。

Tests can not only be retained through an upgrade process, but they can also be used as a valuable safety measure when ensuring that the application does not break during the upgrade. E2E tests are especially useful for this purpose.

E2E 測試

E2E Tests

PhoneCat 專案中同時有基於 Protractor 的 E2E 測試和一些基於 Karma 的單元測試。 對這兩者來說,E2E 測試的轉換要容易得多:根據定義,E2E 測試透過與應用中顯示的這些 UI 元素互動,從外部訪問你的應用來進行測試。 E2E 測試實際上並不關心這些應用中各部件的內部結構。這也意味著,雖然你已經修改了此應用程式, 但是 E2E 測試套件仍然應該能像以前一樣全部透過。因為從使用者的角度來說,你並沒有改變應用的行為。

The PhoneCat project has both E2E Protractor tests and some Karma unit tests in it. Of these two, E2E tests can be dealt with much more easily: By definition, E2E tests access the application from the outside by interacting with the various UI elements the app puts on the screen. E2E tests aren't really that concerned with the internal structure of the application components. That also means that, although you modify the project quite a bit during the upgrade, the E2E test suite should keep passing with just minor modifications. You didn't change how the application behaves from the user's point of view.

在轉成 TypeScript 期間,你不用做什麼就能讓 E2E 測試正常工作。 但是當你想改成按照混合式應用進行引導時,必須做一些修改。

During TypeScript conversion, there is nothing to do to keep E2E tests working. But when you change the bootstrap to that of a Hybrid app, you must make a few changes.

再對 protractor-conf.js 做下列修改,與混合應用同步:

Update the protractor-conf.js to sync with hybrid apps:

ng12Hybrid: true
      
      ng12Hybrid: true
    

當你開始元件和模組升級到 Angular 時,還需要一系列後續的修改。 這是因為 E2E 測試有一些匹配器是 AngularJS 中特有的。對於 PhoneCat 來說,為了讓它能在 Angular 下工作,你得做下列修改:

When you start to upgrade components and their templates to Angular, you'll make more changes because the E2E tests have matchers that are specific to AngularJS. For PhoneCat you need to make the following changes in order to make things work with Angular:

老程式碼

Previous code

新程式碼

New code

說明

Notes

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

by.css('.phones .name')

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

The repeater matcher relies on AngularJS ng-repeat

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

by.css('.phones li')

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

The repeater matcher relies on AngularJS ng-repeat

by.model('$ctrl.query')

by.css('input')

model 匹配器依賴於 AngularJS 中的 ng-model

The model matcher relies on AngularJS ng-model

by.model('$ctrl.orderProp')

by.css('select')

model 匹配器依賴於 AngularJS 中的 ng-model

The model matcher relies on AngularJS ng-model

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

by.css('h1')

binding 匹配器依賴於 AngularJS 的資料繫結

The binding matcher relies on AngularJS data binding

當引導方式從 UpgradeModule 切換到純 Angular 的時,AngularJS 就從頁面中完全消失了。 此時,你需要告訴 Protractor,它不用再找 AngularJS 應用了,而是從頁面中查詢 Angular 應用。 於是在 protractor-conf.js 中做下列修改:

When the bootstrap method is switched from that of UpgradeModule to pure Angular, AngularJS ceases to exist on the page completely. At this point, you need to tell Protractor that it should not be looking for an AngularJS app anymore, but instead it should find Angular apps from the page.

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

Replace the ng12Hybrid previously added with the following in protractor-conf.js:

useAllAngular2AppRoots: true,
      
      useAllAngular2AppRoots: true,
    

同樣,PhoneCat 的測試程式碼中有兩個 Protractor API 呼叫內部使用了 AngularJS 的 $location。該服務沒有了, 你就得把這些呼叫用一個 WebDriver 的通用 URL API 代替。第一個 API 是“重新導向(redirect)”規約:

Also, there are a couple of Protractor API calls in the PhoneCat test code that are using the AngularJS $location service under the hood. As that service is no longer present after the upgrade, replace those calls with ones that use WebDriver's generic URL APIs instead. The first of these is the redirection spec:

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); });
e2e-tests/scenarios.ts
      
      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)”規約:

And the second is the phone links spec:

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); });
e2e-tests/scenarios.ts
      
      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);
});
    

單元測試

Unit Tests

另一方面,對於單元測試來說,需要更多的轉化工作。實際上,它們需要隨著產品程式碼一起升級。

For unit tests, on the other hand, more conversion work is needed. Effectively they need to be upgraded along with the production code.

在轉成 TypeScript 期間,嚴格來講沒有什麼改動是必須的。但把單元測試程式碼轉成 TypeScript 仍然是個好主意, 產品程式碼從 TypeScript 中獲得的那些增益也同樣適用於測試程式碼。

During TypeScript conversion no changes are strictly necessary. But it may be a good idea to convert the unit test code into TypeScript as well.

比如,在這個電話詳情元件的規約中,你不僅用到了 ES2015 中的箭頭函式和塊作用域變數這些特性,還為所用的一些 AngularJS 服務提供了型別定義。

For instance, in the phone detail component spec, you can use ES2015 features like arrow functions and block-scoped variables and benefit from the type definitions of the AngularJS services you're consuming:

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); }); }); });
app/phone-detail/phone-detail.component.spec.ts
      
      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);
    });

  });

});
    

一旦你開始了升級過程並引入了 SystemJS,還需要對 Karma 進行配置修改。 你需要讓 SystemJS 載入所有的 Angular 新程式碼,

Once you start the upgrade process and bring in SystemJS, configuration changes are needed for Karma. You need to let SystemJS load all the new Angular code, which can be done with the following kind of shim file:

// /*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 umd bundles map: { '@angular/core/testing': 'npm:@angular/core/bundles/core-testing.umd.js', '@angular/common/testing': 'npm:@angular/common/bundles/common-testing.umd.js', '@angular/common/http/testing': 'npm:@angular/common/bundles/common-http-testing.umd.js', '@angular/compiler/testing': 'npm:@angular/compiler/bundles/compiler-testing.umd.js', '@angular/platform-browser/testing': 'npm:@angular/platform-browser/bundles/platform-browser-testing.umd.js', '@angular/platform-browser-dynamic/testing': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic-testing.umd.js', '@angular/router/testing': 'npm:@angular/router/bundles/router-testing.umd.js', '@angular/forms/testing': 'npm:@angular/forms/bundles/forms-testing.umd.js', }, }); 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); }
karma-test-shim.js
      
      // /*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 umd bundles
  map: {
    '@angular/core/testing': 'npm:@angular/core/bundles/core-testing.umd.js',
    '@angular/common/testing': 'npm:@angular/common/bundles/common-testing.umd.js',
    '@angular/common/http/testing': 'npm:@angular/common/bundles/common-http-testing.umd.js',
    '@angular/compiler/testing': 'npm:@angular/compiler/bundles/compiler-testing.umd.js',
    '@angular/platform-browser/testing': 'npm:@angular/platform-browser/bundles/platform-browser-testing.umd.js',
    '@angular/platform-browser-dynamic/testing': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic-testing.umd.js',
    '@angular/router/testing': 'npm:@angular/router/bundles/router-testing.umd.js',
    '@angular/forms/testing': 'npm:@angular/forms/bundles/forms-testing.umd.js',
  },
});

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);
}
    

這個 shim 檔案首先載入了 SystemJS 的配置,然後是 Angular 的測試支援函式庫,然後是應用本身的規約檔案。

The shim first loads the SystemJS configuration, then Angular's test support libraries, and then the application's spec files themselves.

然後需要修改 Karma 配置,來讓它使用本應用的根目錄作為基礎目錄(base directory),而不是 app

Karma configuration should then be changed so that it uses the application root dir as the base directory, instead of app.

basePath: './',
karma.conf.js
      
      basePath: './',
    

一旦這些完成了,你就能載入 SystemJS 和其它依賴,並切換配置檔案來載入那些應用檔案,而不用在 Karma 頁面中包含它們。 你要讓這個 shim 檔案和 SystemJS 去載入它們。

Once done, you can load SystemJS and other dependencies, and also switch the configuration for loading application files so that they are not included to the page by Karma. You'll let the shim and SystemJS load them.

// 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/**/*.js', included: false, watched: false}, {pattern: 'node_modules/@angular/**/*.js.map', 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},
karma.conf.js
      
      // 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/**/*.js', included: false, watched: false},
{pattern: 'node_modules/@angular/**/*.js.map', 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 一把,幫它在正確的路徑下找到這些範本:

Since the HTML templates of Angular components will be loaded as well, you must help Karma out a bit so that it can route them to the right paths:

// 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' },
karma.conf.js
      
      // 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'
},
    

如果產品程式碼被切換到了 Angular,單元測試檔案本身也需要切換過來。對勾(checkmark)管道的規約可能是最直觀的,因為它沒有任何依賴:

The unit test files themselves also need to be switched to Angular when their production counterparts are switched. The specs for the checkmark pipe are probably the most straightforward, as the pipe has no dependencies:

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'); }); });
app/core/checkmark/checkmark.pipe.spec.ts
      
      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 服務的測試會牽扯到一點別的。你需要把模擬版的 AngularJS $httpBackend 服務切換到模擬板的 Angular Http 後端。

The unit test for the phone service is a bit more involved. You need to switch from the mocked-out AngularJS $httpBackend to a mocked-out Angular Http backend.

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); }); });
app/core/phone/phone.service.spec.ts
      
      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);
  });

});
    

對於元件的規約,你可以模擬出 Phone 服務本身,並且讓它提供電話的資料。你可以對這些元件使用 Angular 的元件單元測試 API。

For the component specs, you can mock out the Phone service itself, and have it provide canned phone data. You use Angular's component unit testing APIs for both components.

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); }); });
app/phone-detail/phone-detail.component.spec.ts
      
      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); }); });
app/phone-list/phone-list.component.spec.ts
      
      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);
  });
});
    

最後,當你切換到 Angular 路由時,需要重新過一遍這些元件測試。對詳情元件來說,你需要提供一個 Angular RouteParams 的 mock 物件,而不再用 AngularJS 中的 $routeParams

Finally, revisit both of the component tests when you switch to the Angular router. For the details component, provide a mock of Angular ActivatedRoute object instead of using the 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(); }));
app/phone-detail/phone-detail.component.spec.ts
      
      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();
  }));
    

對於電話列表元件,還要再做少量的調整,以便路由器能讓 RouteLink 指令正常工作。

And for the phone list component, a few adjustments to the router make the RouteLink directives work.

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); });
app/phone-list/phone-list.component.spec.ts
      
      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);
  });