AngularJS學習筆記

AngularJS學習筆記

官網:https://angular.io/css

中文社區:https://angular.cn/html

JavaScript速查:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects前端

ECMA2015語法:http://es6.ruanyifeng.com/
node

使用IDE:WebStorm2017。啓動時,能夠鼠標選中package.json中scripts的標籤,例如start,而後執行F5react

WebStorm,沒有Angular語法提示。git

  1. 在 head 裏面添加:
    <script src="http://apps.bdimg.com/libs/angular.js/1.4.6/angular.min.js"></script>
  2. 鼠標放到這行 URL 上面後,你會發現這行代碼的開頭會出現一個燈泡,點擊它以後會出現 download 選項,下載完成以後這個庫就會自動添加到當前項目

目前爲止更新版本是 Angular v6.0程序員

特色

AngularJS,目標是一套框架、多種平臺同時適用手機和桌面es6

跨平臺開發:github

  學習如何基於Angular構建應用程序,並複用代碼和技能來構建適用於全部平臺的應用。web

速度與性能:

  經過Web Worker和服務端渲染,達到在現在的Web平臺上所能達到的最高速。

  Angular讓你有效掌握可伸縮性。基於RxJS、Immutable.js和其餘推送模型。

  能適用海量數據需求。

美妙的工具:

  使用簡單聲明式模板,快速實現各類特性。使用自定義組件和大量現有組件,擴展模板語言。

Angular CLI

一個命令行界面,能夠建立項目、添加文件以及執行一大堆開發任務、好比測試,打包和發佈。

首先須要安裝 Nodejs 和 npm

而後全局安裝Angular CLI:npm install -g @angular/cli

使用 ng new my-app 能夠建立新的app。ng help 能夠查看幫助

會等待一段時間,而後建立完成。

此時 ng serve --open。能夠執行,並在瀏覽器中打開。

若是提示有modules找不到,能夠將modules文件夾刪除,而後npm install。

命令行修改端口號
ng server --port 4201
(ng serve --host localhost --port 4201)

項目文件概覽

Angular CLI項目是作快速試驗和開發企業解決方案的基礎。

你首先要看的文件是README.md。 它提供了一些如何使用CLI命令的基礎信息。 若是你想了解 Angular CLI 的工做原理,請訪問 Angular CLI 的倉庫及其 Wiki

有些生成的文件你可能以爲陌生。接下來咱們就講講它們。

src文件夾

你的應用代碼位於src文件夾中。 全部的Angular組件、模板、樣式、圖片以及你的應用所需的任何東西都在那裏。 這個文件夾以外的文件都是爲構建應用提供支持用的。

文件

用途

app/app.component.{ts,html,css,spec.ts}

使用HTML模板、CSS樣式和單元測試定義AppComponent組件。 它是組件,隨着應用的成長它會成爲一棵組件樹的根節點。

app/app.module.ts

定義AppModule,這個根模塊會告訴Angular如何組裝該應用。 目前,它只聲明瞭AppComponent。 稍後它還會聲明更多組件。

assets/*

這個文件夾下你能夠放圖片等任何東西,在構建應用時,它們全都會拷貝到發佈包中。

environments/*

這個文件夾中包括爲各個目標環境準備的文件,它們導出了一些應用中要用到的配置變量。 這些文件會在構建應用時被替換。 好比你可能在產品環境中使用不一樣的API端點地址,或使用不一樣的統計Token參數。 甚至使用一些模擬服務。 全部這些,CLI都替你考慮到了。

favicon.ico

每一個網站都但願本身在書籤欄中能好看一點。 請把它換成你本身的圖標。

index.html

這是別人訪問你的網站是看到的主頁面的HTML文件。 大多數狀況下你都不用編輯它。 在構建應用時,CLI會自動把全部jscss文件添加進去,因此你沒必要在這裏手動添加任何 <script> 或 <link> 標籤。

main.ts

這是應用的主要入口點。 使用JIT compiler編譯器編譯本應用,並啓動應用的根模塊AppModule,使其運行在瀏覽器中。 你還能夠使用AOT compiler編譯器,而不用修改任何代碼 —— 只要給ng build 或 ng serve 傳入 --aot 參數就能夠了。

polyfills.ts

不一樣的瀏覽器對Web標準的支持程度也不一樣。 填充庫(polyfill)能幫咱們把這些不一樣點進行標準化。 你只要使用core-js 和 zone.js一般就夠了,不過你也能夠查看瀏覽器支持指南以瞭解更多信息。

styles.css

這裏是你的全局樣式。 大多數狀況下,你會但願在組件中使用局部樣式,以利於維護,不過那些會影響你整個應用的樣式你仍是須要集中存放在這裏。

test.ts

這是單元測試的主要入口點。 它有一些你不熟悉的自定義配置,不過你並不須要編輯這裏的任何東西。

tsconfig.{app|spec}.json

TypeScript編譯器的配置文件。tsconfig.app.json是爲Angular應用準備的,而tsconfig.spec.json是爲單元測試準備的。

根目錄

src/文件夾是項目的根文件夾之一。 其它文件是用來幫助你構建、測試、維護、文檔化和發佈應用的。它們放在根目錄下,和src/平級。

文件

用途

e2e/

e2e/下是端到端(End-to-End)測試。 它們不在src/下,是由於端到端測試實際上和應用是相互獨立的,它只適用於測試你的應用而已。 這也就是爲何它會擁有本身的tsconfig.json

node_modules/

Node.js建立了這個文件夾,而且把package.json中列舉的全部第三方模塊都放在其中。

.angular-cli.json

Angular CLI的配置文件。 在這個文件中,咱們能夠設置一系列默認值,還能夠配置項目編譯時要包含的那些文件。 要了解更多,請參閱它的官方文檔。

.editorconfig

給你的編輯器看的一個簡單配置文件,它用來確保參與你項目的每一個人都具備基本的編輯器配置。 大多數的編輯器都支持.editorconfig文件,詳情參見 http://editorconfig.org 。

.gitignore

一個Git的配置文件,用來確保某些自動生成的文件不會被提交到源碼控制系統中。

karma.conf.js

Karma的單元測試配置,當運行ng test時會用到它。

package.json

npm配置文件,其中列出了項目使用到的第三方依賴包。 你還能夠在這裏添加本身的自定義腳本

protractor.conf.js

Protractor使用的端到端測試配置文件,當運行ng e2e的時候會用到它。

README.md

項目的基礎文檔,預先寫入了CLI命令的信息。 別忘了用項目文檔改進它,以便每一個查看此倉庫的人都能據此構建出你的應用。

tsconfig.json

TypeScript編譯器的配置,你的IDE會藉助它來給你提供更好的幫助。

tslint.json

TSLintCodelyzer用的配置信息,當運行ng lint時會用到。 Lint功能能夠幫你保持代碼風格的統一。

官方教程-英雄編輯器

源碼:https://angular.cn/tutorial/toh-pt1

項目只要啓動ng start,當你對項目進行任何修改,都會同步到頁面上。

ng generate component heroes 能夠經過命令行建立新組件

使用ng --help \ ng generate --help能夠查看具體指令。會自動建立並添加引用

架構概覽

Angular是一個用HTML和TypeScript構建客戶端應用的平臺與框架。自己使用TypeScript寫成的。將核心功能和可選功能做爲一組TypeScript庫進行實現。

Angular的基本構造快是NgModule,爲組件提供了編譯的上下文環境。NgModule會把相關的代碼收集到一些功能集中。Angular應用就是由一組NgModule定義的。至少會有一個用於引導應用的跟模塊,一般還會有多個特性模塊。

  • 組件定義視圖。視圖是一組可見的屏幕元素,Angular能夠根據你的程序邏輯和數據來選擇和修改它們。每一個應用都至少有一個根組件。
  • 組件使用服務。服務會提供哪些與視圖不直接相關的功能。服務提供商能夠做爲依賴被注入到組件中,讓你的代碼更加模塊化、可服用、並且高效。

組件和服務都是簡單的類,使用裝飾器來標出它們的類型,並提供元數據告知Angular該如何使用

  • 組件的元數據將組件和一個用來定義視圖的模板關聯起來。模板把普通的HTML和指令與綁定標記組合起來。這樣Angular就能夠在呈現HTML以前先修改這些HTML。
  • 服務的元數據提供了一些信息,Angular要用這些信息來讓組件能夠經過依賴注入使用該服務。

應用的組件一般會定義不少視圖,並進行分級組織。Angular提供了Router服務來幫助你定義視圖之間的導航路徑。路由器提供了現金的瀏覽器內導航功能。

模塊(Module Component)

Angular應用是模塊化的,有本身的模塊系統,稱爲Angular模塊或NgModules。

NgModule 能夠將其組件和一組相關代碼(如服務)關聯起來,造成功能單元。

Angular應用至少有一個模塊(根模塊),習慣命名AppModule

export class AppModule { }  //app.module.ts

根模塊在小型應用中多是惟一的模塊,大多數應用會有不少特性模塊,每一個模塊都是一個內聚的代碼塊。

像 JavaScript 模塊同樣,NgModule 也能夠從其它 NgModule 中導入功能,並容許導出它們本身的功能供其它 NgModule 使用。 好比,要在你的應用中使用路由器(Router)服務,就要導入 Router 這個 NgModule。

模板、指令和數據綁定

模板會把 HTML 和 Angular 的標記(markup)組合起來,這些標記能夠在 HTML 元素顯示出來以前修改它們。 模板中的指令會提供程序邏輯,而綁定標記會把你應用中的數據和 DOM 鏈接在一塊兒。

  • 事件綁定讓你的應用能夠經過更新應用的數據來響應目標環境下的用戶輸入。

  • 屬性綁定讓你將從應用數據中計算出來的值插入到 HTML 中。

在視圖顯示出來以前,Angular 會先根據你的應用數據和邏輯來運行模板中的指令並解析綁定表達式,以修改 HTML 元素和 DOM。 Angular 支持雙向數據綁定,這意味着 DOM 中發生的變化(好比用戶的選擇)一樣能夠反映回你的程序數據中。

你的模板也能夠用管道轉換要顯示的值以加強用戶體驗。好比,能夠使用管道來顯示適合用戶所在地區的日期和貨幣格式。 Angular 爲一些通用的轉換提供了預約義管道,你還能夠定義本身的管道。

模板(Template)

經過自帶的模板來定義組件視圖,以HTML形式存在,告訴Angular如何渲染

模板除了標準的Html元素,還能夠使用Angular的模板語法,例如*ngFor、{{hero.name}}

元數據(Metadata)

元數據告訴Angular如何處理一個類。使用裝飾器@Component來附加元數據。

@Component裝飾器能接受一個配置對象,Angular會基於這些信息建立和展現組件及其視圖。

@Component配置項包括

selector:標籤選擇器,告訴Angular在HTML中查找對應名字的標籤,建立並插入該組件。

templateUrl:組件HTML模板相對地址

providers:組件所需服務的依賴注入模塊數組。告訴Angular該組件的構造須要一個服務

@Component({
  selector: 'hero-search',
  templateUrl: './hero-search.component.html',
  styleUrls: ['./hero-search.component.css'],
  providers: [HeroSearchService]
})

服務於依賴注入

對於與特定視圖無關並但願跨組件共享的數據或邏輯,能夠建立服務類。 服務類的定義一般緊跟在 「@Injectable」 裝飾器以後。該裝飾器提供的元數據可讓你的服務做爲依賴被注入到客戶組件中。

依賴注入(或 DI)讓你能夠保持組件類的精簡和高效。有了 DI,組件就不用從服務器獲取數據、驗證用戶輸入或直接把日誌寫到控制檯,而是會把這些任務委託給服務。

路由

Angular 的 Router 模塊提供了一個服務,它可讓你定義在應用的各個不一樣狀態和視圖層次結構之間導航時要使用的路徑。

  • 在地址欄輸入 URL,瀏覽器就會導航到相應的頁面。

  • 在頁面中點擊連接,瀏覽器就會導航到一個新頁面。

  • 點擊瀏覽器的前進和後退按鈕,瀏覽器就會在你的瀏覽歷史中向前或向後導航。

不過路由器會把相似 URL 的路徑映射到視圖而不是頁面。 當用戶執行一個動做時(好比點擊連接),本應該在瀏覽器中加載一個新頁面,可是路由器攔截了瀏覽器的這個行爲,並顯示或隱藏一個視圖層次結構。

若是路由器認爲當前的應用狀態須要某些特定的功能,而定義此功能的模塊還沒有加載,路由器就會按需惰性加載此模塊。

路由器會根據你應用中的導航規則和數據狀態來攔截 URL。 當用戶點擊按鈕、選擇下拉框或收到其它任何來源的輸入時,你能夠導航到一個新視圖。 路由器會在瀏覽器的歷史日誌中記錄這個動做,因此前進和後退按鈕也能正常工做。

要定義導航規則,你就要把導航路徑和你的組件關聯起來。 路徑(path)使用相似 URL 的語法來和程序數據整合在一塊兒,就像模板語法會把你的視圖和程序數據整合起來同樣。 而後你就能夠用程序邏輯來決定要顯示或隱藏哪些視圖,以根據你制定的訪問規則對用戶的輸入作出響應。

overview

  • 組件和模板共同定義了 Angular 的視圖。

    • 組件類上的裝飾器爲其添加了元數據,其中包括指向相關模板的指針。

    • 組件模板中的指令和綁定標記會根據程序數據和程序邏輯修改這些視圖。

  • 依賴注入器會爲組件提供一些服務,好比路由器服務就能讓你定義如何在視圖之間導航。

數據綁定

若是沒有框架,就須要本身數據推送到HTML控件中,並把用戶反饋動做和值更新。

Angular支持數據綁定,讓模板的各部分與組件的各部分相互合做的機制。咱們往HTML添加綁定標記,告訴Angular如何把二者聯繫起來。

數據綁定的語法有四種方式,每種形式都有一個方向,綁定到DOM、綁定自DOM、以及雙向綁定。

<li>{{hero.name}}</li>
<app-hero-detail [hero]="selectedHero"></app-hero-detail>
<li (click)="selectHero(hero)"></li>
  • {{hero.name]}插值表達式在li標籤中顯示組件hero.name的值
  • [hero]屬性表達式把父組件的值傳到子組件的屬性中
  • (click)事件表達式在用戶點擊英雄的名字時調用組件的selectHero方法
  • 雙向綁定是第四種綁定形式,使用ngModel指令組合屬性綁定和事件綁定的功能。<input [(ngModel)]="hero.name">

注意:雙向綁定必定要加載FormsModule,在app.module.ts中的import暴露出去

import {FormsModule} from '@angular/forms';

指令(directive)

Angular模板是動態的,當Angular渲染他們的時候,會根據指令提供的操做對DOM進行轉換。

組件是一個帶模板的指令,@Component裝飾器實際上就是@Directive裝飾器,只是擴展了一些面向模板的特性。

還有兩種其餘的指令

1.屬性指令:像屬性同樣出如今元素中,偶爾會以名字的形式出現,多數時候做爲賦值目標或綁定目標出現。例如

<input [(ngModel)]="hero.name">

ngModel修改現有元素的行爲,設置其顯示屬性值、響應change事件

2.結構型指令:經過在DOM中添加、移除、替換元素來修改佈局。例如:

<li *ngFor="let hero of heroes"></li>
<app-hero-detail *ngIf="selectedHero"></app-hero-detail>

*ngFor告訴Angular爲heroes列表的每一個英雄生成一個<li>標籤

*ngIf表示只有在選擇英雄存在時,纔會包含HeroDetail組件

服務(Service)

服務是一個廣義範疇、包括:值、函數、應用所需的特性

任何東西都是一個服務,典型的服務是一個類,具備專一、明確的用途,應該作一件特定的事情,並把它作好。

服務並無什麼Angular特性。好比Logger服務,就是以下所示

export class Logger {
  log(msg: any)   { console.log(msg); }
  error(msg: any) { console.error(msg); }
  warn(msg: any)  { console.warn(msg); }
}

依賴注入

提供類的新實例的一種方式,負責處理好類所需的所有依賴。

大多數依賴都是服務,使用依賴注入來提供新數組以及組件所需的服務。

Angular經過查看構造函數的參數類型得知組件須要哪些服務。例以下方代碼就是須要HeroService服務

constructor(private service: HeroService) { }

當Angular建立組件時,會首先爲組件所需的服務請求一個注入器(injector)

注入器維護了一個服務實例的容器,存放着之前建立的實例。若是所請求的服務實例不在容器中,注入器就會建立一個服務實例,而且添加到容器中,而後返回給Angular。

當全部請求的服務都被解析完成並返回時,Angular會以這些服務爲參數去調用組件的構造函數。這就是依賴注入。

一般咱們會把須要注入的類放到根模塊上,以便統一化

providers: [
  BackendService,
  HeroService,
  Logger
],

或者也能夠在@Commonent元數據中的providers屬性把它註冊在組件層。

@Component({
  selector:    'app-hero-list',
  templateUrl: './hero-list.component.html',
  providers:  [ HeroService ]
})

依賴注入的要點是:

1.滲透在整個Angular框架中,被處處使用

2.注入器(injector)是本機制的核心。

  注入器負責維護一個容器,用於存放它建立過的服務實例

  注入器能使用提供商建立的一個新的服務實例

3.提供商是一個用於建立服務的配方。

4.把提供商註冊到注入器中。

模塊簡介

Module

Angular 應用是模塊化的,它擁有本身的模塊化系統,稱做 NgModule。 一個 NgModule 就是一個容器,用於存放一些內聚的代碼塊,這些代碼塊專一於某個應用領域、某個工做流或一組緊密相關的功能。 它能夠包含一些組件、服務提供商或其它代碼文件,其做用域由包含它們的 NgModule 定義。 它還能夠導入一些由其它模塊中導出的功能,並導出一些指定的功能供其它 NgModule 使用。

每一個 Angular 應用都至少有一個 NgModule 類,也就是根模塊,它習慣上命名爲 AppModule,並位於一個名叫 app.module.ts 的文件中。引導這個根模塊就能夠啓動你的應用。

雖然小型的應用可能只有一個 NgModule,不過大多數應用都會有不少特性模塊。應用的根模塊之因此叫根模塊,是由於它能夠包含任意深度的層次化子模塊。

@NgModule元數據

NgModule 是一個帶有 @NgModule 裝飾器的類。@NgModule 裝飾器是一個函數,它接受一個元數據對象,該對象的屬性用來描述這個模塊。其中最重要的屬性以下。

  • declarations(可聲明對象表) —— 那些屬於本 NgModule 的組件、指令、管道。

  • exports(導出表) —— 那些能在其它模塊的組件模板中使用的可聲明對象的子集。

  • imports(導入表) —— 那些導出了本模塊中的組件模板所需的類的其它模塊。

  • providers —— 本模塊向全局服務中貢獻的那些服務的建立器。 這些服務能被本應用中的任何部分使用。(你也能夠在組件級別指定服務提供商,這一般是首選方式。)

  • bootstrap —— 應用的主視圖,稱爲根組件。它是應用中全部其它視圖的宿主。只有根模塊才應該設置這個 bootstrap 屬性。

下面是一個簡單的根 NgModule 定義:

import { NgModule }      from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
@NgModule({  
  imports:      [ BrowserModule ],
  providers:    [ Logger ],
  declarations: [ AppComponent ],
  exports:      [ AppComponent ],
  bootstrap:    [ AppComponent ]
})
export class AppModule { }

NgModule和組件

NgModule 爲其中的組件提供了一個編譯上下文環境。根模塊總會有一個根組件,並在引導期間建立它。 可是,任何模塊都能包含任意數量的其它組件,這些組件能夠經過路由器加載,也能夠經過模板建立。那些屬於這個 NgModule 的組件會共享同一個編譯上下文環境。

Component compilation context

組件及其模板共同定義視圖。組件還能夠包含視圖層次結構,它能讓你定義任意複雜的屏幕區域,能夠將其做爲一個總體進行建立、修改和銷燬。 一個視圖層次結構中能夠混合使用由不一樣 NgModule 中的組件定義的視圖。 這種狀況很常見,特別是對一些 UI 庫來講。

View hierarchy

當你建立一個組件時,它直接與一個叫作宿主視圖的視圖關聯起來。 宿主視圖能夠是視圖層次結構的根,該視圖層次結構能夠包含一些內嵌視圖,這些內嵌視圖又是其它組件的宿主視圖。 這些組件能夠位於相同的 NgModule 中,也能夠從其它 NgModule 中導入。 樹中的視圖能夠嵌套到任意深度。

Angular模塊庫(Library Module) 

Angular提供了一組JS模塊,能夠看作是庫模塊。

每一個庫都是@angular前綴,能夠經過npm安裝,並經過import進行導入。

例如:import { Component } from '@angular/core';

若是應用模塊須要Component的某些素材,就得把他加入@NgModule元數據的import中。

組件簡介

組件控制屏幕上被稱爲視圖的一小片區域。好比,教程中的下列視圖都是由一個個組件所定義和控制的:

  • 帶有導航連接的應用根組件。

  • 英雄列表。

  • 英雄編輯器。

你在類中定義組件的應用邏輯,爲視圖提供支持。 組件經過一些由屬性和方法組成的 API 與視圖交互。

好比,HeroListComponent 有一個 heroes 屬性,它會返回一個從服務中取到的英雄數組。HeroListComponent 還有一個 selectHero() 方法,當用戶從列表中選擇一個英雄時,它會設置 selectedHero 屬性的值。

export class HeroListComponent implements OnInit {
  heroes: Hero[];
  selectedHero: Hero;

  constructor(private service: HeroService) { }

  ngOnInit() {
    this.heroes = this.service.getHeroes();
  }

  selectHero(hero: Hero) { this.selectedHero = hero; }
}

當用戶在應用中穿行時,Angular 就會建立、更新、銷燬一些組件。 你的應用能夠經過一些可選的生命週期鉤子(好比ngOnInit())來在每一個特定的時機採起行動。

組件的元數據

@Component 裝飾器會指出緊隨其後的那個類是個組件類,併爲其指定元數據。 在下面的範例代碼中,你能夠看到 HeroListComponent 只是一個普通類,徹底沒有 Angular 特有的標記或語法。 直到給它加上了 @Component 裝飾器,它才變成了組件。

組件的元數據告訴 Angular 到哪裏獲取它須要的主要構造塊,以建立和展現這個組件及其視圖。 具體來講,它把一個模板(不管是直接內聯在代碼中仍是引用的外部文件)和該組件關聯起來。 該組件及其模板,共同描述了一個視圖。

除了包含或指向模板以外,@Component 的元數據還會配置要如何在 HTML 中引用該組件,以及該組件須要哪些服務等等。

下面的例子中就是 HeroListComponent 的基礎元數據:

@Component({
  selector:    'app-hero-list',
  templateUrl: './hero-list.component.html',
  providers:  [ HeroService ]
})
export class HeroListComponent implements OnInit {
/* . . . */
}

這個例子展現了一些最經常使用的 @Component 配置選項:

  • selector:是一個 CSS 選擇器,它會告訴 Angular,一旦在模板 HTML 中找到了這個選擇器對應的標籤,就建立並插入該組件的一個實例。 好比,若是應用的 HTML 中包含 <app-hero-list></app-hero-list>,Angular 就會在這些標籤中插入一個 HeroListComponent 實例的視圖。

  • templateUrl:該組件的 HTML 模板文件相對於這個組件文件的地址。 另外,你還能夠用 template 屬性的值來提供內聯的 HTML 模板。 這個模板定義了該組件的宿主視圖。

  • providers 是當前組件所需的依賴注入提供商的一個數組。在這個例子中,它告訴 Angular,該組件的構造函數須要一個 HeroService 實例,以獲取要顯示的英雄列表。

模板與視圖

你要經過組件的配套模板來定義其視圖。模板就是一種 HTML,它會告訴 Angular 如何渲染該組件。

視圖一般會分層次進行組織,讓你能以 UI 分區或頁面爲單位進行修改、顯示或隱藏。 與組件直接關聯的模板會定義該組件的宿主視圖。該組件還能夠定義一個帶層次結構的視圖,它包含一些內嵌的視圖做爲其它組件的宿主。

Component tree

帶層次結構的視圖能夠包含同一模塊(NgModule)中組件的視圖,也能夠(並且常常會)包含其它模塊中定義的組件的視圖。

模板語法

模板很像標準的 HTML,可是它還包含 Angular 的模板語法,這些模板語法能夠根據你的應用邏輯、應用狀態和 DOM 數據來修改這些 HTML。 你的模板能夠使用數據綁定來協調應用和 DOM 中的數據,使用管道在顯示出來以前對其進行轉換,使用指令來把程序邏輯應用到要顯示的內容上。

好比,下面是本教程中 HeroListComponent 的模板:

<h2>Hero List</h2>

<p><i>Pick a hero from the list</i></p>
<ul>
  <li *ngFor="let hero of heroes" (click)="selectHero(hero)">
    {{hero.name}}
  </li>
</ul>

<app-hero-detail *ngIf="selectedHero" [hero]="selectedHero"></app-hero-detail>

這個模板使用了典型的 HTML 元素,好比 <h2> 和 <p>,還包括一些 Angular 的模板語法元素,如 *ngFor{{hero.name}}click[hero] 和 <app-hero-detail>。這些模板語法元素告訴 Angular 該如何根據程序邏輯和數據在屏幕上渲染 HTML。

  • *ngFor 指令告訴 Angular 在一個列表上進行迭代。

  • {{hero.name}}(click) 和 [hero] 把程序數據綁定到及綁定回 DOM,以響應用戶的輸入。更多內容參見稍後的數據綁定部分。

  • 模板中的 <app-hero-detail> 標籤是一個表明新組件 HeroDetailComponent 的元素。HeroDetailComponent(代碼略)是 HeroListComponent 的一個子組件,它定義了英雄詳情視圖。 注意觀察像這樣的自定義組件是如何與原生 HTML 元素無縫的混合在一塊兒的。

數據綁定

若是沒有框架,你就要本身負責把數據值推送到 HTML 控件中,並把來自用戶的響應轉換成動做和對值的更新。 手動寫這種數據推拉邏輯會很枯燥、容易出錯,難以閱讀 —— 用過 jQuery 的程序員必定深有體會。

Angular 支持雙向數據綁定,這是一種對模板中的各個部件與組件中的各個部件進行協調的機制。 往模板 HTML 中添加綁定標記能夠告訴 Angular 該如何鏈接它們。

下圖顯示了數據綁定標記的四種形式。每種形式都有一個方向 —— 從組件到 DOM、從 DOM 到組件或雙向。

Data Binding

這個來自 HeroListComponent 模板中的例子使用了其中的三種形式:

<li>{{hero.name}}</li>
<app-hero-detail [hero]="selectedHero"></app-hero-detail>
<li (click)="selectHero(hero)"></li>
  • {{hero.name}}插值表達式在 <li> 標籤中顯示組件的 hero.name 屬性的值。
  • [hero]屬性綁定把父組件 HeroListComponent 的 selectedHero 的值傳到子組件 HeroDetailComponent 的 hero 屬性中。
  • 當用戶點擊某個英雄的名字時,(click)事件綁定會調用組件的 selectHero 方法

第四種重要的綁定形式是雙向數據綁定,它把屬性綁定和事件綁定組合成一種單獨的寫法。下面這個來自 HeroDetailComponent 模板中的例子經過 ngModel 指令使用了雙向數據綁定:

<input [(ngModel)]="hero.name">

在雙向綁定中,數據屬性值經過屬性綁定從組件流到輸入框。用戶的修改經過事件綁定流回組件,把屬性值設置爲最新的值。

Angular 在每一個 JavaScript 事件循環中處理全部的數據綁定,它會從組件樹的根部開始,遞歸處理所有子組件。

Data Binding

數據綁定在模板及其組件之間的通信中扮演了很是重要的角色,它對於父組件和子組件之間的通信也一樣重要。

Parent/Child binding 

管道

Angular 的管道可讓你在模板中聲明顯示值的轉換邏輯。 帶有 @Pipe 裝飾器的類中會定義一個轉換函數,用來把輸入值轉換成供視圖顯示用的輸出值。

Angular 自帶了不少管道,好比 date 管道和 currency 管道,完整的列表參見 Pipes API 列表。你也能夠本身定義一些新管道。

要在 HTML 模板中指定值的轉換方式,請使用 管道操做符 (|)

{{interpolated_value | pipe_name}}

你能夠把管道串聯起來,把一個管道函數的輸出送給另外一個管道函數進行轉換。 管道還能接收一些參數,來控制它該如何進行轉換。好比,你能夠把要使用的日期格式傳給 date 管道:

<p>Today is {{today | date}}</p>
<p>The date is {{today | date:'fullDate'}}</p>
<p>The time is {{today | date:'shortTime'}}</p>

指令

Angular 的模板是動態的。當 Angular 渲染它們的時候,會根據指令給出的指示對 DOM 進行轉換。

指令就是一個帶有 @Directive裝飾器的類。

組件從技術角度上說就是一個指令,可是因爲組件對 Angular 應用來講很是獨特、很是重要,所以 Angular 專門定義了 @Component 裝飾器,它使用一些面向模板的特性擴展了 @Directive 裝飾器。

除組件外,還有兩種指令:結構型指令和屬性型指令。和組件同樣,指令的元數據把指令類和一個 selector 關聯起來,selector 用來把該指令插入到 HTML 中。 在模板中,指令一般做爲屬性出如今元素標籤上,可能僅僅做爲名字出現,也可能做爲賦值目標或綁定目標出現。

結構型指令

結構型指令經過添加、移除或替換 DOM 元素來修改佈局。 這個範例模板使用了兩個內置的結構型指令來爲要渲染的視圖添加程序邏輯:

<li *ngFor="let hero of heroes"></li>
<app-hero-detail *ngIf="selectedHero"></app-hero-detail>
  • *ngFor 是一個迭代器,它要求 Angular 爲 heroes 列表中的每一個 <li> 渲染出一個 <li>

  • *ngIf 是個條件語句,只有當選中的英雄存在時,它纔會包含 HeroDetail 組件。

屬性型指令

屬性型指令會修改現有元素的外觀或行爲。 在模板中,它們看起來就像普通的 HTML 屬性同樣,所以得名「屬性型指令」。

ngModel 指令就是屬性型指令的一個例子,它實現了雙向數據綁定。 ngModel 修改現有元素(通常是 <input>)的行爲:設置其顯示屬性值,並響應 change 事件。

<input [(ngModel)]="hero.name">

Angular 還有不少預約義指令,它們或者修改佈局結構(好比 ngSwitch),或者修改 DOM 元素和組件的某些方面(好比 ngStyle 和 ngClass)。

你還能夠寫本身的指令。像 HeroListComponent 這樣的組件就是一種自定義指令,你還能夠建立自定義的結構型指令和屬性型指令。

服務是一個廣義的概念,它包括應用所需的任何值、函數或特性。狹義的服務是一個明肯定義了用途的類。它應該作一些具體的事,並作好。

服務與依賴注入簡介

Angular 把組件和服務區分開,以提升模塊性和複用性。

  • 經過把組件中和視圖有關的功能與其餘類型的處理分離開,你可讓組件類更加精簡、高效。 理想狀況下,組件的工做只管用戶體驗,而不用顧及其它。 它應該提供用於數據綁定的屬性和方法,以便做爲視圖(由模板渲染)和應用邏輯(一般包含一些模型的概念)的中介者。

  • 組件不該該定義任何諸如從服務器獲取數據、驗證用戶輸入或直接往控制檯中寫日誌等工做。 而要把這些任務委託給各類服務。經過把各類處理任務定義到可注入的服務類中,你可讓它能夠被任何組件使用。 經過在不一樣的環境中注入同一種服務的不一樣提供商,你還可讓你的應用更具適應性。

Angular 不會強制遵循這些原則。它只會經過依賴注入讓你能更容易地將應用邏輯分解爲服務,並讓這些服務可用於各個組件中。

服務範例

下面是一個服務類的範例,用於把日誌記錄到瀏覽器的控制檯:

export class Logger {
  log(msg: any)   { console.log(msg); }
  error(msg: any) { console.error(msg); }
  warn(msg: any)  { console.warn(msg); }
}

服務也能夠依賴其它服務。這裏的 HeroService 就依賴於 Logger 服務,它還用 BackendService 來獲取英雄數據。BackendService 還可能再轉而依賴 HttpClient 服務來從服務器異步獲取英雄列表。

export class HeroService {
  private heroes: Hero[] = [];

  constructor(
    private backend: BackendService,
    private logger: Logger) { }

  getHeroes() {
    this.backend.getAll(Hero).then( (heroes: Hero[]) => {
      this.logger.log(`Fetched ${heroes.length} heroes.`);
      this.heroes.push(...heroes); // fill cache
    });
    return this.heroes;
  }
}

依賴注入(dependency injection)

組件是服務的消費者,也就是說,你能夠把一個服務注入到組件中,讓組件類得以訪問該服務類。

在 Angular 中,要把一個類定義爲服務,就要用 @Injectable 裝飾器來提供元數據,以便讓 Angular 能夠把它做爲依賴注入到組件中。

一樣,也要使用 @Injectable 裝飾器來代表一個組件或其它類(好比另外一個服務、管道或 NgModule)擁有一個依賴。 依賴並沒必要然是服務,它也多是函數或值等等。

依賴注入(一般簡稱 DI)被引入到 Angular 框架中,而且處處使用它,來爲新建的組件提供所需的服務或其它東西。

  • 注入器是主要的機制。你不用本身建立 Angular 注入器。Angular 會在啓動過程當中爲你建立全應用級注入器。

  • 該注入器維護一個包含它已建立的依賴實例的容器,並儘量複用它們。

  • 提供商是一個建立依賴的菜譜。對於服務來講,它一般就是這個服務類自己。你在應用中要用到的任何類都必須使用該應用的注入器註冊一個提供商,以便注入器能夠使用它來建立新實例。

當 Angular 建立組件類的新實例時,它會經過查看該組件類的構造函數,來決定該組件依賴哪些服務或其它依賴項。 好比 HeroListComponent 的構造函數中須要 HeroService

constructor(private service: HeroService) { }

當 Angular 發現某個組件依賴某個服務時,它會首先檢查是否該注入器中已經有了那個服務的任何現有實例。若是所請求的服務尚不存在,注入器就會使用之前註冊的服務提供商來製做一個,並把它加入注入器中,而後把該服務返回給 Angular。

當全部請求的服務已解析並返回時,Angular 能夠用這些服務實例爲參數,調用該組件的構造函數。

HeroService 的注入過程以下所示:

Service

提供服務

對於要用到的任何服務,你必須至少註冊一個提供商。你能夠在模塊中或者組件中註冊這些提供商。

  • 當你往根模塊中添加服務提供商時,服務的同一個實例會服務於你應用中的全部組件。

providers: [
  BackendService,
  HeroService,
  Logger
],
  • 當你在組件級註冊提供商時,你會爲該組件的每個新實例提供該服務的一個新實例。 要在組件級註冊,就要在 @Component 元數據的 providers 屬性中註冊服務提供商。
@Component({
  selector:    'app-hero-list',
  templateUrl: './hero-list.component.html',
  providers:  [ HeroService ]
})

模板與數據綁定

顯示數據

標籤選擇器:是 app-root。在index.html中就會做爲佔位符顯示。

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
<body>
  <app-root></app-root>
</body>

當經過main.ts的AppComponent類啓動時,Angular在index.html中查找一個<app-root>元素,而後實例化AppComponent渲染其中。 

在@Component中,模板分爲兩種類型

template:內聯模板

templateUrl:獨立模板

選擇內聯仍是獨立取決於我的喜愛、具體情況、組織及策略。若是模板很小能夠選擇內聯。

使用構造函數來進行初始化屬性,或者使用生命週期鉤子OnInit

*ngFor

模板中使用*ngFor指令來顯示一個數組中的每一項。*號必不可少

<ul>
    <li *ngFor="let hero of heroes">
      {{ hero }}
    </li>
  </ul>

優先選擇建立實體類,而後export導出使用

export class Hero {
  constructor(
    public id: number,
    public name: string) { }
}

導入後,直接使用實體類會更加方便

heroes = [
  new Hero(1, 'Windstorm'),
  new Hero(13, 'Bombasto'),
  new Hero(15, 'Magneta'),
  new Hero(20, 'Tornado')
];
myHero = this.heroes[0];

*NgIf

ngIf指令會根據一個布爾值來顯示或移出一個元素。*號必不可少。

<p *ngIf="heroes.length > 3">There are many heroes!</p>

模板中的HTML

HTML 是 Angular 模板的語言。幾乎全部的 HTML 語法都是有效的模板語法。 但值得注意的例外是 <script> 元素,它被禁用了,以阻止腳本注入攻擊的風險。

有些合法的 HTML 被用在模板中是沒有意義的。<html><body> 和 <base> 元素這個舞臺上中並無扮演有用的角色。剩下的全部元素基本上就都同樣用了。

插值表達式

{{myHero}} 。Angular會自動從組件中提取myHero屬性的值,而且插入到瀏覽器中。

屬性變化,會自動刷新。插入表達式通常爲相應屬性,也能夠調用宿主組件的方法。

<p>The sum of 1 + 1 is not {{1 + 1 + getVal()}}</p>

會先進行計算,在轉換爲字符串,因此{{1+1}} 內容爲2.

模板表達式

{{1+1}}所表明的模板表達式是1+1。在屬性綁定中會再次看到模板表達式。

例如:[property]="expression"

JavaScript 中那些具備或可能引起反作用的表達式是被禁止的,包括:

  • 賦值 (=+=-=, ...)

  • new 運算符

  • 使用 ; 或 , 的鏈式表達式

  • 自增和自減運算符:++ 和 --

和 JavaScript 語 法的其它顯著不一樣包括:

表達式上下文

表達式上下文就是組件實例,是各類綁定值的來源。例以下面的插入表達式和屬性表達式,表達式上下文就是export實體

{{title}}
<span [hidden]="isUnchanged">changed</span>

除了組件實體以外,上下文還能夠包括組件以外的對象。

<div *ngFor="let hero of heroes">{{hero.name}}</div>
<input #heroInput> {{heroInput.value}}

表達式中的上下文變量是由模板變量、指令的上下文變量和組件的成員疊加而成。

若是要引用的變量名存在一個以上的命名空間中,模板變量最優先,其次是指令的上下文、最後是組件的成員。

例如,上面代碼。若是組件實例包含hero屬性,而*ngFor也聲明瞭一個叫hero的模板變量,在表達式中實際引用的是模板變量。

表達式指南 

模板表達式就能成就或毀掉一個應用,請遵循下列指南。

1. 沒有可見的反作用

模板表達式除了目標屬性的值之外,不該該改變應用的任何狀態。

Angular單向數據流策略的基礎,不用擔憂讀取組件值可能改變另外的顯示值。單獨渲染中,視圖應老是穩定的。

2. 執行迅速

Angular執行模板表達式會很頻繁,表達式應該快速結束。當計算代價較高時,應該考慮緩存。

3. 很是簡單

常規是屬性名+方法的調用,不要寫的過於繁瑣。

4. 冪等性

最好使用冪等的表達式,由於沒有反作用,還能提高Angular變動檢測的性能。

冪等性含義

任意屢次執行所產生的影響均與一次執行的影響相同。指能夠使用相同參數重複執行,並能得到相同結果的函數。

這些函數不會影響系統狀態,也不用擔憂重複執行會對系統形成改變。

模板語句

模板語句用來響應綁定目標(HTML元素、組件、指令)觸發的事件。出如今=號右側。

<button (click)="deleteHero()">Delete hero</button>

模板語句有反作用

響應事件是 Angular 中「單向數據流」的另外一面。 在一次事件循環中,能夠隨意改變任何地方的任何東西。

和模板表達式同樣,模板語句使用的語言也像 JavaScript。某些 JavaScript 語法仍然是不容許的

  • new 運算符

  • 自增和自減運算符:++ 和 --

  • 操做並賦值,例如 += 和 -=

  • 位操做符 | 和 &

  • 模板表達式運算符

語句上下文

模板自身上下文中的屬性。下面例子分別是:傳遞$event對象、模板輸入變量、模板引用變量給組件中的一個事件處理方法。

<button (click)="onSave($event)">Save</button>
<button *ngFor="let hero of heroes" (click)="deleteHero(hero)">{{hero.name}}</button>
<form #heroForm (ngSubmit)="onSubmit(heroForm)"> ... </form>

模板上下文的變量名優先級高於組件上下文。例如deleteHero中的hero參數就是let的參數。

模板語句不能引用全局命名空間的任何東西。好比不能引用 window 或 document,也不能調用 console.log 或 Math.max

語句指南

避免複雜的模板語句,常規是函數調用或者屬性賦值。

綁定語法:概覽

數據綁定是一種機制,用來協調用戶所見和應用數據。

綁定的類型能夠根據數據流的方向分爲三種:從數據源到視圖、從視圖到數據源、雙向綁定

單向從數據源到視圖(插值表達式、屬性、Attribute、CSS樣式

{{expression}}
[target]="expression"

從視圖到數據源的單向綁定(事件

(target)="statement"
on-target="statement"

雙向綁定

[(target)]="expression"
bindon-target="expression"

除了插值表達式外,等號左邊是目標名,也就是屬性(Property)的名字。

新的思惟模型

數據綁定的威力和容許用自定義標記擴展 HTML 詞彙的能力,會讓你把模板 HTML 當成 HTML+

在正常的 HTML 開發過程當中,你使用 HTML 元素來建立視覺結構, 經過把字符串常量設置到元素的 attribute 來修改那些元素。

<div class="special">Mental Model</div>
<img src="assets/images/hero.png">
<button disabled>Save</button>

在 Angular 模板中,你仍使用一樣的方式建立結構和初始化 attribute 值。

而後,用封裝了 HTML 的組件建立新元素,並把它們看成原生 HTML 元素在模板中使用。

<div class="special">Mental Model</div>
<app-hero-detail></app-hero-detail>

這就是HTML+

<button [disabled]="isUnchanged">Save</button>

直覺告訴你,你正在綁定按鈕的 disabled attribute。 並把它設置爲組件的 isUnchanged 屬性的當前值。

但你的直覺是錯的!實際上,一旦開始數據綁定,就再也不跟 HTML attribute 打交道了。 這裏不是設置 attribute,而是設置 DOM 元素、組件和指令的 property。

HTML attribute 與 DOM property 的對比

attribute 是由 HTML 定義的。property 是由 DOM (Document Object Model) 定義的。

  • 少許 HTML attribute 和 property 之間有着 1:1 的映射,如 id

  • 有些 HTML attribute 沒有對應的 property,如 colspan

  • 有些 DOM property 沒有對應的 attribute,如 textContent

  • 大量 HTML attribute 看起來映射到了 property

attribute 初始化 DOM property。property 的值能夠改變;attribute 的值不能改變。

例如,當瀏覽器渲染 <input type="text" value="Bob"> 時,它將建立相應 DOM 節點, 它的 value 這個 property 被初始化爲 「Bob」。

當用戶在輸入框中輸入 「Sally」 時,DOM 元素的 value 這個 property 變成了 「Sally」。 可是該 HTML 的 value 這個 attribute 保持不變。若是你讀取 input 元素的 attribute,就會發現確實沒變: input.getAttribute('value') // 返回 "Bob"

HTML 的 value 這個 attribute 指定了初始值;DOM 的 value 這個 property 是當前值。

disabled 這個 attribute 是另外一種特例。按鈕的 disabled 這個 property 是 false,由於默認狀況下按鈕是可用的。 當你添加 disabled 這個 attribute 時,只要它出現了按鈕的 disabled 這個 property 就初始化爲 true,因而按鈕就被禁用了。

添加或刪除 disabled 這個 attribute 會禁用或啓用這個按鈕。但 attribute 的值可有可無,這就是你爲何無法經過 <button disabled="false">仍被禁用</button> 這種寫法來啓用按鈕。

設置按鈕的 disabled 這個 property(如,經過 Angular 綁定)能夠禁用或啓用這個按鈕。 這就是 property 的價值。

模板綁定是經過 property 和事件來工做的,而不是 attribute。

綁定目標

數據綁定的目標是DOM中的某些東西,多是屬性、事件、元素屬性。彙總以下

綁定類型

目標

範例

屬性

元素的 property

組件的 property

指令的 property

<img [src]="heroImageUrl"> <app-hero-detail [hero]="currentHero"></app-hero-detail>
<button [disabled]="isUnchanged">Cancel is disabled</button> <div [ngClass]="{'test':true,'test2':true}">[ngClass] 選擇綁定的class樣式(test\test2)</div>
<div [ngStyle]="{'background-color':'red','font-size':'1em'}">[ngStyle] 選擇綁定的style樣式</div>

事件

元素的事件

組件的事件

指令的事件

<button (click)="onSave()">Save</button> <app-hero-detail (deleteRequest)="deleteHero()"></app-hero-detail> <div (myClick)="clicked=$event" clickable>click me</div>

雙向

事件與 property

 <input [(ngModel)]="name">
Attribute

attribute(例外狀況)

   <button [attr.aria-label]="help">help</button>

CSS 類

class屬性

<div [class.special]="isSpecial">Special</div>
推薦使用ngClass

樣式

style屬性

<button [style.font-size.em]="1" [style.color]="isSpecial ? 'red' : 'green'">
推薦使用ngStyle

屬性綁定([屬性名])

在方括號之中的元素屬性名標記着目標屬性,例如 [src],就是src屬性。也能夠寫成bind-src。

使用ngClass能夠綁定元素擁有的class樣式。

不能在屬性綁定表達式中對任何東西賦值、不能使用自增、自減運算符。

模板表達式應當返回目標屬性所需類型的值,若是屬性想要字符串就返回字符串,想要對象就返回對象。

<app-hero-detail [hero]="currentHero"></app-hero-detail>

方括號告訴Angular要計算模板表達式,若是忘了加方括號,就當作字符串常量來看待。

一次性字符串初始化

當知足下列條件是,應當省略方括號

1.目標屬性接受字符串

2.字符串是固定不變的,能夠直接合併到模塊中。

3.初始值永遠不變

<app-hero-detail prefix="You are my" [hero]="currentHero"></app-hero-detail>

屬性綁定和插值表達式效果一致。若是數據類型不是字符串,必須使用屬性綁定!!!

無論是插值表達式仍是屬性表達式,都不容許帶有script標籤的HTML

$event和事件處理語句

事件綁定中,AngularJS會爲目標事件設置事件處理器。

當事件發生時,處理器會執行模板語句,一般涉及到響應事件執行動做的接收器。

綁定會經過名叫$event的事件對象傳遞關於此事件的信息(包括數值),若是目標是原生DOM元素事件,$event就是DOM事件對象,有target和target.value

<input [value]="currentHero.name"
       (input)="currentHero.name=$event.target.value" >

上面代碼把輸入框的value屬性綁定到currentHero.name屬性上,要監聽對值的修改,代碼綁定到輸入框input事件。

用戶形成更改時,input事件被觸發,幷包含在DOM事件對象($event)的上下文中執行這條語句。

要更新currentHero.name屬性,就須要經過$event.target.value來獲取更改後的值。

<input [value]="name" (input)="name=$event.target.value"> 
等於
<input [(ngModel)]="name">

EventEmitter實現自定義事件

指令使用Angular EventEmitter來觸發自定義事件。建立一個EventEmitter實例,而且把它做爲屬性暴露出來。

指令調用EventEmitter.emit(payload)來觸發事件,能夠傳入任何東西做爲信息載體。

父指令經過綁定到這個屬性來監聽事件,並經過$event對象來訪問載荷。

首先須要定義EventEmitter實例,而後設置調用規則。注意:@Output()必須存在,否則不起做用。

@Component({
  selector: 'app-componet-del',
  template: `
    <div>This is Componet</div>
    <input [(ngModel)]="name">
    <button (click)="del()">Del</button>
  `
})

export class Componet {
  public name: string;
  @Output() deleteRequest = new EventEmitter<string>();
  del(): void {
    this.deleteRequest.emit(this.name);
  }
}

父組件頁面直接定義自定義事件和觸發方法便可。

<app-componet-del (deleteRequest)="Coo($event)"></app-componet-del>

Coo(event) {
    debugger;
    console.log(`123`);
  }

雙向數據綁定

須要顯示數據屬性,而且在用戶做出更改時更新此屬性。

Angular提供一種特殊的雙向綁定語法:[(x)]。語法結合了屬性綁定[]、事件綁定()。盒子裏頭有香蕉

下面是一個自定義事件的雙向綁定示例。會把fontSizePx賦值給size屬性,而後自定義事件觸發修改。

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

@Component({
  selector: 'app-sizer',
  template: `
    <div>
      <button (click)="dec()" title="smaller">-</button>
      <button (click)="inc()" title="bigger">+</button>
      <label [style.font-size.px]="size">FontSize: {{size}}px</label>
    </div>`
})
export class SizerComponent {
  @Input() size: number | string;
  @Output() sizeChange = new EventEmitter<number>();

  dec() {
    this.resize(-1);
  }

  inc() {
    this.resize(+1);
  }

  resize(delta: number) {
    this.size = Math.min(40, Math.max(8, +this.size + delta));
    this.sizeChange.emit(this.size);
  }
}
<app-sizer [(size)]="fontSizePx"></app-sizer>
<app-sizer [size]="fontSizePx" (sizeChange)="fontSizePx=$event"></app-sizer>

<div [style.font-size.px]="fontSizePx">Resizable Text</div>

實際上[(size)],就是[size] (sizeChange)的語法糖。

$event裏面就是sizeChange的參數,點擊按鈕時,Angular將$event賦值給fontSizePx。

比起單獨綁定屬性和事件,雙向綁定語法顯得很是方便。可是雙向綁定須要遵循x值和xChange事件的模式,原生HTML不支持。

幸運的是,Angular和ngModel指令爲橋樑,容許在表單元素上使用雙向綁定。ngModel會本身設定xChange事件

內置屬性型指令

屬性指令會監聽和修改其它HTML元素或組件的行爲、元素屬性、DOM屬性。一般會做爲HTML屬性的名稱應用在元素上。

NgClass

刪除單個類最佳作法

<div [class.special]="isSpecial">The class binding is special</div>

動態添加或刪除Class類的方法,能夠同時添加或移出多個類。

ngClass須要綁定到一個Key:Value對象上,key是css類名,vlaue是true。

ngClass的值也能夠綁定屬性,只要屬性是對象形式便可。

this.currentClasses =  {
    'saveable': this.canSave,
    'modified': !this.isUnchanged,
    'special':  this.isSpecial
  };

<div [ngClass]="currentClasses">This div is initially</div> 

NgStyle

設置單同樣式值最佳作法

<div [style.font-size]="isSpecial ? 'x-large' : 'smaller'" >
This div is x-large or smaller.
</div>

能夠綁定多個內聯樣式。

ngStyle須要綁定一個Key:Value對象上,key是樣式名,value是用於這個樣式的值。

ngStyle的值也能夠綁定屬性,只要屬性是對象形式便可。

this.currentStyles = {
    'font-style':  this.canSave      ? 'italic' : 'normal',
    'font-weight': !this.isUnchanged ? 'bold'   : 'normal',
    'font-size':   this.isSpecial    ? '24px'   : '12px'
  };

<div [ngStyle]="currentStyles">
  This div is initially italic, normal weight, and extra large (24px).
</div>

NgModel

表單操做時,既要顯示數據屬性又要根據用戶的更改去修改那個屬性。

使用ngModel能夠進行簡化雙向綁定的操做。注意:使用ngModel時須要FormModule

<input [(ngModel)]="currentHero.name">
<input [ngModel]="currentHero.name" (ngModelChange)="currentHero.name=$event">

ngModel指令經過本身的輸入屬性ngModel和輸出屬性ngModelChange隱藏了事件細節。

若是咱們須要特殊的事件處理,也能夠寫成以下形式

<input
  [ngModel]="currentHero.name"
  (ngModelChange)="setUppercaseName($event)">

內置結構型命令

結構型命令的職責是HTML佈局,塑造或重塑DOM的結構,一般是經過添加、移出它們所附加到的宿主元素來實現的。

NgIf

*ngIf指令應用到元素上,能夠往DOM中添加或者移出這個元素。*號必不可少

<app-hero-detail *ngIf="isActive"></app-hero-detail>

NgFor

重複器指令,自定義數據顯示的一種方法。綁定到元素上,會渲染列表中的每個條目,並生成一個元素。

<div *ngFor="let hero of heroes">{{hero.name}}</div>

*ngFor指令上下文中有index屬性,能夠返回一個從零開始的索引,表示迭代中的順序。

能夠經過模板輸入變量捕獲這個index值,並把它用在模板中。

<div *ngFor="let hero of heroes; let i=index">{{i + 1}} - {{hero.name}}</div>

trackBy的*ngFor

trackBy能夠防止Angular更新所有DOM。往組件中添加一個方法,會返回NgFor應該追蹤的值。

trackByHeroes(index: number, hero: Hero): number { return hero.id; }

<div *ngFor="let hero of heroes; trackBy: trackByHeroes">
  ({{hero.id}}) {{hero.name}}
</div>

若是建立的是相同的hero.id,那麼頁面不會刷新DOM樹。只有id變換了,纔會刷新DOM樹

NgSwitch

[ngSwitch]相似於JS中的Switch,能夠從多個可能的元素中根據switch條件來顯示某一個。Angular只會把選中的元素放進DOM中。

NgSwitch包含三個相互協做指令,NgSwitch、NgSwitchCase、NgSwitchDefault

<div [ngSwitch]="currentHero.emotion">
  <app-happy-hero    *ngSwitchCase="'happy'"    [hero]="currentHero"></app-happy-hero>
  <app-sad-hero      *ngSwitchCase="'sad'"      [hero]="currentHero"></app-sad-hero>
  <app-confused-hero *ngSwitchCase="'confused'" [hero]="currentHero"></app-confused-hero>
  <app-unknown-hero  *ngSwitchDefault           [hero]="currentHero"></app-unknown-hero>
</div>

模板引用變量(#var)

用來引用模板中的某個DOM元素,還能夠引用Angular組件或指令

使用#號來聲明引用變量,#phone的意思是聲明一個叫phone的變量來引用<input>元素

能夠在模板的任何地方引用模板引用變量,好比聲明在input上的變量在另外一邊也能夠直接使用

<input #phone placeholder="phone number">

<button (click)="callPhone(phone.value)">Call</button>

大多數狀況下,Angular會把模板引用變量的值設置爲聲明它的內個元素。

<form (ngSubmit)="onSubmit(heroForm)" #heroForm="ngForm">
  <div class="form-group">
    <label for="name">Name
      <input class="form-control" name="name" required [(ngModel)]="hero.name">
    </label>
  </div>
  <button type="submit" [disabled]="!heroForm.form.valid">Submit</button>
</form>
<div [hidden]="!heroForm.form.valid">
  {{submitMessage}}
</div>

上圖代碼中模板引用變量heroForm出現了三次。這裏的heroForm就是Angular NgForm指令的引用,具有跟蹤表單每一個控件值和有效性的能力。

模板引用變量的做用範圍是整個模板,不要在同一個模板中定義屢次,不然會沒法肯定。

也能夠使用ref-前綴代替#,例如 

<input ref-phone placeholder="phone number">

輸出和輸入屬性

綁定目標是在=號左側部分,綁定源是=號右側部分

綁定的目標是綁定符:[]、()、[()]中的屬性或事件名

綁定源則是引號中的部分或插值符號中的部分。

<img [src]="iconUrl"/>
<button (click)="onSave()">Save</button>

上面代碼中src是綁定目標的屬性綁定,iconUrl是綁定源的屬性

目標屬性必須被顯式的標記爲輸入或輸出。@Input() 和 @Output

@Input()  hero: Hero;
@Output() deleteRequest = new EventEmitter<Hero>();

另外也能夠在@Component裏面寫出輸出數組,使用inputs和outputs

@Component({
  inputs: ['hero'],
  outputs: ['deleteRequest'],
})

兩種方法不要同時使用。

輸入仍是輸出? 

輸入屬性一般接受數據值。輸出屬性一般暴露事件產生者,例如:EventEmitter對象

輸入和輸出是從目標指令的角度來看的。

 

hero是輸入由於數據流從模板綁定表達式流入那個屬性

deleteRequest是輸出由於事件從那個屬性流出,流向模板綁定語句中的處理器

別名

能夠把別名傳進@Input或者@Output裝飾器,就能夠爲屬性指定別名。或者在數組中指定別名。

@Output('myClick') clicks = new EventEmitter<string>();

  inputs:['size'],
  outputs:['sizeChange:Change']

模板表達式操做符

管道操做符(|)

在綁定以前,表達式的結果可能須要一些轉換。例如,數字顯示成金額、強制文本大寫、過濾列表

Angular管道對於這樣的小型轉換是很是好的選擇,管道就是一個簡單的函數,接受一個輸入值,並返回結果。

<div>Title through uppercase pipe: {{title | uppercase | lowercase}}</div>

管道操做符會把它左側表達式結果傳遞給他右側的管道函數,能夠串聯起來。Angular一共16個管道函數。使用管道函數不要寫Pipe

管道函數API:https://angular.cn/api?type=pipe

全部管道均在 '@angular/common';,使用以前記得引入:import { AsyncPipe } from '@angular/common';

AsyncPipe:更新Promise\Observable的狀態,讓他當即返回結果

DatePipe:根據當前環境格式化日期

I18nPluralPipe:將值映射到根據區域設置規則將值複數化的字符串。

I18nSelectPipe:顯示與當前值匹配的字符串的通用選擇器。

JsonPipe:將值轉換爲JSON字符串。

LowerCasePipe:將文本轉換爲小寫。

CurrencyPipe:使用區域設置規則將數字格式化爲貨幣。

DecimalPipe:根據區域設置規則格式化數字。

PercentPipe:根據語言環境規則將數字格式化爲百分比。

SlicePipe:建立包含元素子集(切片)的新List或String。

UpperCasePipe:將文本轉換爲大寫。

TitleCasePipe:將文本轉換爲標題。

DeprecatedDatePipe:根據語言環境規則格式化日期。

DeprecatedCurrencyPipe:使用區域設置規則將數字格式化爲貨幣。

DeprecatedDecimalPipe:根據區域設置規則格式化數字。

DeprecatedPercentPipe:根據語言環境規則將數字格式化爲百分比。

安全導航操做符(?.)

安全導航操做符?.是一種流暢而便利的方式,用來保護出如今屬性路徑中 null 和 undefined 值。 下例中,當 currentHero 爲空時,保護視圖渲染器,讓它免於失敗

The current hero's name is {{currentHero?.name}}

Angular 安全導航操做符 (?.) 是在屬性路徑中保護空值的更加流暢、便利的方式。 表達式會在它遇到第一個空值的時候跳出。 顯示是空的,但應用正常工做,而沒有發生錯誤。

非空斷言操做符(!.)

有類型的變量默認是不容許 null 或 undefined 值的,若是有未賦值的變量,或者試圖把 null 或 undefined 賦值給不容許爲空的變量,類型檢查器就會拋出一個錯誤。

若是類型檢查器在運行期間沒法肯定一個變量是 null 或 undefined,那麼它也會拋出一個錯誤。 你本身可能知道它不會爲空,但類型檢查器不知道。 因此你要告訴類型檢查器,它不會爲空,這時就要用到非空斷言操做符

The hero's name is {{hero!.name}}

在 Angular 編譯器把你的模板轉換成 TypeScript 代碼時,這個操做符會防止 TypeScript 報告 "hero.name 可能爲 null 或 undefined"的錯誤。

類型轉換函數$any

有時候,綁定表達式可能會報類型錯誤,而且它不能或很難指定類型。要消除這種報錯,你能夠使用 $any 轉換函數來把表達式轉換成 any 類型

The hero's marker is {{$any(hero).marker}}

當 Angular 編譯器把模板轉換成 TypeScript 代碼時,$any 表達式能夠防止 TypeScript 編譯器報錯說 marker 不是 Hero 接口的成員。

$any 轉換函數能夠和 this 聯合使用,以便訪問組件中未聲明過的成員。

Undeclared members is {{$any(this).member}}

生命週期鉤子

每一個組件都有一個被Angular管理的生命週期。建立、渲染、並渲染子組件,屬性變化時檢查,移出前銷燬

Angular提供了生命週期的鉤子,把這些生命週期暴露出來,賦予咱們在它發生時採起行動的能力。

每一個接口都有惟一的一個鉤子方法,名字都是由接口名+ng前綴構成的。好比,下方OnInit接口

export class PeekABoo implements OnInit {
  constructor(private logger: LoggerService) { }

  // implement OnInit's `ngOnInit` method
  ngOnInit() { this.logIt(`OnInit`); }

  logIt(msg: string) {
    this.logger.log(`#${nextId++} ${msg}`);
  }
}

生命週期的順序 

當使用構造函數新建一個組件或指令後,按下面順序在特定時刻調用這些生命鉤子

鉤子

目的和時機

ngOnChanges()

當Angular(從新)設置數據綁定輸入屬性時響應。 該方法接受當前和上一屬性值的SimpleChanges對象

當被綁定的輸入屬性的值發生變化時調用,首次調用必定會發生在ngOnInit()以前。

ngOnInit()

在Angular第一次顯示數據綁定和設置指令/組件的輸入屬性以後,初始化指令/組件。

在第一輪ngOnChanges()完成以後調用,只調用一次。

ngDoCheck()

檢測,並在發生Angular沒法或不肯意本身檢測的變化時做出反應。

在每一個Angular變動檢測週期中調用,ngOnChanges()ngOnInit()以後。

ngAfterContentInit()

當把內容投影進組件以後調用。

第一次ngDoCheck()以後調用,只調用一次。

只適用於組件。

ngAfterContentChecked()

每次完成被投影組件內容的變動檢測以後調用。

ngAfterContentInit()和每次ngDoCheck()以後調用

只適合組件。

ngAfterViewInit()

初始化完組件視圖及其子視圖以後調用。

第一次ngAfterContentChecked()以後調用,只調用一次。

只適合組件。

ngAfterViewChecked()

每次作完組件視圖和子視圖的變動檢測以後調用。

ngAfterViewInit()和每次ngAfterContentChecked()以後調用。

只適合組件。

ngOnDestroy

當Angular每次銷燬指令/組件以前調用並清掃。 在這兒反訂閱可觀察對象和分離事件處理器,以防內存泄漏。

在Angular銷燬指令/組件以前調用。

組件之間的交互

經過輸入型綁定能夠把數據從父組件傳到子組件

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

import { Hero } from './hero';

@Component({
  selector: 'app-hero-child',
  template: `
    <h3>{{hero.name}} says:</h3>
    <p>I, {{hero.name}}, am at your service, {{masterName}}.</p>
  `
})
export class HeroChildComponent {
  @Input() hero: Hero;
  @Input('master') masterName: string;
}

第二個 @Input 爲子組件的屬性名 masterName 指定一個別名 master

父組件 HeroParentComponent 把子組件的 HeroChildComponent 放到 *ngFor 循環器中,把本身的 master 字符串屬性綁定到子組件的 master 別名上,並把每一個循環的 hero 實例綁定到子組件的 hero 屬性。

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

import { HEROES } from './hero';

@Component({
  selector: 'app-hero-parent',
  template: `
    <h2>{{master}} controls {{heroes.length}} heroes</h2>
    <app-hero-child *ngFor="let hero of heroes"
      [hero]="hero"
      [master]="master">
    </app-hero-child>
  `
})
export class HeroParentComponent {
  heroes = HEROES;
  master = 'Master';
}

經過setter截聽輸入屬性值的變化

使用一個輸入屬性的 setter,以攔截父組件中值的變化,並採起行動。

子組件 NameChildComponent 的輸入屬性 name 上的這個 setter,會 trim 掉名字裏的空格,並把空值替換成默認字符串。

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

@Component({
  selector: 'app-name-child',
  template: '<h3>"{{name}}"</h3>'
})
export class NameChildComponent {
  private _name = '';

  @Input()
  set name(name: string) {
    this._name = (name && name.trim()) || '<no name set>';
  }

  get name(): string { return this._name; }
}
import { Component } from '@angular/core';

@Component({
  selector: 'app-name-parent',
  template: `
  <h2>Master controls {{names.length}} names</h2>
  <app-name-child *ngFor="let name of names" [name]="name"></app-name-child>
  `
})
export class NameParentComponent {
  // Displays 'Mr. IQ', '<no name set>', 'Bombasto'
  names = ['Mr. IQ', '   ', '  Bombasto  '];
}

經過ngOnChanges()來截聽輸入屬性值的變化

使用 OnChanges 生命週期鉤子接口的 ngOnChanges() 方法來監測輸入屬性值的變化並作出迴應。

VersionChildComponent 會監測輸入屬性 major 和 minor 的變化,並把這些變化編寫成日誌以報告這些變化。

import { Component, Input, OnChanges, SimpleChange } from '@angular/core';

@Component({
  selector: 'app-version-child',
  template: `
    <h3>Version {{major}}.{{minor}}</h3>
    <h4>Change log:</h4>
    <ul>
      <li *ngFor="let change of changeLog">{{change}}</li>
    </ul>
  `
})
export class VersionChildComponent implements OnChanges {
  @Input() major: number;
  @Input() minor: number;
  changeLog: string[] = [];

  ngOnChanges(changes: {[propKey: string]: SimpleChange}) {
    let log: string[] = [];
    for (let propName in changes) {
      let changedProp = changes[propName];
      let to = JSON.stringify(changedProp.currentValue);
      if (changedProp.isFirstChange()) {
        log.push(`Initial value of ${propName} set to ${to}`);
      } else {
        let from = JSON.stringify(changedProp.previousValue);
        log.push(`${propName} changed from ${from} to ${to}`);
      }
    }
    this.changeLog.push(log.join(', '));
  }
}

VersionParentComponent 提供 minor 和 major 值,把修改它們值的方法綁定到按鈕上。

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

@Component({
  selector: 'app-version-parent',
  template: `
    <h2>Source code version</h2>
    <button (click)="newMinor()">New minor version</button>
    <button (click)="newMajor()">New major version</button>
    <app-version-child [major]="major" [minor]="minor"></app-version-child>
  `
})
export class VersionParentComponent {
  major = 1;
  minor = 23;

  newMinor() {
    this.minor++;
  }

  newMajor() {
    this.major++;
    this.minor = 0;
  }
}

 

父組件監聽子組件的事件

子組件暴露一個 EventEmitter 屬性,當事件發生時,子組件利用該屬性 emits(向上彈射)事件。父組件綁定到這個事件屬性,並在事件發生時做出迴應。

子組件的 EventEmitter 屬性是一個輸出屬性,一般帶有@Output 裝飾器,就像在 VoterComponent 中看到的。

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

@Component({
  selector: 'app-voter',
  template: `
    <h4>{{name}}</h4>
    <button (click)="vote(true)"  [disabled]="voted">Agree</button>
    <button (click)="vote(false)" [disabled]="voted">Disagree</button>
  `
})
export class VoterComponent {
  @Input()  name: string;
  @Output() onVoted = new EventEmitter<boolean>();
  voted = false;

  vote(agreed: boolean) {
    this.onVoted.emit(agreed);
    this.voted = true;
  }
}

點擊按鈕會觸發 true 或 false(布爾型有效載荷)的事件。

父組件 VoteTakerComponent 綁定了一個事件處理器(onVoted()),用來響應子組件的事件($event)並更新一個計數器。

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

@Component({
  selector: 'app-vote-taker',
  template: `
    <h2>Should mankind colonize the Universe?</h2>
    <h3>Agree: {{agreed}}, Disagree: {{disagreed}}</h3>
    <app-voter *ngFor="let voter of voters"
      [name]="voter"
      (onVoted)="onVoted($event)">
    </app-voter>
  `
})
export class VoteTakerComponent {
  agreed = 0;
  disagreed = 0;
  voters = ['Mr. IQ', 'Ms. Universe', 'Bombasto'];

  onVoted(agreed: boolean) {
    agreed ? this.agreed++ : this.disagreed++;
  }
}

父組件與子組件經過本地變量互動

父組件不能使用數據綁定來讀取子組件的屬性或調用子組件的方法。但能夠在父組件模板裏,新建一個本地變量來表明子組件,而後利用這個變量來讀取子組件的屬性和調用子組件的方法,以下例所示。

子組件 CountdownTimerComponent 進行倒計時,歸零時發射一個導彈。start 和 stop 方法負責控制時鐘並在模板裏顯示倒計時的狀態信息。

import { Component, OnDestroy, OnInit } from '@angular/core';

@Component({
  selector: 'app-countdown-timer',
  template: '<p>{{message}}</p>'
})
export class CountdownTimerComponent implements OnInit, OnDestroy {

  intervalId = 0;
  message = '';
  seconds = 11;

  clearTimer() { clearInterval(this.intervalId); }

  ngOnInit()    { this.start(); }
  ngOnDestroy() { this.clearTimer(); }

  start() { this.countDown(); }
  stop()  {
    this.clearTimer();
    this.message = `Holding at T-${this.seconds} seconds`;
  }

  private countDown() {
    this.clearTimer();
    this.intervalId = window.setInterval(() => {
      this.seconds -= 1;
      if (this.seconds === 0) {
        this.message = 'Blast off!';
      } else {
        if (this.seconds < 0) { this.seconds = 10; } // reset
        this.message = `T-${this.seconds} seconds and counting`;
      }
    }, 1000);
  }
}

計時器組件的宿主組件 CountdownLocalVarParentComponent 以下:

import { Component }                from '@angular/core';
import { CountdownTimerComponent }  from './countdown-timer.component';

@Component({
  selector: 'app-countdown-parent-lv',
  template: `
  <h3>Countdown to Liftoff (via local variable)</h3>
  <button (click)="timer.start()">Start</button>
  <button (click)="timer.stop()">Stop</button>
  <div class="seconds">{{timer.seconds}}</div>
  <app-countdown-timer #timer></app-countdown-timer>
  `,
  styleUrls: ['../assets/demo.css']
})
export class CountdownLocalVarParentComponent { }

父組件不能經過數據綁定使用子組件的 start 和 stop 方法,也不能訪問子組件的 seconds 屬性。

把本地變量(#timer)放到(<countdown-timer>)標籤中,用來表明子組件。這樣父組件的模板就獲得了子組件的引用,因而能夠在父組件的模板中訪問子組件的全部屬性和方法。

這個例子把父組件的按鈕綁定到子組件的 start 和 stop 方法,並用插值表達式來顯示子組件的 seconds 屬性。

父組件調用@ViewChild()

這個本地變量方法是個簡單便利的方法。可是它也有侷限性,由於父組件-子組件的鏈接必須所有在父組件的模板中進行。父組件自己的代碼對子組件沒有訪問權。

若是父組件的類須要讀取子組件的屬性值或調用子組件的方法,就不能使用本地變量方法。

當父組件類須要這種訪問時,能夠把子組件做爲 ViewChild,注入到父組件裏面

import { AfterViewInit, ViewChild } from '@angular/core';
import { Component }                from '@angular/core';
import { CountdownTimerComponent }  from './countdown-timer.component';

@Component({
  selector: 'app-countdown-parent-vc',
  template: `
  <h3>Countdown to Liftoff (via ViewChild)</h3>
  <button (click)="start()">Start</button>
  <button (click)="stop()">Stop</button>
  <div class="seconds">{{ seconds() }}</div>
  <app-countdown-timer></app-countdown-timer>
  `,
  styleUrls: ['../assets/demo.css']
})
export class CountdownViewChildParentComponent implements AfterViewInit {

  @ViewChild(CountdownTimerComponent)
  private timerComponent: CountdownTimerComponent;

  seconds() { return 0; }

  ngAfterViewInit() {
    // Redefine `seconds()` to get from the `CountdownTimerComponent.seconds` ...
    // but wait a tick first to avoid one-time devMode
    // unidirectional-data-flow-violation error
    setTimeout(() => this.seconds = () => this.timerComponent.seconds, 0);
  }

  start() { this.timerComponent.start(); }
  stop() { this.timerComponent.stop(); }
}

把子組件的視圖插入到父組件類須要作一點額外的工做。

首先,你要使用 ViewChild 裝飾器導入這個引用,並掛上 AfterViewInit 生命週期鉤子。

接着,經過 @ViewChild 屬性裝飾器,將子組件 CountdownTimerComponent 注入到私有屬性 timerComponent 裏面。

組件元數據裏就再也不須要 #timer 本地變量了。而是把按鈕綁定到父組件本身的 start 和 stop 方法,使用父組件的 seconds 方法的插值表達式來展現秒數變化。

這些方法能夠直接訪問被注入的計時器組件。

ngAfterViewInit() 生命週期鉤子是很是重要的一步。被注入的計時器組件只有在 Angular 顯示了父組件視圖以後才能訪問,因此它先把秒數顯示爲 0.

而後 Angular 會調用 ngAfterViewInit 生命週期鉤子,但這時候再更新父組件視圖的倒計時就已經太晚了。Angular 的單向數據流規則會阻止在同一個週期內更新父組件視圖。應用在顯示秒數以前會被迫再等一輪。

使用 setTimeout() 來等下一輪,而後改寫 seconds() 方法,這樣它接下來就會從注入的這個計時器組件裏獲取秒數的值。

父組件和子組件經過服務來通信

父組件和它的子組件共享同一個服務,利用該服務在家庭內部實現雙向通信。

該服務實例的做用域被限制在父組件和其子組件內。這個組件子樹以外的組件將沒法訪問該服務或者與它們通信。

這個 MissionService 把 MissionControlComponent 和多個 AstronautComponent 子組件鏈接起來。

import { Injectable } from '@angular/core';
import { Subject }    from 'rxjs';

@Injectable()
export class MissionService {

  // Observable string sources
  private missionAnnouncedSource = new Subject<string>();
  private missionConfirmedSource = new Subject<string>();

  // Observable string streams
  missionAnnounced$ = this.missionAnnouncedSource.asObservable();
  missionConfirmed$ = this.missionConfirmedSource.asObservable();

  // Service message commands
  announceMission(mission: string) {
    this.missionAnnouncedSource.next(mission);
  }

  confirmMission(astronaut: string) {
    this.missionConfirmedSource.next(astronaut);
  }
}

MissionControlComponent 提供服務的實例,並將其共享給它的子組件(經過 providers 元數據數組),子組件能夠經過構造函數將該實例注入到自身。

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

import { MissionService }     from './mission.service';

@Component({
  selector: 'app-mission-control',
  template: `
    <h2>Mission Control</h2>
    <button (click)="announce()">Announce mission</button>
    <app-astronaut *ngFor="let astronaut of astronauts"
      [astronaut]="astronaut">
    </app-astronaut>
    <h3>History</h3>
    <ul>
      <li *ngFor="let event of history">{{event}}</li>
    </ul>
  `,
  providers: [MissionService]
})
export class MissionControlComponent {
  astronauts = ['Lovell', 'Swigert', 'Haise'];
  history: string[] = [];
  missions = ['Fly to the moon!',
              'Fly to mars!',
              'Fly to Vegas!'];
  nextMission = 0;

  constructor(private missionService: MissionService) {
    missionService.missionConfirmed$.subscribe(
      astronaut => {
        this.history.push(`${astronaut} confirmed the mission`);
      });
  }

  announce() {
    let mission = this.missions[this.nextMission++];
    this.missionService.announceMission(mission);
    this.history.push(`Mission "${mission}" announced`);
    if (this.nextMission >= this.missions.length) { this.nextMission = 0; }
  }
}

AstronautComponent 也經過本身的構造函數注入該服務。因爲每一個 AstronautComponent 都是 MissionControlComponent 的子組件,因此它們獲取到的也是父組件的這個服務實例。

import { Component, Input, OnDestroy } from '@angular/core';

import { MissionService } from './mission.service';
import { Subscription }   from 'rxjs';

@Component({
  selector: 'app-astronaut',
  template: `
    <p>
      {{astronaut}}: <strong>{{mission}}</strong>
      <button
        (click)="confirm()"
        [disabled]="!announced || confirmed">
        Confirm
      </button>
    </p>
  `
})
export class AstronautComponent implements OnDestroy {
  @Input() astronaut: string;
  mission = '<no mission announced>';
  confirmed = false;
  announced = false;
  subscription: Subscription;

  constructor(private missionService: MissionService) {
    this.subscription = missionService.missionAnnounced$.subscribe(
      mission => {
        this.mission = mission;
        this.announced = true;
        this.confirmed = false;
    });
  }

  confirm() {
    this.confirmed = true;
    this.missionService.confirmMission(this.astronaut);
  }

  ngOnDestroy() {
    // prevent memory leak when component destroyed
    this.subscription.unsubscribe();
  }
}

在父組件 MissionControlComponent 和子組件 AstronautComponent 之間,信息經過該服務實現了雙向傳遞。

組件樣式

Angular 應用使用標準的 CSS 來設置樣式。這意味着你能夠把關於 CSS 的那些知識和技能直接用於 Angular 程序中,例如:樣式表、選擇器、規則以及媒體查詢等。

另外,Angular 還能把組件樣式捆綁在組件上,以實現比標準樣式表更加模塊化的設計。

:host選擇器

使用 :host 僞類選擇器,用來選擇組件宿主元素中的元素(相對於組件模板內部的元素)。

:host {
  display: block;
  border: 1px solid black;
}

 

:host 選擇是是把宿主元素做爲目標的惟一方式。除此以外,你將沒辦法指定它, 由於宿主不是組件自身模板的一部分,而是父組件模板的一部分。 

:host-context選擇器

使用 :host-context() 僞類選擇器。它也以相似 :host() 形式使用。它在當前組件宿主元素的祖先節點中查找 CSS 類, 直到文檔的根節點爲止。在與其它選擇器組合使用時,它很是有用。

下面的例子中,只有當某個祖先元素有 CSS 類 theme-light 時,纔會把 background-color 樣式應用到組件內部的全部 <h2> 元素中。

:host-context(.theme-light) h2 {
  background-color: #eef;
}

 

Angular元素(Elements)概覽

https://angular.cn/guide/elements

動態組件加載器

組件的模板不會永遠是固定的。應用可能會須要在運行期間加載一些新的組件。

新的組件加載方式,不須要在模板中引用固定的組件

指令

定義錨點告訴Angular要把組件插入的地方。 

//ad.directive
import {Directive, ViewContainerRef} from '@angular/core';

@Directive({
  selector: '[ad-host]',
})
export class AdDirective {
  constructor(public viewContainerRef: ViewContainerRef) {
  }
}

注入ViewContainerRef來獲取對容器的訪問權,這就是動態加入的組件宿主。

@Directive定義的名字ad-host,就是要應用到元素上的指令。

而後咱們須要一個組件容器,來裝這些組件。建立ad-item.ts

//ad-item
import { Type } from '@angular/core';

export class AdItem {
  constructor(public component: Type<any>, public data: any) {}
}

接着咱們須要一個廣告接口,用來保證動態的組件所必要的屬性。建立ad.component.ts

//ad.component.ts
export interface AdComponent {
  data: any;
}

而後咱們建立廣告切換顯示組件,實現代碼都在這個組件中。建立ad-banner.component.ts

ng-template,就是剛纔製做的指令將應用到的地方。ng-template元素時動態加載最佳選擇,不會渲染任何額外的輸出。

//ad-banner.component.ts

import {Component, Input, AfterViewInit, ViewChild, ComponentFactoryResolver, OnDestroy} from '@angular/core';

import {AdDirective} from './ad.directive';
import {AdItem} from './ad-item';
import {AdComponent} from './ad.component';

@Component({
  selector: 'app-add-banner',
  template: `
    <div class="ad-banner">
      <h3>Advertisements</h3>
      <ng-template ad-host></ng-template>
    </div>
  `
})
export class AdBannerComponent implements AfterViewInit, OnDestroy {
  @Input() ads: AdItem[];
  currentAddIndex: number = -1;
  @ViewChild(AdDirective) adHost: AdDirective;
  subscription: any;
  interval: any;

  constructor(private componentFactoryResolver: ComponentFactoryResolver) {
  }

  ngAfterViewInit() {
    this.loadComponent();
    this.getAds();
  }

  ngOnDestroy() {
    clearInterval(this.interval);
  }

  loadComponent() {
    this.currentAddIndex = (this.currentAddIndex + 1) % this.ads.length;
    let adItem = this.ads[this.currentAddIndex];
    let componentFactory = this.componentFactoryResolver.resolveComponentFactory(adItem.component);
    let viewContainerRef = this.adHost.viewContainerRef;
    viewContainerRef.clear();
    let componentRef = viewContainerRef.createComponent(componentFactory);
    (<AdComponent>componentRef.instance).data = adItem.data;
  }

  getAds() {
    this.interval = setInterval(() => {
      this.loadComponent();
    }, 3000);
  }
}

AdBannerComponent接受一個AdItem輸入對象數組做爲輸入。AdItem指定要加載的組件類,已經綁定上的任意數據。

經過getAds()方法,循環遍歷AdItem數組,並每三秒調用一次。

loadComponent方法使用循環選取算法,來選擇廣告。以後使用ComponentFactoryResolver來爲每一個組件解析一個ComponentFactor。

而後把viewContainerRef指向這個組件的現有實例,這個實例就是咱們以前設定的指令adHost。

AdDirective的構造函數注入了ViewContainerRef,這個指令能夠訪問用做動態組件宿主的元素。

使用ViewContainerRef的createComponent方法返回一個引用,指向這個剛剛加載的組件。

對選擇器的引用

angular編譯器會爲模板中所引用的每一個組件都生成一個ComponentFactory類。

可是對於動態加載的組件,模板中不會出現對它們的選擇器的引用。

須要動態加載組件到app.module.ts的entryComponents數組中

entryComponents: [ HeroJobAdComponent, HeroProfileComponent ],

接下來,建立兩個範例組件

//hero-job-ad.component.ts
import { Component, Input } from '@angular/core';

import { AdComponent }      from './ad.component';

@Component({
  template: `
    <div class="job-ad">
      <h4>{{data.headline}}</h4> 
      
      {{data.body}}
    </div>
  `
})
export class HeroJobAdComponent implements AdComponent {
  @Input() data: any;
}
//hero-profile.component
import { Component, Input }  from '@angular/core';

import { AdComponent }       from './ad.component';

@Component({
  template: `
    <div class="hero-profile">
      <h3>Featured Hero Profile</h3>
      <h4>{{data.name}}</h4>
      
      <p>{{data.bio}}</p>

      <strong>Hire this hero today!</strong>
    </div>
  `
})
export class HeroProfileComponent implements AdComponent {
  @Input() data: any;
}

最後咱們在AppComponent組件上應用它,而且給他初始化數據便可。頁面作屬性綁定,Init鉤子作加載。

 public ads: AdItem[];

  ngOnInit(): void {
    this.ads = [
      new AdItem(HeroProfileComponent, {name: 'Bombasto', bio: 'Brave as they come'}),
      new AdItem(HeroJobAdComponent, {headline: 'Hiring for several positions', body: 'Submit your resume today!'}),
    ];
  }


<app-add-banner [ads]="ads"></app-add-banner>

指令概覽

在 Angular 中有三種類型的指令:

  1. 組件 — 擁有模板的指令

  2. 結構型指令 — 經過添加和移除 DOM 元素改變 DOM 佈局的指令

  3. 屬性型指令 — 改變元素、組件或其它指令的外觀和行爲的指令。

組件是這三種指令中最經常使用的

結構型指令修改視圖的結構。例如,NgFor 和 NgIf。 要了解更多,參見結構型指令 guide。

屬性型指令改變一個元素的外觀或行爲。例如,內置的 NgStyle 指令能夠同時修改元素的多個樣式。

屬性型指令

屬性型指令用於改變一個DOM元素的外觀或行爲

建立屬性型指令須要一個帶有@Directive裝飾器的控制類,該裝飾器指定了一個用於標識屬性的選擇器。

import {Directive, ElementRef, Input} from '@angular/core';

@Directive({
  selector: '[appHighlight]'
})

export class HighlightDirective {
  constructor(el: ElementRef) {
    el.nativeElement.style.backgroundColor = 'yellow';
  }
}

ng generate directive highlight  建立指令類

import 引入Angular的core庫公開類

Directive:提供裝飾器功能

ElementRef:提供DOM訪問功能,注入到指令構造函數中。

Input:數據從綁定表達式傳達到指令中

裝飾器@Directive函數以配置對象參數的形式,包含了指令的元數據,須要一個CSS選擇器。

指令的選擇器是[myHighlightDirective],Angular會在模板中找到全部帶myHighlight屬性的元素

裝飾器完了就是指令控制類,叫HighlightDirective,包含該指令的邏輯。須要導出此類

Angular會爲每一個匹配的元素建立一個指令控制類的實例,並把ElementRef和Renderer注入到構造函數中。

ElementRef服務賦予咱們經過它訪問DOM的能力,Renderer服務容許經過代碼設置元素樣式。

使用屬性型指令

要使用這個HighlightDirective,建立一個模板,把這個指令做爲屬性應用到一個段落上。這個元素也就是指令的宿主

在html能夠直接使用這個指令

<h1>My First Attribute Directive</h1>
<p appHightlight>Highlight me!</p>

不要忘了,還要在app.module裏面把指令類暴露出來。給declarations數組添加元素。

總結:Angular在p元素上發現這個屬性,建立一個Hightlight實例,並把元素的引用注入到指令的構造函數中。構造裏面設置了背景色

響應用戶事件

設置用戶鼠標懸浮在一個元素時,設置它的顏色

首先給highlightDirective導入HostListener,用來添加事件響應

!!!注意:在類構造函數中,若是但願某個參數暴露出來,須要給這個參數添加private訪問修飾符!!!

import {Directive, ElementRef, HostListener, Input} from '@angular/core';

@Directive({
  selector: '[appHighlight]'
})

export class HighlightDirective {
  constructor(private el: ElementRef) {
  }

  @HostListener('mouseenter') onMouseEnter() {
    this.highlight('yellow');
  }

  @HostListener('mouseleave') onMouseLeave() {
    this.highlight(null);
  }

  private highlight(color: string): void {
    this.el.nativeElement.style.backgroundColor = color;
  }
}

使用@HostListener裝飾器,能夠添加事件處理器,上面代碼註冊監聽了兩個事件。監聽方法名,任意起便可。

數據傳遞值

使用Input輸入屬性,可讓顏色從頁面傳遞過來。注意,這邊推薦用別名,來區分具體含義。

在指令內部,該屬性叫hightlightColor,外部綁定的時候叫appHightlight。若是忘記添加,也能夠給默認值。||非空運算符能夠使用

import {Directive, ElementRef, HostListener, Input} from '@angular/core';

@Directive({
  selector: '[appHighlight]'
})

export class HighlightDirective {
  constructor(private el: ElementRef) {
  }

  @Input("appHighlight") public highlightColor: string;

  @HostListener('mouseenter') onMouseEnter() {
    this.highlight(this.highlightColor || 'red');
  }

  @HostListener('mouseleave') onMouseLeave() {
    this.highlight(null);
  }

  private highlight(color: string): void {
    this.el.nativeElement.style.backgroundColor = color;
  }
}

能夠經過一樣的方法,綁定更多的屬性,直接定義@Input,而後綁定指令上便可。

<p [appHighlight]="color" defaultColor="violet">
  Highlight me too!
</p>

@Input() defaultColor: string;

結構型指令

結構性指令的職責是HTML佈局,重塑DOM結構,好比添加、刪除等。大部分包含*號開頭,例如*ngIf

NgIf

星號前綴是一個用來簡化語法的語法糖,*ngIf會翻譯成<ng-template>元素幷包裹宿主元素。

<div *ngIf="hero" >{{hero.name}}</div>
-----------------------------------------------------
<ng-template [ngIf]="hero">
  <div>{{hero.name}}</div>
</ng-template>

NgFor

<div *ngFor="let hero of heroes; let i=index; let odd=odd; trackBy: trackById" [class.odd]="odd">
  ({{i}}) {{hero.name}}
</div>

<ng-template ngFor let-hero [ngForOf]="heroes" let-i="index" let-odd="odd" [ngForTrackBy]="trackById">
  <div [class.odd]="odd">({{i}}) {{hero.name}}</div>
</ng-template>

微語法

Angular微語法可以讓咱們經過簡短的、友好的字符串來配置一個指令。

微語法解析器會把這個字符串翻譯成<ng-template>上的屬性。

  • let關鍵字聲明一個模板輸入變量,咱們會在模板中引用它。本例子中,這個輸入變量就是heroiodd。 解析器會把let herolet ilet odd翻譯成命名變量let-herolet-ilet-odd

  • 微語法解析器接收oftrackby,把它們首字母大寫(of -> OftrackBy -> TrackBy), 而且給它們加上指令的屬性名(ngFor)前綴,最終生成的名字是ngForOfngForTrackBy。 還有兩個NgFor輸入屬性,指令據此瞭解到列表是heroes,而track-by函數是trackById

  • NgFor指令在列表上循環,每一個循環中都會設置和重置它本身的上下文對象上的屬性。 這些屬性包括indexodd以及一個特殊的屬性名$implicit(隱式變量)。

  • let-ilet-odd變量是經過let i=indexlet odd=odd來定義的。 Angular把它們設置爲上下文對象中的indexodd屬性的當前值。

  • 上下文中的屬性let-hero沒有指定過,實際上它來自一個隱式變量。 Angular會把let-hero設置爲上下文對象中的$implicit屬性,NgFor會用當前迭代中的英雄初始化它。

  • API參考手冊中描述了NgFor指令的其它屬性和上下文屬性。

  • NgFor是由NgForOf指令來實現的。請參閱NgForOf API reference來了解NgForOf指令的更多屬性及其上下文屬性。

模板輸入變量

能夠在單個實例的模板中引用它的值,例如:hero、i、odd。都是用let做爲前導關鍵字

模板輸入變量和模板引用變量是不一樣的。

使用let關鍵字在模板中聲明一個模板輸入變量,變量範圍被限制在所重複模板的單一實例上。

而聲明模板引用變量是給變量添加#號前綴的方式,一個引用變量引用的是它所附着的元素、組件或指令。能夠在模板任意地方訪問。

模板輸入變量和引用變量都有各自的命名空間,不會發生影響。

每一個宿主元素上只能有一個結構型指令。ngFor和ngIf不能再同一個元素上

NgSwitch

<div [ngSwitch]="hero?.emotion">
  <app-happy-hero    *ngSwitchCase="'happy'"    [hero]="hero"></app-happy-hero>
  <app-sad-hero      *ngSwitchCase="'sad'"      [hero]="hero"></app-sad-hero>
  <app-confused-hero *ngSwitchCase="'app-confused'" [hero]="hero"></app-confused-hero>
  <app-unknown-hero  *ngSwitchDefault           [hero]="hero"></app-unknown-hero>
</div>
---------------------------------------------------------------
<div [ngSwitch]="hero?.emotion">
  <ng-template [ngSwitchCase]="'happy'">
    <app-happy-hero [hero]="hero"></app-happy-hero>
  </ng-template>
  <ng-template [ngSwitchCase]="'sad'">
    <app-sad-hero [hero]="hero"></app-sad-hero>
  </ng-template>
  <ng-template [ngSwitchCase]="'confused'">
    <app-confused-hero [hero]="hero"></app-confused-hero>
  </ng-template >
  <ng-template ngSwitchDefault>
    <app-unknown-hero [hero]="hero"></app-unknown-hero>
  </ng-template>
</div>

優先使用星號語法的語法糖

<ng-template>

Angular元素用來渲染HTML,永遠不會直接顯示出來。若是沒有使用結構型指令,僅僅是把元素包進裏面,這些元素就是不可見的。

渲染視圖以前<ng-template>會將其內容替換爲一個註釋。

一般都要一個根元素做爲結構型指令的數組。列表元素<li>就是典型的NgFor使用的宿主元素。

若是沒有宿主元素時,能夠包括在原生的HTML元素中,好比<div>。可是用這種分組元素可能會破壞外觀表現

<ng-container>

Angular的ng-container是分組元素,不會污染樣式或元素佈局,由於Angular不會把他放進DOM

<p>
  I turned the corner
  <ng-container *ngIf="hero">
    and saw {{hero.name}}. I waved
  </ng-container>
  and continued on my way.
</p>
<div>
  Pick your favorite hero
  (<label><input type="checkbox" checked (change)="showSad = !showSad">show sad</label>)
</div>
<select [(ngModel)]="hero">
  <ng-container *ngFor="let h of heroes">
    <ng-container *ngIf="showSad || h.emotion !== 'sad'">
      <option [ngValue]="h">{{h.name}} ({{h.emotion}})</option>
    </ng-container>
  </ng-container>
</select>

ng-container是由Angular解析器負責識別處理的語法元素。不是指令、組件、類或接口,更像是if中的花括號。包括接下來的元素。

建立結構型指令

導入Directive裝飾器

導入Input、TemplateRef和ViewContainerRef

給指令類添加裝飾器

設置CSS屬性選擇器

import {Directive, Input, TemplateRef, ViewContainerRef} from '@angular/core';

@Directive({ selector: '[appUnless]'})

export class UnlessDirective {}

指令的選擇器一般把指令的屬性名放在方括號中,如[myUnless]。這個方括號定義了CSS屬性選擇器

TemplateRef和ViewContainerRef

使用TemplateRef能夠取得<ng-template>的內容,經過ViewContainerRef能夠來訪問這個視圖容器。

因此須要在構造中引用,而且建立爲私有

myUnless屬性

指令的使用者會把一個true/false條件綁定在[myUnless]屬性。指令須要一個帶有@Input的myUnless屬性。

@Input() set appUnless(condition: boolean) {
  if (!condition && !this.hasView) {
    this.viewContainer.createEmbeddedView(this.templateRef);
    this.hasView = true;
  } else if (condition && this.hasView) {
    this.viewContainer.clear();
    this.hasView = false;
  }
}

一旦該值的條件發生了變化,Angular就會去設置myUnless屬性,這個時候就須要一個設置器

若是條件是假的,而且以前沒有建立過視圖,就告訴視圖容器根據模板建立

若是條件是真的,而且視圖已經顯示出來了,就會清除該容器,並銷燬視圖

import {Directive, Input, TemplateRef, ViewContainerRef} from '@angular/core';

@Directive({selector: '[appUnless]'})

export class UnlessDirective {
  private hasView: boolean = false;

  constructor(private templateRef: TemplateRef<any>, private viewContainerRef: ViewContainerRef) {
  }

  @Input() set appUnless(condition: boolean) {
    if (!condition && !this.hasView) {
      this.viewContainerRef.createEmbeddedView(this.templateRef);
      this.hasView = true;
    }
    else if (condition && this.hasView) {
      this.viewContainerRef.clear();
      this.hasView = false;
    }
  }
}


<p *appUnless="false">
  condition = false
</p>
<p *appUnless="true">
  condition = true
</p>

管道 

管道把數據做爲輸入,而後轉換它,給出指望的輸出。咱們將把組件的birthday屬性轉換成對人類更友好的日期格式。

原始日期格式:{{birthday}} <br>
管道日期格式:{{birthday | date}}

能夠對管道進行參數化來進行微調,在管道後面添加冒號再跟一個參數值,若是有多個參數就是用冒號分隔

管道日期格式:{{birthday | date:'yyyy-MM-dd'}}

 參數能夠是模板表達式、字符串、組件的屬性。

自定義管道

須要Pipe裝飾器,是一個帶有元數據的類

實現PipeTransform接口,方法接受輸入和一些可選參數,並返回轉換後的值

當每一個輸入值被傳遞給transform方法時,還會帶上另外一個參數。

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

@Pipe({name: 'exponentialStrength'})
export class ExponentialStrengthPipe implements PipeTransform{
  transform(value: string, exponent: string): string {
    return `${value}-${exponent}`;
  }
}


自定義管道:{{name | exponentialStrength:'管道'}}

管道還能夠應用到循環之中

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

import { Flyer } from './heroes';

@Pipe({ name: 'flyingHeroes' })
export class FlyingHeroesPipe implements PipeTransform {
  transform(allHeroes: Flyer[]) {
    return allHeroes.filter(hero => hero.canFly);
  }
}

<div *ngFor="let hero of (heroes | flyingHeroes)">
  {{hero.name}}
</div>

純管道(pure)和非純管道(impure)

@Pipe({ name: 'flyingHeroesImpure', pure: false })

pure能夠指定管道是否純管道。默認均爲純管道

純管道

Angular只有在它檢測到輸入值發生純變動時纔會執行純管道。

指對原始類型值得更改或者對對象引用的更改。

Angular會忽略對象內部的更改,好比更改日期月份、添加數組、更新對象屬性。都不會調用純管道

非純管道

Angular會在每一個組件的變動檢測週期中執行非純管道,可能會被調用屢次,會增長開銷。

動畫

Angular的動畫系統賦予了製做各類動畫效果的能力,構建出於原生CSS動畫性能相同的動畫。

Angular動畫是基於標準的Web動畫API構建的,它們在支持此 API 的瀏覽器中會用原生方式工做。

在往應用中添加動畫以前,你要首先在應用的根模塊中引入一些與動畫有關的模塊和函數。

import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';

@NgModule({
  imports: [ BrowserModule, BrowserAnimationsModule ],
  // ... more stuff ...
})
export class AppModule { }

Hero 類有一個 name 屬性、一個 state 屬性(用於代表該英雄是否爲激活狀態)和一個 toggleState() 函數,用來在這兩種狀態之間切換。

export class Hero {
  constructor(public name: string, public state = 'inactive') {
  }

  toggleState() {
    this.state = 'active' === this.state ? 'inactive' : 'active';
  }
} 

能夠在組件元數據中定義一個名叫 heroState 的動畫觸發器。它在兩個狀態 active 和 inactive 之間進行轉場。 當英雄處於激活狀態時,它會把該元素顯示得稍微大一點、亮一點。

import {Component, Input, OnInit} from '@angular/core';
import {trigger, state, style, animate, transition} from '@angular/animations';

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

@Component({
  selector: 'app-hero-list-basic',
  templateUrl: './hero-list-basic.component.html',
  styleUrls: ['./hero-list-basic.component.css'],
  animations: [
    trigger('heroState', [
      state('inactive', style({
        backgroundColor: '#eee',
        transform: 'scale(1)'
      })),
      state('active', style({
        backgroundColor: '#cfd8dc',
        transform: 'scale(1.1)'
      })),
      transition('inactive => active', animate('100ms ease-in')),
      transition('active => inactive', animate('100ms ease-out'))
    ])
  ]
})

 

<ul>
  <li *ngFor="let hero of heros"
      [@heroState]="hero.state"
      (click)="hero.toggleState()">
    {{hero.name}}
  </li>
</ul>

狀態與轉場

這些 state 具體定義了每一個狀態的最終樣式。一旦元素轉場到那個狀態,該樣式就會被應用到此元素上,當它留在此狀態時,這些樣式也會一直保持着。

定義完狀態,就能定義在狀態之間的各類轉場了。每一個轉場都會控制一條在一組樣式和下一組樣式之間切換的時間線:

若是多個轉場都有一樣的時間線配置,就能夠把它們合併進同一個 transition 定義中:

transition('inactive => active, active => inactive', animate('100ms ease-out'))

若是要對同一個轉場的兩個方向都使用相同的時間線(就像前面的例子中那樣),就能夠使用 <=> 這種簡寫語法

transition('inactive <=> active', animate('100ms ease-out'))

*(通配符)狀態

*(通配符)狀態匹配任何動畫狀態。當定義那些不須要管當前處於什麼狀態的樣式及轉場時,這頗有用。好比:

  • 當該元素的狀態從 active 變成任何其它狀態時,active => * 轉場都會生效。

  • 當在任意兩個狀態之間切換時,* => * 轉場都會生效。

void狀態

有一種叫作 void 的特殊狀態,它能夠應用在任何動畫中。它表示元素沒有被附加到視圖。這種狀況多是因爲它還沒有被添加進來或者已經被移除了。 void 狀態在定義「進場」和「離場」的動畫時會很是有用。

好比當一個元素離開視圖時,* => void 轉場就會生效,而無論它在離場之前是什麼狀態。

transition('* => void', [ animate(100, style({transform: 'translateX(100%)'})) ])

https://angular.cn/guide/animations

用戶輸入

經過$event對象能夠取得用戶輸入

template: `
  <input (keyup)="onKey($event)">
  <p>{{values}}</p>
`

export class KeyUpComponent_v1 {
  onKey(event: any) { // without type info
    this.values += event.target.value + ' | ';
  }
}

從引用變量得到用戶輸入

<input #box (keyup)="0">
<p>{{box.value}}</p>

除非綁定一個事件,不然這將徹底沒法工做。keyup事件綁定數字0,這是最短的模板語句。

按鍵事件過濾

(keyup)事件處理器監聽每一次按鍵,有時候只有回車鍵才觸發事件,能夠綁定keyup.enter模擬事件。

<input #box (keyup.enter)="0">
<p>{{box.value}}</p>

也能夠綁定多個事件,好比失去焦點

<input #box (keyup.enter)="0" (blur)="0">
<p>{{box.value}}</p>

模板驅動表單

<form (ngSubmit)="onSubmit()" #heroForm="ngForm">
  <label>{{title}}</label>
  <button type="submit">Save</button>
</form>

能夠給表單定義ngSubmit事件來模擬表單提交。

也能夠結合驗證來模擬按鈕不能點擊事件。前端同樣,方法集合Promise寫成以下形式。

onSubmit(){
    this.isShow = true;
    let valid = new Promise((resolve, reject)=>{
      return setTimeout(()=>{
        this.isShow = false;
        resolve('OK');
      },3000);
    }).then((value)=>{
      this.title = '已提交';
    });
  }

表單驗證

https://angular.cn/guide/form-validation

響應式表單

https://angular.cn/guide/reactive-forms

動態表單

https://angular.cn/guide/dynamic-form

可觀察對象(Observable)與RxJS

可觀察對象支持在應用中的發佈者和訂閱者之間傳遞消息。在須要進行事件處理、異步編程和處理多個值的時候,可觀察對象相對其它技術有着顯著的優勢。

可觀察對象是聲明式的 —— 也就是說,雖然你定義了一個用於發佈值的函數,可是在有消費者訂閱它以前,這個函數並不會實際執行。 訂閱以後,當這個函數執行完或取消訂閱時,訂閱者就會收到通知。

可觀察對象能夠發送多個任意類型的值。不管這些值是同步發送仍是異步發送,接受這些值的API都是同樣的。

基本用法

建立一個 Observable 的實例,其中定義了一個訂閱者(subscriber)函數。 當有消費者調用 subscribe() 方法時,這個函數就會執行。 訂閱者函數用於定義「如何獲取或生成那些要發佈的值或消息」。

要執行所建立的可觀察對象,並開始從中接收通知,你就要調用它的 subscribe() 方法,並傳入一個觀察者(observer)。 這是一個 JavaScript 對象,它定義了你收到的這些消息的處理器(handler)。 subscribe() 調用會返回一個 Subscription 對象,該對象具備一個 unsubscribe() 方法。 當調用該方法時,你就會中止接收通知。

定義觀察者

用於接收可觀察對象通知的處理器須要Observer接口。定義了一些回調函數來處理可觀察對象可能會發來的三種通知

next:必要。用來處理每一個送達值。在開始執行後可能執行零次或屢次。

error:可選。用來處理錯誤通知。錯誤會中斷這個可觀察對象實例的執行過程。

complete:可選。用來處理執行完畢(complete)通知。當執行完畢後,這些值就會繼續傳給下一個處理器。

訂閱

只有當有人訂閱 Observable 的實例時,它纔會開始發佈值。 訂閱時要先調用該實例的 subscribe() 方法,並把一個觀察者對象傳給它,用來接收通知。

爲了展現訂閱的原理,咱們須要建立新的可觀察對象。它有一個構造函數能夠用來建立新實例,可是爲了更簡明,也能夠使用 Observable 上定義的一些靜態方法來建立一些經常使用的簡單可觀察對象:

  • Observable.of(...items) —— 返回一個 Observable 實例,它用同步的方式把參數中提供的這些值發送出來。

  • Observable.from(iterable) —— 把它的參數轉換成一個 Observable 實例。 該方法一般用於把一個數組轉換成一個(發送多個值的)可觀察對象。

// Create simple observable that emits three values
const myObservable = Observable.of(1, 2, 3);

// Create observer object
const myObserver = {
  next: x => console.log('Observer got a next value: ' + x),
  error: err => console.error('Observer got an error: ' + err),
  complete: () => console.log('Observer got a complete notification'),
};

// Execute with the observer object
myObservable.subscribe(myObserver);
// Logs:
// Observer got a next value: 1
// Observer got a next value: 2
// Observer got a next value: 3
// Observer got a complete notification

另外,subscribe() 方法還能夠接收定義在同一行中的回調函數,不管 nexterror 仍是 complete 處理器。好比,下面的 subscribe() 調用和前面指定預約義觀察者的例子是等價的。

myObservable.subscribe(
  x => console.log('Observer got a next value: ' + x),
  err => console.error('Observer got an error: ' + err),
  () => console.log('Observer got a complete notification')
);

不管哪一種狀況,next 處理器都是必要的,而 error 和 complete 處理器是可選的。

注意,next() 函數能夠接受消息字符串、事件對象、數字值或各類結構,具體類型取決於上下文。 爲了更通用一點,咱們把由可觀察對象發佈出來的數據統稱爲流。任何類型的值均可以表示爲可觀察對象,而這些值會被髮布爲一個流。

建立可觀察對象

使用 Observable 構造函數能夠建立任何類型的可觀察流。 當執行可觀察對象的 subscribe()方法時,這個構造函數就會把它接收到的參數做爲訂閱函數來運行。 訂閱函數會接收一個 Observer 對象,並把值發佈給觀察者的 next() 方法。

好比,要建立一個與前面的 Observable.of(1, 2, 3) 等價的可觀察對象,你能夠這樣作:

// This function runs when subscribe() is called
function sequenceSubscriber(observer) {
  // synchronously deliver 1, 2, and 3, then complete
  observer.next(1);
  observer.next(2);
  observer.next(3);
  observer.complete();

  // unsubscribe function doesn't need to do anything in this
  // because values are delivered synchronously
  return {unsubscribe() {}};
}

// Create a new Observable that will deliver the above sequence
const sequence = new Observable(sequenceSubscriber);

// execute the Observable and print the result of each notification
sequence.subscribe({
  next(num) { console.log(num); },
  complete() { console.log('Finished sequence'); }
});

// Logs:
// 1
// 2
// 3
// Finished sequence

若是要略微增強這個例子,咱們能夠建立一個用來發布事件的可觀察對象

function fromEvent(target, eventName) {
  return new Observable((observer) => {
    const handler = (e) => observer.next(e);

    // Add the event handler to the target
    target.addEventListener(eventName, handler);

    return () => {
      // Detach the event handler from the target
      target.removeEventListener(eventName, handler);
    };
  });
}

如今,你就能夠使用這個函數來建立可發佈 keydown 事件的可觀察對象了:

const ESC_KEY = 27;
const nameInput = document.getElementById('name') as HTMLInputElement;

const subscription = fromEvent(nameInput, 'keydown')
  .subscribe((e: KeyboardEvent) => {
    if (e.keyCode === ESC_KEY) {
      nameInput.value = '';
    }
  });

多播

典型的可觀察對象會爲每個觀察者建立一次新的、獨立的執行。 當觀察者進行訂閱時,該可觀察對象會連上一個事件處理器,而且向那個觀察者發送一些值。當第二個觀察者訂閱時,這個可觀察對象就會連上一個新的事件處理器,並獨立執行一次,把這些值發送給第二個可觀察對象。

有時候,不該該對每個訂閱者都獨立執行一次,你可能會但願每次訂閱都獲得同一批值 —— 即便是那些你已經發送過的。這在某些狀況下有用,好比用來發送 document 上的點擊事件的可觀察對象。

多播用來讓可觀察對象在一次執行中同時廣播給多個訂閱者。藉助支持多播的可觀察對象,你沒必要註冊多個監聽器,而是複用第一個(next)監聽器,而且把值發送給各個訂閱者。

當建立可觀察對象時,你要決定你但願別人怎麼用這個對象以及是否對它的值進行多播。

來看一個從 1 到 3 進行計數的例子,它每發出一個數字就會等待 1 秒。

function sequenceSubscriber(observer) {
  const seq = [1, 2, 3];
  let timeoutId;

  // Will run through an array of numbers, emitting one value
  // per second until it gets to the end of the array.
  function doSequence(arr, idx) {
    timeoutId = setTimeout(() => {
      observer.next(arr[idx]);
      if (idx === arr.length - 1) {
        observer.complete();
      } else {
        doSequence(arr, idx++);
      }
    }, 1000);
  }

  doSequence(seq, 0);

  // Unsubscribe should clear the timeout to stop execution
  return {unsubscribe() {
    clearTimeout(timeoutId);
  }};
}

// Create a new Observable that will deliver the above sequence
const sequence = new Observable(sequenceSubscriber);

sequence.subscribe({
  next(num) { console.log(num); },
  complete() { console.log('Finished sequence'); }
});

// Logs:
// (at 1 second): 1
// (at 2 seconds): 2
// (at 3 seconds): 3
// (at 3 seconds): Finished sequence

注意,若是你訂閱了兩次,就會有兩個獨立的流,每一個流都會每秒發出一個數字

// Subscribe starts the clock, and will emit after 1 second
sequence.subscribe({
  next(num) { console.log('1st subscribe: ' + num); },
  complete() { console.log('1st sequence finished.'); }
});

// After 1/2 second, subscribe again.
setTimeout(() => {
  sequence.subscribe({
    next(num) { console.log('2nd subscribe: ' + num); },
    complete() { console.log('2nd sequence finished.'); }
  });
}, 500);

// Logs:
// (at 1 second): 1st subscribe: 1
// (at 1.5 seconds): 2nd subscribe: 1
// (at 2 seconds): 1st subscribe: 2
// (at 2.5 seconds): 2nd subscribe: 2
// (at 3 seconds): 1st subscribe: 3
// (at 3 seconds): 1st sequence finished
// (at 3.5 seconds): 2nd subscribe: 3
// (at 3.5 seconds): 2nd sequence finished

修改這個可觀察對象以支持多播

function multicastSequenceSubscriber() {
  const seq = [1, 2, 3];
  // Keep track of each observer (one for every active subscription)
  const observers = [];
  // Still a single timeoutId because there will only ever be one
  // set of values being generated, multicasted to each subscriber
  let timeoutId;

  // Return the subscriber function (runs when subscribe()
  // function is invoked)
  return (observer) => {
    observers.push(observer);
    // When this is the first subscription, start the sequence
    if (observers.length === 1) {
      timeoutId = doSequence({
        next(val) {
          // Iterate through observers and notify all subscriptions
          observers.forEach(obs => obs.next(val));
        },
        complete() {
          // Notify all complete callbacks
          observers.forEach(obs => obs.complete());
        }
      }, seq, 0);
    }

    return {
      unsubscribe() {
        // Remove from the observers array so it's no longer notified
        observers.splice(observers.indexOf(observer), 1);
        // If there's no more listeners, do cleanup
        if (observers.length === 0) {
          clearTimeout(timeoutId);
        }
      }
    };
  };
}

// Run through an array of numbers, emitting one value
// per second until it gets to the end of the array.
function doSequence(observer, arr, idx) {
  return setTimeout(() => {
    observer.next(arr[idx]);
    if (idx === arr.length - 1) {
      observer.complete();
    } else {
      doSequence(observer, arr, idx++);
    }
  }, 1000);
}

// Create a new Observable that will deliver the above sequence
const multicastSequence = new Observable(multicastSequenceSubscriber);

// Subscribe starts the clock, and begins to emit after 1 second
multicastSequence.subscribe({
  next(num) { console.log('1st subscribe: ' + num); },
  complete() { console.log('1st sequence finished.'); }
});

// After 1 1/2 seconds, subscribe again (should "miss" the first value).
setTimeout(() => {
  multicastSequence.subscribe({
    next(num) { console.log('2nd subscribe: ' + num); },
    complete() { console.log('2nd sequence finished.'); }
  });
}, 1500);

// Logs:
// (at 1 second): 1st subscribe: 1
// (at 2 seconds): 1st subscribe: 2
// (at 2 seconds): 2nd subscribe: 2
// (at 3 seconds): 1st subscribe: 3
// (at 3 seconds): 1st sequence finished
// (at 3 seconds): 2nd subscribe: 3
// (at 3 seconds): 2nd sequence finished

錯誤處理

因爲可觀察對象會異步生成值,因此用 try/catch 是沒法捕獲錯誤的。你應該在觀察者中指定一個 error 回調來處理錯誤。發生錯誤時還會致使可觀察對象清理現有的訂閱,而且中止生成值。可觀察對象能夠生成值(調用 next 回調),也能夠調用 complete 或 error 回調來主動結束。

myObservable.subscribe({
  next(num) { console.log('Next num: ' + num)},
  error(err) { console.log('Received an errror: ' + err)}
});

RxJS庫

響應式編程是一種面向數據流和變動傳播的異步編程範式(Wikipedia)。RxJS(響應式擴展的 JavaScript 版)是一個使用可觀察對象進行響應式編程的庫,它讓組合異步代碼和基於回調的代碼變得更簡單 (RxJS Docs)。

RxJS 提供了一種對 Observable 類型的實現,直到 Observable 成爲了 JavaScript 語言的一部分而且瀏覽器支持它以前,它都是必要的。這個庫還提供了一些工具函數,用於建立和使用可觀察對象。這些工具函數可用於:

  • 把現有的異步代碼轉換成可觀察對象

  • 迭代流中的各個值

  • 把這些值映射成其它類型

  • 對流進行過濾

  • 組合多個流

建立可觀察對象的函數

RxJS 提供了一些用來建立可觀察對象的函數。這些函數能夠簡化根據某些東西建立可觀察對象的過程,好比事件、定時器、承諾等等。好比:

import { fromPromise } from 'rxjs';

// Create an Observable out of a promise
const data = fromPromise(fetch('/api/endpoint'));
// Subscribe to begin listening for async result
data.subscribe({
 next(response) { console.log(response); },
 error(err) { console.error('Error: ' + err); },
 complete() { console.log('Completed'); }
});
import { interval } from 'rxjs';

// Create an Observable that will publish a value on an interval
const secondsCounter = interval(1000);
// Subscribe to begin publishing values
secondsCounter.subscribe(n =>
  console.log(`It's been ${n} seconds since subscribing!`));
import { fromEvent } from 'rxjs';

const el = document.getElementById('my-element');

// Create an Observable that will publish mouse movements
const mouseMoves = fromEvent(el, 'mousemove');

// Subscribe to start listening for mouse-move events
const subscription = mouseMoves.subscribe((evt: MouseEvent) => {
  // Log coords of mouse movements
  console.log(`Coords: ${evt.clientX} X ${evt.clientY}`);

  // When the mouse is over the upper-left of the screen,
  // unsubscribe to stop listening for mouse movements
  if (evt.clientX < 40 && evt.clientY < 40) {
    subscription.unsubscribe();
  }
});
import { ajax } from 'rxjs/ajax';

// Create an Observable that will create an AJAX request
const apiData = ajax('/api/data');
// Subscribe to create the request
apiData.subscribe(res => console.log(res.status, res.response));

操做符

操做符是基於可觀察對象構建的一些對集合進行復雜操做的函數。RxJS 定義了一些操做符,好比 map()filter()concat() 和 flatMap()

操做符接受一些配置項,而後返回一個以來源可觀察對象爲參數的函數。當執行這個返回的函數時,這個操做符會觀察來源可觀察對象中發出的值,轉換它們,並返回由轉換後的值組成的新的可觀察對象。下面是一個簡單的例子:

import { map } from 'rxjs/operators';

const nums = of(1, 2, 3);

const squareValues = map((val: number) => val * val);
const squaredNums = squareValues(nums);

squaredNums.subscribe(x => console.log(x));

// Logs
// 1
// 4
// 9

你能夠使用管道來把這些操做符連接起來。管道讓你能夠把多個由操做符返回的函數組合成一個。pipe() 函數以你要組合的這些函數做爲參數,而且返回一個新的函數,當執行這個新函數時,就會順序執行那些被組合進去的函數。

應用於某個可觀察對象上的一組操做符就像一個菜譜 —— 也就是說,對你感興趣的這些值進行處理的一組操做步驟。這個菜譜自己不會作任何事。你須要調用 subscribe() 來經過這個菜譜生成一個結果。

例子以下:

import { filter, map } from 'rxjs/operators';

const nums = of(1, 2, 3, 4, 5);

// Create a function that accepts an Observable.
const squareOddVals = pipe(
  filter(n => n % 2),
  map(n => n * n)
);

// Create an Observable that will run the filter and map functions
const squareOdd = squareOddVals(nums);

// Suscribe to run the combined functions
squareOdd.subscribe(x => console.log(x));

pipe() 函數也同時是 RxJS 的 Observable 上的一個方法,因此你能夠用下列簡寫形式來達到一樣的效果:

import { filter, map } from 'rxjs/operators';

const squareOdd = of(1, 2, 3, 4, 5)
  .pipe(
    filter(n => n % 2 !== 0),
    map(n => n * n)
  );

// Subscribe to get values
squareOdd.subscribe(x => console.log(x));

經常使用操做符

RxJS 提供了不少操做符(超過 150 個),不過只有少數是經常使用的。 下面是一個經常使用操做符的列表,要查看用法範例,參見 RxJS 文檔中的 RxJS 5 操做符範例

類別 操做
建立 from , fromPromise , fromEvent , of
組合 combineLatest , concat , merge , startWith , withLatestFrom , zip
過濾 debounceTime , distinctUntilChanged , filter , take , takeUntil
轉換 bufferTime , concatMap , map , mergeMap , scan , switchMap
工具 tap
多播 share

錯誤處理

除了能夠在訂閱時提供 error() 處理器外,RxJS 還提供了 catchError 操做符,它容許你在管道中處理已知錯誤。

假設你有一個可觀察對象,它發起 API 請求,而後對服務器返回的響應進行映射。若是服務器返回了錯誤或值不存在,就會生成一個錯誤。若是你捕獲這個錯誤並提供了一個默認值,流就會繼續處理這些值,而不會報錯。

下面是使用 catchError 操做符實現這種效果的例子:

import { ajax } from 'rxjs/ajax';
import { map, catchError } from 'rxjs/operators';
// Return "response" from the API. If an error happens,
// return an empty array.
const apiData = ajax('/api/data').pipe(
  map(res => {
    if (!res.response) {
      throw new Error('Value expected!');
    }
    return res.response;
  }),
  catchError(err => of([]))
);

apiData.subscribe({
  next(x) { console.log('data: ', x); },
  error(err) { console.log('errors already caught... will not run'); }
});

重試失敗的可觀察對象

catchError 提供了一種簡單的方式進行恢復,而 retry 操做符讓你能夠嘗試失敗的請求。

能夠在 catchError 以前使用 retry 操做符。它會訂閱到原始的來源可觀察對象,它能夠從新運行致使結果出錯的動做序列。若是其中包含 HTTP 請求,它就會從新發起那個 HTTP 請求。

下列代碼爲前面的例子加上了捕獲錯誤前重發請求的邏輯:

import { ajax } from 'rxjs/ajax';
import { map, retry, catchError } from 'rxjs/operators';

const apiData = ajax('/api/data').pipe(
  retry(3), // Retry up to 3 times before failing
  map(res => {
    if (!res.response) {
      throw new Error('Value expected!');
    }
    return res.response;
  }),
  catchError(err => of([]))
);

apiData.subscribe({
  next(x) { console.log('data: ', x); },
  error(err) { console.log('errors already caught... will not run'); }
});

可觀察對象的命名約定

因爲 Angular 的應用幾乎都是用 TypeScript 寫的,你一般會但願知道某個變量是否可觀察對象。雖然 Angular 框架並無針對可觀察對象的強制性命名約定,不過你常常會看到可觀察對象的名字以「$」符號結尾。

這在快速瀏覽代碼並查找可觀察對象值時會很是有用。一樣的,若是你但願用某個屬性來存儲來自可觀察對象的最近一個值,它的命名慣例是與可觀察對象同名,但不帶「$」後綴。

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

@Component({
  selector: 'app-stopwatch',
  templateUrl: './stopwatch.component.html'
})
export class StopwatchComponent {

  stopwatchValue: number;
  stopwatchValue$: Observable<number>;

  start() {
    this.stopwatchValue$.subscribe(num =>
      this.stopwatchValue = num
    );
  }
}

Angular中的可觀察對象

Angular 使用可觀察對象做爲處理各類經常使用異步操做的接口。好比:

  • EventEmitter 類派生自 Observable

  • HTTP 模塊使用可觀察對象來處理 AJAX 請求和響應。

  • 路由器和表單模塊使用可觀察對象來監聽對用戶輸入事件的響應。

事件發送器EventEmitter

Angular 提供了一個 EventEmitter 類,它用來從組件的 @Output() 屬性中發佈一些值。EventEmitter 擴展了 Observable,並添加了一個 emit() 方法,這樣它就能夠發送任意值了。當你調用 emit() 時,就會把所發送的值傳給訂閱上來的觀察者的 next() 方法。

這種用法的例子參見 EventEmitter 文檔。下面這個範例組件監聽了 open 和 close 事件:

<zippy (open)="onOpen($event)" (close)="onClose($event)"></zippy>

@Component({
  selector: 'zippy',
  template: `
  <div class="zippy">
    <div (click)="toggle()">Toggle</div>
    <div [hidden]="!visible">
      <ng-content></ng-content>
    </div>
  </div>`})

export class ZippyComponent {
  visible = true;
  @Output() open = new EventEmitter<any>();
  @Output() close = new EventEmitter<any>();

  toggle() {
    this.visible = !this.visible;
    if (this.visible) {
      this.open.emit(null);
    } else {
      this.close.emit(null);
    }
  }
}

HTTP

Angular 的 HttpClient 從 HTTP 方法調用中返回了可觀察對象。例如,http.get(‘/api’) 就會返回可觀察對象。相對於基於承諾(Promise)的 HTTP API,它有一系列優勢:

  • 可觀察對象不會修改服務器的響應(和在承諾上串聯起來的 .then() 調用同樣)。反之,你能夠使用一系列操做符來按需轉換這些值。

  • HTTP 請求是能夠經過 unsubscribe() 方法來取消的。

  • 請求能夠進行配置,以獲取進度事件的變化。

  • 失敗的請求很容易重試。

Async管道

AsyncPipe 會訂閱一個可觀察對象或承諾,並返回其發出的最後一個值。當發出新值時,該管道就會把這個組件標記爲須要進行變動檢查的(譯註:所以可能致使刷新界面)。

下面的例子把 time 這個可觀察對象綁定到了組件的視圖中。這個可觀察對象會不斷使用當前時間更新組件的視圖。

@Component({
  selector: 'async-observable-pipe',
  template: `<div><code>observable|async</code>:
       Time: {{ time | async }}</div>`
})
export class AsyncObservablePipeComponent {
  time = new Observable(observer =>
    setInterval(() => observer.next(new Date().toString()), 1000)
  );
}

路由器(router)

Router.events 以可觀察對象的形式提供了其事件。 你能夠使用 RxJS 中的 filter() 操做符來找到感興趣的事件,而且訂閱它們,以便根據瀏覽過程當中產生的事件序列做出決定。 例子以下:

import { Router, NavigationStart } from '@angular/router';
import { filter } from 'rxjs/operators';

@Component({
  selector: 'app-routable',
  templateUrl: './routable.component.html',
  styleUrls: ['./routable.component.css']
})
export class Routable1Component implements OnInit {

  navStart: Observable<NavigationStart>;

  constructor(private router: Router) {
    // Create a new Observable the publishes only the NavigationStart event
    this.navStart = router.events.pipe(
      filter(evt => evt instanceof NavigationStart)
    ) as Observable<NavigationStart>;
  }

  ngOnInit() {
    this.navStart.subscribe(evt => console.log('Navigation Started!'));
  }
}

ActivatedRoute 是一個可注入的路由器服務,它使用可觀察對象來獲取關於路由路徑和路由參數的信息。好比,ActivateRoute.url 包含一個用於彙報路由路徑的可觀察對象。例子以下:

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

@Component({
  selector: 'app-routable',
  templateUrl: './routable.component.html',
  styleUrls: ['./routable.component.css']
})
export class Routable2Component implements OnInit {
  constructor(private activatedRoute: ActivatedRoute) {}

  ngOnInit() {
    this.activatedRoute.url
      .subscribe(url => console.log('The URL changed to: ' + url));
  }
}

響應式表單

響應式表單具備一些屬性,它們使用可觀察對象來監聽表單控件的值。 FormControl 的 valueChanges 屬性和 statusChanges 屬性包含了會發出變動事件的可觀察對象。訂閱可觀察的表單控件屬性是在組件類中觸發應用邏輯的途徑之一。好比:

import { FormGroup } from '@angular/forms';

@Component({
  selector: 'my-component',
  template: 'MyComponent Template'
})
export class MyComponent implements OnInit {
  nameChangeLog: string[] = [];
  heroForm: FormGroup;

  ngOnInit() {
    this.logNameChange();
  }
  logNameChange() {
    const nameControl = this.heroForm.get('name');
    nameControl.valueChanges.forEach(
      (value: string) => this.nameChangeLog.push(value)
    );
  }
}

輸入提示(type-ahead)建議

可觀察對象能夠簡化輸入提示建議的實現方式。典型的輸入提示要完成一系列獨立的任務:

  • 從輸入中監聽數據。

  • 移除輸入值先後的空白字符,並確認它達到了最小長度。

  • 防抖(這樣才能防止連續按鍵時每次按鍵都發起 API 請求,而應該等到按鍵出現停頓時才發起)

  • 若是輸入值沒有變化,則不要發起請求(好比按某個字符,而後快速按退格)。

  • 若是已發出的 AJAX 請求的結果會由於後續的修改而變得無效,那就取消它。

徹底用 JavaScript 的傳統寫法實現這個功能可能須要大量的工做。使用可觀察對象,你能夠使用這樣一個 RxJS 操做符的簡單序列:

import { fromEvent } from 'rxjs';
import { ajax } from 'rxjs/ajax';
import { map, filter, debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators';

const searchBox = document.getElementById('search-box');

const typeahead = fromEvent(searchBox, 'input').pipe(
  map((e: KeyboardEvent) => e.target.value),
  filter(text => text.length > 2),
  debounceTime(10),
  distinctUntilChanged(),
  switchMap(() => ajax('/api/endpoint'))
);

typeahead.subscribe(data => {
 // Handle the data from the API
});

指數化退避

指數化退避是一種失敗後重試 API 的技巧,它會在每次連續的失敗以後讓重試時間逐漸變長,超過最大重試次數以後就會完全放棄。 若是使用承諾和其它跟蹤 AJAX 調用的方法會很是複雜,而使用可觀察對象,這很是簡單:

import { pipe, range, timer, zip } from 'rxjs';
import { ajax } from 'rxjs/ajax';
import { retryWhen, map, mergeMap } from 'rxjs/operators';

function backoff(maxTries, ms) {
 return pipe(
   retryWhen(attempts => range(1, maxTries)
     .pipe(
       zip(attempts, (i) => i),
       map(i => i * i),
       mergeMap(i =>  timer(i * ms))
     )
   )
 );
}

ajax('/api/endpoint')
  .pipe(backoff(3, 250))
  .subscribe(data => handleData(data));

function handleData(data) {
  // ...
}

可觀察對象與其餘技術的比較

你能夠常用可觀察對象(Observable)而不是承諾(Promise)來異步傳遞值。 相似的,可觀察對象也能夠取代事件處理器的位置。最後,因爲可觀察對象傳遞多個值,因此你能夠在任何可能構建和操做數組的地方使用可觀察對象。

在這些狀況下,可觀察對象的行爲與其替代技術有一些差別,不過也提供了一些顯著的優點。下面是對這些差別的詳細比較。

可觀察對象VS承諾

可觀察對象常常拿來和承諾進行對比。有一些關鍵的不一樣點:

  • 可觀察對象是聲明式的,在被訂閱以前,它不會開始執行。承諾是在建立時就當即執行的。這讓可觀察對象可用於定義那些應該按需執行的菜譜。

  • 可觀察對象能提供多個值。承諾只提供一個。這讓可觀察對象可用於隨着時間的推移獲取多個值。

  • 可觀察對象會區分串聯處理和訂閱語句。承諾只有 .then() 語句。這讓可觀察對象可用於建立供系統的其它部分使用而不但願當即執行的複雜菜譜。

  • 可觀察對象的 subscribe() 會負責處理錯誤。承諾會把錯誤推送給它的子承諾。這讓可觀察對象可用於進行集中式、可預測的錯誤處理。

建立與訂閱

在有消費者訂閱以前,可觀察對象不會執行。subscribe() 會執行一次定義好的行爲,而且能夠再次調用它。每次訂閱都是單獨計算的。從新訂閱會致使從新計算這些值。

// declare a publishing operation
new Observable((observer) => { subscriber_fn });
// initiate execution
observable.subscribe(() => {
      // observer handles notifications
    });

承諾會當即執行,而且只執行一次。當承諾建立時,會當即計算出結果。沒有辦法從新作一次。全部的 then 語句(訂閱)都會共享同一次計算。

// initiate execution
new Promise((resolve, reject) => { executer_fn });
// handle return value
promise.then((value) => {
      // handle result here
    });

串聯

可觀察對象會區分各類轉換函數,好比映射和訂閱。只有訂閱纔會激活訂閱者函數,以開始計算那些值。

observable.map((v) => 2*v);

承諾並不區分最後的 .then() 語句(等價於訂閱)和中間的 .then() 語句(等價於映射)。

promise.then((v) => 2*v);

可取消

可觀察對象的訂閱是可取消的。取消訂閱會移除監聽器,使其再也不接受未來的值,並通知訂閱者函數取消正在進行的工做。

const sub = obs.subscribe(...);
sub.unsubscribe();

錯誤處理

可觀察對象的錯誤處理是交給訂閱者的錯誤處理器的,而且該訂閱者會自動取消對這個可觀察對象的訂閱。

obs.subscribe(() => {
throw Error('my error');
});

承諾會把錯誤推給其子承諾。

promise.then(() => {
throw Error('my error');
});

速查表

操做

可觀察對象

承諾

建立

new Observable((observer) => {
  observer.next(123);
});
new Promise((resolve, reject) => {
  resolve(123);
});

轉換

obs.map((value) => value * 2 );
promise.then((value) => value * 2);

訂閱

sub = obs.subscribe((value) => {
  console.log(value)
});
promise.then((value) => {
  console.log(value);
});

取消訂閱

sub.unsubscribe();

承諾被解析時隱式完成。

 

可觀察對象VS事件API

可觀察對象和事件 API 中的事件處理器很像。這兩種技術都會定義通知處理器,並使用它們來處理一段時間內傳遞的多個值。訂閱可觀察對象與添加事件處理器是等價的。一個顯著的不一樣是你能夠配置可觀察對象,使其在把事件傳給事件處理器之間先進行轉換。

使用可觀察對象來處理錯誤和異步操做在 HTTP 請求這樣的場景下更加具備一致性。

下列代碼片斷揭示了一樣的操做要如何分別使用可觀察對象和事件 API 進行實現。

 

可觀察對象

事件 API

建立與取消

// Setup
let clicks$ = fromEvent(buttonEl, ‘click’);
// Begin listening
let subscription = clicks$
  .subscribe(e => console.log(‘Clicked’, e))
// Stop listening
subscription.unsubscribe();
function handler(e) {
  console.log(‘Clicked’, e);
}

// Setup & begin listening
button.addEventListener(‘click’, handler);
// Stop listening
button.removeEventListener(‘click’, handler);

訂閱

observable.subscribe(() => {
  // notification handlers here
});
element.addEventListener(eventName, (event) => {
  // notification handler here
});

配置

監聽按鍵,提供一個流來表示這些輸入的值。

fromEvent(inputEl, 'keydown').pipe(
  map(e => e.target.value)
);

不支持配置。

element.addEventListener(eventName, (event) => {
  // Cannot change the passed Event into another
  // value before it gets to the handler
});

 

啓動過程

Angular模塊類描述應用的部件是如何組合在一塊兒的,每一個應用都至少有一個Angular模塊,也就是根模塊。

用來引導並運行應用,常規名字是AppModule。下面是最小的AppModule

import { NgModule }      from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent }  from './app.component';

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

@NgModule裝飾器將AppModule標記爲模塊類,接受一個元數據對象,告訴Angular如何編譯和啓用

imports數組

Angular模塊把特性合併成離散單元的一種方式,Angular自身的許多特性也是經過Angular模塊組織的。

HTTP服務在HttpModule裏,路由器在RouterModule裏。當應用須要模塊的特性時,將其添加到此數組中。

NgMobile屬於FormsModule,RouterLink屬於RouterModule

declarations數組

告訴Angular那個組件屬於AppModule,只要建立組件都應該放在此數組裏。不然會報錯。

bootstrap數組

經過引導根AppModule來啓動應用,建立列在此數組的組件,並把他們插入瀏覽器的DOM中。

providers數組

提供依賴注入類,能夠從構造函數注入

特性模塊

特性模塊是帶有@NgModule裝飾器及其元數據的類,就像根模塊同樣。特性模塊的元數據和根模塊的元數據的屬性是同樣的。

根模塊和特性模塊共享着相同的執行環境,共享着同一個依賴注入器,意味着某個模塊中定義的服務在全部模塊中都能使用。

能夠解決如下問題:

1.隨着一個個類被加入到應用中,根模塊AppModule變大

2.指令衝突

3.和其餘特性區之間缺少清晰的邊界,致使難以在不一樣的開發組之間分配職責

他在技術上有兩個顯著的不一樣點

1.咱們引導根模塊來啓動應用,但導入特性模塊來擴展應用

2.特性模塊能夠對其餘模塊暴露或隱藏本身的實現

特性模塊用來提供內聚的功能集合,應用於某個領域、工做流、基礎設施

建立特性模塊

import { NgModule }           from '@angular/core';
import { CommonModule }       from '@angular/common';
import { FormsModule }        from '@angular/forms';
 
import { AwesomePipe }        from './awesome.pipe';
 
import { ContactComponent }   from './contact.component';
import { ContactService }     from './contact.service';
import { HighlightDirective } from './highlight.directive';
 
@NgModule({
  imports:      [ CommonModule, FormsModule ],
  declarations: [ ContactComponent, HighlightDirective, AwesomePipe ],
  exports:      [ ContactComponent ],
  providers:    [ ContactService ]
})
export class ContactModule { }

而後在app.module中,填寫這條特性模塊

imports:      [ BrowserModule, ContactModule ],

Angular模塊常見問題

應該添加那些類到declarations中

把可聲明的類(組件、指令和管道)添加到declarations列表中。

這些類只能在應用程序的一個而且只有一個模塊中聲明,只有當他們從屬於某個模塊時,才能把此模塊中聲明它們。

可聲明指的是:組件、指令和管道等能夠被加到模塊的declarations列表中的類。

哪些類不須要declarations中

只有可聲明的類才能加到模塊的declarations列表中

如下狀況不須要添加:

1.已經在其餘模塊中聲明過的類,不管它來自應用本身的模塊(@NgModule)仍是第三方模塊

2.其餘模塊中導入的指令。

3.模塊類

4.服務類

5.非Angular類和對象

https://angular.cn/guide/ngmodule-faq

依賴注入

依賴注入(DI)是用來建立對象及其依賴的其它對象的一種方式。 當依賴注入系統建立某個對象實例時,會負責提供該對象所依賴的對象(稱爲該對象的依賴)

ng generate service heroes/hero   建立服務

配置注入器

不須要建立Angular注入器,Angular啓動過程當中自動爲咱們建立一個應用級注入器。

//main.ts

platformBrowserDynamic().bootstrapModule(AppModule);

必須經過註冊提供商(provider)來配置注入器,提供商爲應用建立所需服務。

@Injectable的providers數組

@Injectable 裝飾器會指出這些服務或其它類是用來注入的。它還能用於爲這些服務提供配置項。

這裏咱們使用類上的 @Injectable 裝飾器來爲 HeroService 配置了一個提供商。

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

@Injectable({
  providedIn: 'root',
})
export class HeroService {
  constructor() { }
}

 

providedIn 告訴 Angular,它的根注入器要負責調用 HeroService 類的構造函數來建立一個實例,並讓它在整個應用中都是可用的。在使用 CLI 生成新服務時,會默認幫你設置爲這種提供商。

這種狀況下,服務提供商應該關聯到一個特定的 @NgModule 類,並且應該用於該模塊包含的任何一個注入器中。

@Injectable 裝飾器用來配置一個服務提供商,它能夠用在任何包含了 HeroModule 的注入器中。

import { Injectable } from '@angular/core';
import { HeroModule } from './hero.module';
import { HEROES }     from './mock-heroes';

@Injectable({
  // we declare that this service should be created
  // by any injector that includes HeroModule.

  providedIn: HeroModule,
})
export class HeroService {
  getHeroes() { return HEROES; }
}

在NgModule中註冊提供商

@NgModule({
  imports: [
    BrowserModule
  ],
  declarations: [
    AppComponent,
    CarComponent,
    HeroesComponent
  ],
  providers: [
    UserService,
    { provide: APP_CONFIG, useValue: HERO_DI_CONFIG }
  ],
  bootstrap: [ AppComponent ]
})
export class AppModule { }

第一條使用UserService這個注入令牌(injection token)註冊了UserService類。

第二條使用APP_CONFIG這個注入令牌註冊了一個值(HERO_DI_CONFIG)

在組件中註冊提供商

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

import { HeroService }        from './hero.service';

@Component({
  selector: 'app-heroes',
  providers: [HeroService],
  template: `
  <h2>Heroes</h2>
  <app-hero-list></app-hero-list>
  `
})
export class HeroesComponent { }

@Injectable,@NgModule仍是@Component

該使用 @Injectable 裝飾器、@NgModule仍是 @Component 來提供服務。差異在於最終的打包體積、服務的範圍和服務的生命週期。

當你在服務自己的 @Injectable 裝飾器中註冊提供商時,優化工具(好比 CLI 產品模式構建時所用的)能夠執行搖樹優化,這會移除全部沒在應用中使用過的服務。搖樹優化會致使更小的打包體積。

Angular 模塊中的 providers@NgModule.providers)是註冊在應用的根注入器下的。 所以,Angular 能夠往它所建立的任何類中注入相應的服務。 一旦建立,服務的實例就會存在於該應用的所有生存期中,Angular 會把這一個服務實例注入到需求它的每一個類中。

組件的提供商(@Component.providers)會註冊到每一個組件實例本身的注入器上。

所以 Angular 只能在該組件及其各級子組件的實例上注入這個服務實例,而不能在其它地方注入這個服務實例。

注意,由組件提供的服務,也一樣具備有限的生命週期。組件的每一個實例都會有它本身的服務實例,而且,當組件實例被銷燬的時候,服務的實例也一樣會被銷燬。

在這個範例應用中,HeroComponent 會在應用啓動時建立,而且它從未銷燬,所以,由 HeroComponent 建立的 HeroService 也一樣會活在應用的整個生命週期中。

若是你要把 HeroService 的訪問權限定在 HeroesComponent 及其嵌套的 HeroListComponent中,那麼在 HeroesComponent 中提供這個 HeroService 就是一個好選擇。

@Injectable()

標識一個類能夠被注入器實例化,一般在視圖實例化沒有被標識爲@Injectable類時,注入會報錯。

建議爲每一個服務類,都添加@Injectable()裝飾器

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

@Injectable()
export class HeroService{

}

Provide類和提供商字面量

providers:[Logger]

這實際上是用於註冊提供商的簡寫表達式,使用的是一個帶有兩個屬性的提供商字面量

[{ provide: Logger, useClass: Logger }]

第一個是令牌(token),做爲鍵值(key)使用,用於定位依賴值和註冊提供商

第二個是提供商定義對象,能夠當作是如何建立依賴值的配方。

有些時候,若是咱們須要請求一個不一樣的類來提供服務,能夠寫成以下形式

[{ provide: Logger, useClass: BetterLogger }]

當有人請求Logger時,返回BetterLogger。注意:BetterLogger必須繼承Logger

值提供商

能夠提供一個預先作好的對象,會比請求注入器從類中建立它更容易

值得注意的是,對象必須與類一致。

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

@Injectable()
export class Logger {
  logs: string[] = []; // capture logs for testing

  log(message: string) {
    this.logs.push(message);
    console.log(message);
  }
}
providers: [
    {
      provide: Logger, useValue: {
        logs: ['Log'],
        log: () => {
          console.log(`123`);
        }
      }
    }
  ]

工廠提供商

有時候須要動態建立依賴值,由於所須要的信息直到最後一刻才能肯定。這種狀況下,須要調用工廠提供商

建立工廠方法

//hero.service.ts
constructor(
  private logger: Logger,
  private isAuthorized: boolean) { }

getHeroes() {
  let auth = this.isAuthorized ? 'authorized ' : 'unauthorized';
  this.logger.log(`Getting heroes for ${auth} user.`);
  return HEROES.filter(hero => this.isAuthorized || !hero.isSecret);
}

//hero.service.provider.ts
let heroServiceFactory = (logger: Logger, userService: UserService) => {
  return new HeroService(logger, userService.user.isAuthorized);
};

同時把Logger和UserService注入到工廠提供商中,而且讓注入器把他們傳給工廠

export let heroServiceProvider =
  { provide: HeroService,
    useFactory: heroServiceFactory,
    deps: [Logger, UserService]
  };

useFactory字段告訴Angular:這個提供商是一個工廠方法,實現是heroServiceFactory

deps屬性是提供商令牌數組,Logger和UserService類做爲他們自身的提供商的令牌

import { Component }          from '@angular/core';
 
import { heroServiceProvider } from './hero.service.provider';
 
@Component({
  selector: 'app-heroes',
  template: `
  <h2>Heroes</h2>
  <app-hero-list></app-hero-list>
  `,
  providers: [heroServiceProvider]
})
export class HeroesComponent { }

依賴注入令牌

向注入器註冊提供商時,實際上就是把提供商和DI令牌關聯起來。

注入器維護一個內部的令牌-提供商映射表,會在請求依賴時被引用 。令牌就是這個映射表的鍵值。

前面的全部例子中,依賴值都是一個類實例,而且類的類型做爲它本身的查找鍵值。

編寫須要基於類的依賴注入的構造函數很簡單,只須要添加參數便可。

非類依賴

若是依賴值不是一個類,想注入的東西是一個字符串、函數或者對象。例如全局配置

export interface AppConfig {
  apiEndpoint: string;
  title: string;
}

export const HERO_DI_CONFIG: AppConfig = {
  apiEndpoint: 'api.heroes.com',
  title: 'Dependency Injection'
};

使用值提供商來註冊一個對象。

可是,沒辦法找到一個類當作令牌,由於沒有Config類。TypeScript不能把接口用做令牌。

// FAIL! Can't inject using the interface as the parameter type
constructor(private config: AppConfig){ }

InjectionToKend值

解決方案是爲非類依賴定義和使用InjectionToken值做爲提供商令牌。總體代碼以下:

//app-config
export interface AppConfig {
  message: string;
}

export const HERO_CONFIG: AppConfig = {
  message: 'HeroConfig'
}

//app.config
import {InjectionToken} from '@angular/core';

import {AppConfig, HERO_CONFIG} from './app-config';

export let APP_CONFIG = new InjectionToken<AppConfig>('app.config');

//providers.component
import {Component, Inject} from '@angular/core';

import {AppConfig, HERO_CONFIG} from './app-config';
import {APP_CONFIG} from './app.config';

@Component({
  selector: 'app-provider',
  template: `
    <div>{{config.message}}</div>
  `,
  providers: [{provide: APP_CONFIG, useValue: HERO_CONFIG}]
})

export class ProvidersComponent {
  constructor(@Inject(APP_CONFIG) private config: AppConfig) {

  }
}

new InjectionToKend的參數是可選的,填寫之後能夠給開發者和開發工具傳遞信息,提供描述做用。

註冊的時候,直接註冊APP_CONFIG這個變量

構造函數的時候,須要使用@Inject裝飾器來獲取實際的值

可選依賴 

若是不想提供構造函數,也想調用類,能夠把構造函數標記爲@Optional(),告訴Angular該依賴可選。

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

constructor(@Optional() private logger: Logger) {
  if (this.logger) {
    this.logger.log(some_message);
  }
}

當使用Optional的時候,代碼必須準備好如何處理空值,這是參數爲null

多級依賴注入 

Angular有一個多級依賴注入系統,應用程序中有一個與組件樹平行的注入器樹。能夠在組件樹中的任何級別上從新配置注入器。

能夠在組件樹中的任何級別上從新配置注入器。

注入器冒泡

當一個組件申請得到一個依賴時,Angular先嚐試用該組件本身的注入器來知足它。

若是該組件的注入器沒有找到對應的提供商,就把這個申請轉交給他父組件的注入器來處理。

若是那個注入器也沒法知足這個申請,就繼續傳遞給它的父組件的注入器。按照這個順序繼續往上冒泡,知道找到注入器爲止。

DI使用技巧

https://angular.cn/guide/dependency-injection-in-action

HttpClient庫

大多數前端應用都須要經過HTTP協議與後端服務器通信,瀏覽器支持兩種不一樣API發起HTTP請求:XMLHttpRequest接口和fetch()API

@angular/common/http中的HttpClient類,Angular爲應用程序提供了一個簡化的API來實現HTTP功能。

HttpClient帶來其餘的優勢包括:可測試性、強類型的請求和響應對象、發起請求與接收響應時的攔截器支持,更好的、基於可觀察(Observable)的對象錯誤處理機制。

安裝

首先須要在app.module中的imports暴露出來

import {NgModule} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
 
// Import HttpClientModule from @angular/common/http
import {HttpClientModule} from '@angular/common/http';
 
@NgModule({
  imports: [
    BrowserModule,
    // Include it under 'imports' in your application module
    // after BrowserModule.
    HttpClientModule,
  ],
})
export class MyAppModule {}

獲取請求 

HttpClient的get方法能夠直接從api獲取數據

@Component(...)
export class MyComponent implements OnInit {

  results: string[];

  // Inject HttpClient into your component or service.
  constructor(private http: HttpClient) {}

  ngOnInit(): void {
    // Make the HTTP request:
    this.http.get('/api/items').subscribe(data => {
      // Read the result field from the JSON response.
      this.results = data['results'];
    });
  }
}

響應類型檢查 

能夠告訴HttpClient這個響應體應該是什麼類型的,這是推薦作法。

首先要定義接口來描述這個類型的正確形態

interface ItemsResponse {
  results: string[];
}

而後,發起HttpClient.get調用時,傳入一個類型參數:

http.get<ItemsResponse>('/api/items').subscribe(data => {
  // data is now an instance of type ItemsResponse, so you can do this:
  this.results = data.results;
});

完整響應體

響應體可能並不包含咱們須要的所有信息,有時候服務器會返回一個特殊的響應頭或狀態碼。

須要經過observe選項來告訴HttpClient,你想要完整的響應信息,而不是隻有響應體。

http
  .get<MyJsonData>('/data.json', {observe: 'response'})
  .subscribe(resp => {
    // Here, resp is of type HttpResponse<MyJsonData>.
    // You can inspect its headers:
    console.log(resp.headers.get('X-Custom-Header'));
    // And access the body directly, which is typed as MyJsonData as requested.
    console.log(resp.body.someField);
  });

錯誤處理 

若是請求致使了服務器錯誤,HttpClient會返回error。要處理他能夠在subscribe中添加錯誤處理器

http
  .get<ItemsResponse>('/api/items')
  .subscribe(
    // Successful responses call the first callback.
    data => {...},
    // Errors will call this callback instead:
    err => {
      console.log('Something went wrong!');
    }
  );

錯誤分爲兩種

1.後端返回一個失敗的返回碼(404,500) ,會返回一個錯誤

2.客戶端這邊出現錯誤,會拋出一個Error類型的異常

能夠經過instanceof來判斷是那種類型

http
  .get<ItemsResponse>('/api/items')
  .subscribe(
    data => {...},
    (err: HttpErrorResponse) => {
      if (err.error instanceof Error) {
        // A client-side or network error occurred. Handle it accordingly.
        console.log('An error occurred:', err.error.message);
      } else {
        // The backend returned an unsuccessful response code.
        // The response body may contain clues as to what went wrong,
        console.log(`Backend returned code ${err.status}, body was: ${err.error}`);
      }
    }
  );

.retry()操做符 

能夠重試請求,對於臨時性的並且不太可能重複發生的錯誤頗有用。

RxJS有一個叫.retry()的頗有用的操做符,會在遇到錯誤時自動從新訂閱這個可觀察對象,會再次發送請求

import 'rxjs/add/operator/retry';

http
  .get<ItemsResponse>('/api/items')
  // Retry this request up to 3 times.
  .retry(3)
  // Any errors after the 3rd retry will fall through to the app.
  .subscribe(...);

請求非JSON數據

若是須要請求的數據不是JSON,須要告訴HttpClient指望的響應類型。

http
  .get('/textfile.txt', {responseType: 'text'})
  // The Observable returned by get() is of type Observable<string>
  // because a text response was specified. There's no need to pass
  // a <string> type parameter to get().
  .subscribe(data => console.log(data));

發起POST請求

const body = {name: 'Brad'};

http
  .post('/api/developers/add', body)
  // See below - subscribe() is still necessary when using post().
  .subscribe(...);

注意,subscribe方法,全部從HttpClient返回的可觀察對象都是冷的,也就是說,還未發起請求。

在咱們調用subscribe方法以前,什麼都不會發生,而當咱們每次調用subscribe時,就會獨立發起一次請求。

好比,下面代碼會發送兩次請求

const req = http.post('/api/items/add', body);
// 0 requests made - .subscribe() not called.
req.subscribe();
// 1 request made.
req.subscribe();
// 2 requests made.

配置請求中的其餘部分

除了URL和可能的請求體以外,能夠經過傳遞options對象來解決,HTTP頭

添加Authorization頭

http
  .post('/api/items/add', body, {
    headers: new HttpHeaders().set('Authorization', 'my-auth-token'),
  })
  .subscribe();

添加URL參數

http
  .post('/api/items/add', body, {
    params: new HttpParams().set('id', '3'),
  })
  .subscribe();

實際發送的請求會變成,/api/items/add?id=3 這個地址 

攔截器

@angular/common/http 的主要特性之一是攔截器,能聲明一些攔截器,攔在應用和後端之間。

當應用程序發起一個請求時,攔截器能夠在請求被髮往服務器之間先轉換這個請求。

而且在應用看到服務器發回來的響應以前,轉換這個響應。對於處理包括認證和記錄日誌在內的一系列工做都很是有用。

要實現一個攔截器,就要聲明一個實現HttpInterceptor接口的類,只有一個intercept方法。

import {Injectable} from '@angular/core';
import {Observable} from 'rxjs';
import {HttpEvent, HttpInterceptor, HttpHandler, HttpRequest} from '@angular/common/http';
@Injectable() export
class NoopInterceptor implements HttpInterceptor { intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { return next.handle(req); } }

intercept是一個方法,它把一個請求對象轉換成一個返回這個響應的可觀察對象(Observables)。 每一個攔截器都要本身處理請求。

大多時候,攔截器會對請求作一些小修改,而後才傳給攔截器鏈中的其餘部分,這就是傳進來的next參數。

next是一個HttpHandler,是一個相似於intercept的接口,會把一個請求對象轉換成一個可觀察的響應對象。

攔截器中next老是表明位於攔截器鏈中的下一個攔截器,若是沒有更多的攔截器,就會是最終的後端。

大多數攔截器的最後一句都會以它們轉換後請求對象爲參數調用next.handle函數。上面代碼就是簡單的請求,不作修改。

應用模塊設置攔截器

import {NgModule} from '@angular/core';
import {HTTP_INTERCEPTORS} from '@angular/common/http';

@NgModule({
  providers: [{
    provide: HTTP_INTERCEPTORS,
    useClass: NoopInterceptor,
    multi: true,
  }],
})
export class AppModule {}

multi:true是必須的,會告訴Angular這個HTTP_INTERCEPTORS表示一個數組,而不單是一個值。

注意,intercept和Handler.handle返回的可觀察對象並非Observable<HttpResponse<any>>而是Observable<HttpEvent<any>>

攔截器所工做的層級要低於HttpClient接口,單個請求會生成多個事件。好比上傳、下載過程的事件。

HttpReponse類實際上自己也是一個事件,只是它的type是HttpEventType.HttpResponseEvent

攔截器必須透傳全部它不理解或不打算修改的事件,不能過濾本身不許備處理的事件。

順序

當咱們設置多個攔截器時,會按照你提供的順序應用他們,即Providers數組中列出的順序

不可變性 

攔截器要檢查和修改準備發出的請求和接受進來的響應。可是HttpRequest和HttpResponse類倒是不可變的

由於應用可能會重發請求,而攔截器鏈可能會屢次處理同一個請求。若是請求是可變的,每次重試時的請求均可能和原始的請求不同。

而不可變對象能夠確保攔截器每次重試時處理的都是同一個請求。

請求體(body)沒法在寫攔截器時提供保護,在攔截器修改請求體應該是無效的,可是類型檢查系統沒法發現它

若是確實須要修改請求體,就得本身複製它,修改這個複本,而後clone()複製這個請求,並使用新的請求體

intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
  // This is a duplicate. It is exactly the same as the original.
  const dupReq = req.clone();

  // Change the URL and replace 'http://' with 'https://'
  const secureReq = req.clone({url: req.url.replace('http://', 'https://')});
}

設置HTTP頭 

攔截器最多見用途之一是設置默認的請求頭,好比有一個可注入的AuthService,能夠提供一個認證令牌。

而咱們但願寫一個攔截器,把這個令牌添加到全部要發出的請求中。

import {Injectable} from '@angular/core';
import {HttpEvent, HttpInterceptor, HttpHandler, HttpRequest} from '@angular/common/http';
 
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
  constructor(private auth: AuthService) {}
 
  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    // Get the auth header from the service.
    const authHeader = this.auth.getAuthorizationHeader();
    // Clone the request to add the new header.
    const authReq = req.clone({headers: req.headers.set('Authorization', authHeader)});
    // Pass on the cloned request instead of the original request.
    return next.handle(authReq);
  }
}

這種克隆一個請求並設置一組新的請求頭的操做很是常見,所以有了一種快捷寫法

const authReq = req.clone({setHeaders: {Authorization: authHeader}});

這種能夠修改頭的攔截器,能夠用於不少不一樣的操做。好比:

認證/受權、控制緩存行爲,好比If-Modified-Since、XSRF防禦

記日誌 

因爲攔截器能夠同時處理請求和響應,能夠用來記錄日誌或請求計時等。 下面是記錄請求時長的例子

import {HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpResponse} from '@angular/common/http';
import {Observable} from 'rxjs';

export class TimingInterceptor implements HttpInterceptor {
  constructor(private auth: AuthService) {
  }

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    const started = Date.now();
    return next.handle(req).do(event => {
      if (event instanceof HttpResponse) {
        const elapsed = Date.now() - started;
        console.log(`Request for ${req.urlWithParams} took ${elapsed} ms.`);
      }
    });
  }
}

 注意,RxJS的do操做符,能夠爲可觀察對象添加一個反作用,而不會影響到流中的值。

這裏,會檢測HttpResponse的事件,並記錄這個請求所花費的時間。

緩存

能夠使用攔截器來實現緩存,好比,已經寫好的一個HTTP緩存,具備以下簡單的接口

abstract class HttpCache {
  /**
   * Returns a cached response, if any, or null if not present.
   */
  abstract get(req: HttpRequest<any>): HttpResponse<any>|null;

  /**
   * Adds or updates the response in the cache.
   */
  abstract put(req: HttpRequest<any>, resp: HttpResponse<any>): void;
}

攔截器能夠把這個請求緩存應用到所發出的請求上

@Injectable()
export class CachingInterceptor implements HttpInterceptor {
  constructor(private cache: HttpCache) {}

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
      // Before doing anything, it's important to only cache GET requests.
    // Skip this interceptor if the request method isn't GET.
    if (req.method !== 'GET') {
      return next.handle(req);
    }

    // First, check the cache to see if this request exists.
    const cachedResponse = this.cache.get(req);
    if (cachedResponse) {
      // A cached response exists. Serve it instead of forwarding
      // the request to the next handler.
      return Observable.of(cachedResponse);
    }

    // No cached response exists. Go to the network, and cache
    // the response when it arrives.
    return next.handle(req).do(event => {
      // Remember, there may be other events besides just the response.
      if (event instanceof HttpResponse) {
          // Update the cache.
          this.cache.put(req, event);
      }
    });
  }
}

攔截器除了轉換請求外,還有不少強力的功能。能夠徹底接管請求流程。

修改上面例子,改成若是有請求,返回兩個響應 

intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
  // Still skip non-GET requests.
  if (req.method !== 'GET') {
    return next.handle(req);
  }

  // This will be an Observable of the cached value if there is one,
  // or an empty Observable otherwise. It starts out empty.
  let maybeCachedResponse: Observable<HttpEvent<any>> = Observable.empty();

  // Check the cache.
  const cachedResponse = this.cache.get(req);
  if (cachedResponse) {
    maybeCachedResponse = Observable.of(cachedResponse);
  }

  // Create an Observable (but don't subscribe) that represents making
  // the network request and caching the value.
  const networkResponse = next.handle(req).do(event => {
    // Just like before, check for the HttpResponse event and cache it.
    if (event instanceof HttpResponse) {
      this.cache.put(req, event);
    }
  });

  // Now, combine the two and send the cached response first (if there is
  // one), and the network response second.
  return Observable.concat(maybeCachedResponse, networkResponse);
}

若是URL被緩存過,那麼任何人調用http.get(URL)都會收到兩個響應。

監聽進度事件

有時候應用須要傳輸大量數據,過程當中給用戶反饋能帶來更好的體驗,而@angular/common/http就支持它

要發起要給支持進度事件的請求,首先要建立一個reportProgress選項的一個HttpRequest實例

const req = new HttpRequest('POST', '/upload/file', file, {
  reportProgress: true,
});

該選項讓咱們能夠跟蹤事件進度,每一個進度事件都會出發變動檢測,因此只有真的打算在每一個事件中更新UI纔打開它

接下來經過HttpClient的request方法發起請求,其結果應該是一個關於事件的可觀察對象,就像攔截器中看到的那樣

http.request(req).subscribe(event => {
  // Via this API, you get access to the raw event stream.
  // Look for upload progress events.
  if (event.type === HttpEventType.UploadProgress) {
    // This is an upload progress event. Compute and show the % done:
    const percentDone = Math.round(100 * event.loaded / event.total);
    console.log(`File is ${percentDone}% uploaded.`);
  } else if (event instanceof HttpResponse) {
    console.log('File is completely uploaded!');
  }
});

安全XSRF防禦 

跨請求僞造(XSRF)是一個攻擊技術,能讓攻擊者假冒一個已認證的用戶在你的網站上進行未知的操做。

HttpClient支持一種通用的機制來防範XSRF攻擊,當執行HTTP請求時,一個攔截器會從cookie中讀取XSRF令牌(默認名字爲XSRF-TOKEN)

而且把它設置成一個HTTP頭X-XSRF-TOKEN,因爲只有運行在咱們本身的域名下的代碼才能讀取到這個cookie

所以後端能夠確認這個HTTP請求真的來自咱們的客戶端應用,而不是攻擊者。

默認狀況下,攔截器會在全部的修改型請求中(好比POST等)把這個cookie發送給使用相對URL的請求,但不會在GET/HEAD請求中發送,也不會發送給使用絕對URL的請求。

要得到這種優勢,咱們的服務器須要在頁面加載或首個GET請求中把一個名叫XSRF-TOKEN的令牌寫入可被JavaScript讀取到的會話cookie。

而在後續的請求中,服務器能夠驗證這個cookie是否與HTTP頭X-XSRF-TOKEN的值一致。以確保只有運行時在咱們本身的域名下的代碼才能發起這個請求

這個令牌必須對每一個用戶都是惟一的,而且必須能被服務器驗證,所以不能由客戶端生成本身的令牌,把這個令牌設置爲你的站點認證信息並加了摘要,提高安全性。

爲了防止多個Angular應用共享同一個域名或子域的時候出現衝突,要給每一個應用分配一個惟一的cookie名稱。

配置自定義cookie/hander名稱

若是咱們的後端服務中對XSRF令牌的cookie或頭使用了不同的名稱,就要使用HttpClientXsfModule.withConfig來覆蓋掉默認值

imports: [
  HttpClientModule,
  HttpClientXsrfModule.withConfig({
    cookieName: 'My-Xsrf-Cookie',
    headerName: 'My-Xsrf-Header',
  }),
]

路由與導航

Angular的路由器能讓用戶從一個視圖導航到另外一個視圖

瀏覽器具備咱們熟悉的導航模式:

1.在地址欄輸入URL,瀏覽器就會導航到相應的頁面

2.在頁面中點擊連接,瀏覽器就會導航到一個新頁面。

3.點擊瀏覽器的前進和後退按鈕,瀏覽器就會在你的瀏覽記錄中向前或向後導航。

Angular的Router借鑑了這個模型,它把瀏覽器中的URL看做一個操做指南,據此導航到一個由客戶端生成的視圖,並能夠把參數傳給支撐視圖的相應組件

幫他決定具體該展示哪些內容,咱們能夠爲頁面中的連接綁定一個路由,這樣當用戶點擊連接時,就會導航到相應的視圖中。

當用戶點擊按鈕、從下拉框拉取,或響應來自任何地方的事件時,也能夠在代碼控制下進行導航。

路由器在瀏覽器的歷史記錄下這些活動,這樣瀏覽器的前進和後退按鈕也能照常工做。

<base href>元素

大多數帶路由的應用都要在index.html的<head>標籤下添加<base href="/">,來告訴路由器該如何合成導航用的URL。

路由導入

Angular的路由器是一個可選的服務,用來呈現指定的URL所對應的視圖,並非Angular核心庫的一部分,而是在他本身的@angular/router包中

像其餘的Angular包同樣,能夠從它導入所需的一切。

import { RouterModule, Routes } from '@angular/router';

配置 

每一個帶路由的Angular應用都有一個Router(路由器)服務的單例對象。當瀏覽器URL變換時,路由器會查找對應的Route路由,並決定顯示那個組件。

路由器須要先配置纔會有路由信息,下面例子建立了四個路由定義,並用RouterModule.forRoot方式來配置路由器,並把返回值添加到AppModule的imports數組中。

const appRoutes: Routes = [
  { path: 'crisis-center', component: CrisisListComponent },
  { path: 'hero/:id',      component: HeroDetailComponent },
  {
    path: 'heroes',
    component: HeroListComponent,
    data: { title: 'Heroes List' }
  },
  { path: '',
    redirectTo: '/heroes',
    pathMatch: 'full'
  },
  { path: '**', component: PageNotFoundComponent }
];

@NgModule({
  imports: [
    RouterModule.forRoot(
      appRoutes,
      { enableTracing: true } // <-- debugging purposes only
    )
    // other imports here
  ],
  ...
})
export class AppModule { }

這裏的路由數組appRoutes描述如何進行導航,把它傳遞給RouterModule.forRoot方法並傳給本模塊的imports數組便可

每一個Router都會把一個URL的path映射到一個組件,注意path不能以斜槓(/)開頭。路由器會解析和構建最終的URL。

這樣當咱們在多個視圖之間導航時,就能夠任意使用相對、絕對路徑了。

第一個路由器:是寫的靜態路徑,對應的組件,當URL是該地址,則會跳轉到對應組件。

第二個路由器:中的:id是一個路由參數的令牌(Token),好比/hero/42中42就是id參數的值。此URL對應的HeroDetailComponent組件將據此查找和展現id爲42的數據

第三個路由器:中的data屬性用來存放每一個具體路由相關的任意信息,該數據能夠被任何一個激活路由訪問,並能用來綁定靜態只讀數據

第四個路由器: 空路徑表示應用的默認路徑,URL爲空會訪問到哪裏,所以一般會做爲起點,默認重定向到/heroes路由。

第五個路由器:路徑**是一個通配符,當所請求的URL不匹配前面定義的路由表中的任何路徑時,會選擇此路由。可用於404頁面等

路由的定義順序是刻意如此設計的,使用先匹配者優先的策略來匹配路由,因此具體路由應該放在通用路由的前面。

在上面配置中,帶靜態路徑的路由被放在前面,後面是空路徑路由,所以它會做爲默認路由。

而通配符路由被放在最後面,這是由於他能匹配上每個URL,所以應該只有在前面找不到其餘匹配的路由時才匹配它

若是咱們想看到在導航的生命週期中發生過那些事情,能夠使用配置路由器的enableTracing選項,會把每一個導航生命週期中的事件輸出到瀏覽器的控制檯。

這應該只是用於調試,只須要把enableTracing:true選項做爲第二個參數傳遞給RouterModule.forRoot方法就能夠了。

路由出口

有了這份配置,當應用的URL變爲對應路由時,就會匹配到對應組件。並在宿主視圖中,RouterOutlet以後顯示。

<router-outlet></router-outlet>
<!-- Routed views go here -->

路由器連接

template: `
  <h1>Angular Router</h1>
  <nav>
    <a routerLink="/crisis-center" routerLinkActive="active">Crisis Center</a>
    <a routerLink="/heroes" routerLinkActive="active">Heroes</a>
  </nav>
  <router-outlet></router-outlet>
`

a標籤上的RouterLink指令讓路由器得以控制這個a元素,這裏的導航路徑是固定的,所以能夠把一個字符串賦值給routeLink

若是須要動態添加導航路徑,就須要把它綁定在一個返回連接參數數組的模板表達式,路由器會把這個數組解析成完整的URL

每一個a標籤上的RouterLinkActive指令,能夠幫用戶在外觀區分出當前選中的活動路由。

當與它關聯的RouterLink被激活時,路由器會把CSS類active添加到這個元素上,能夠把指令添加到a元素或它的父元素上。

路由器狀態 

在導航時的每一個生命週期成功完成時,路由器會構建出一個ActivatedRoute組成的樹。表示路由器的當前狀態。

能夠在應用中的任何地方用Router服務及其routerState屬性來訪問當前的RouterState值。

激活的路由

該路由的路徑和參數能夠經過注入一個名叫ActivatedRouter的路由服務來獲取,他有一大堆信息,包括:

Property 屬性 Description 描述
url

路由路徑的Observable對象,是一個由路由路徑中的各個部分組成的字符串數組。

data

一個Observable,其中包含提供給路由的data對象。也包含由解析守衛(resolve guard)解析而來的值。

paramMap

一個Observable,其中包含一個由當前路由的必要參數和可選參數組成的map對象。用這個map能夠獲取來自同名參數的單一值或多重值。

queryParamMap

一個Observable,其中包含一個對全部路由都有效的查詢參數組成的map對象。 用這個map能夠獲取來自查詢參數的單一值或多重值。

fragment

An Observable of the URL fragment available to all routes.

outlet

要把該路由渲染到的RouterOutlet的名字。對於無名路由,它的路由名是primary,而不是空串。

routeConfig

用於該路由的路由配置信息,其中包含原始路徑。

parent

當該路由是一個子路由時,表示該路由的父級ActivatedRoute

firstChild

包含該路由的子路由列表中的第一個ActivatedRoute

children

包含當前路由下全部已激活的子路由

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

路由事件

每次導航中,Router都經過Router.event屬性發布一些導航事件,這些事件的範圍覆蓋了從開始導航到結束導航之間的不少時間點。

Router Event 路由器事件 Description 描述
NavigationStart

事件會在導航開始時觸發。

RoutesRecognized

事件會在路由器解析完URL,並識別出了相應的路由時觸發

RouteConfigLoadStart

事件會在Router對一個路由配置進行惰性加載以前觸發。

RouteConfigLoadEnd

事件會在路由被惰性加載以後觸發。

NavigationEnd

事件會在導航成功結束以後觸發。

NavigationCancel

事件會在導航被取消以後觸發。 這多是由於在導航期間某個路由守衛返回了false

NavigationError

這個事件會在導航因爲意料以外的錯誤而失敗時觸發。

 

 

 

 

 

 

 

 

 

 

 

當打開enableTracing選項時,這些事件也同時會記錄到控制檯中,因爲這些事件是以Observable的形式提供的。

因此咱們能夠對本身感興趣的事件進行filter(),並subscribe()他們,以便根據導航過程當中的時間順序作出決策。

總結

應用有一個配置過的路由器,外殼組件中有一個RouterOutlet,能顯示路由器所生成的視圖,還有一些RouterLink,用戶能夠點擊他們來進行導航。

路由器部件

含義

Router(路由器)

爲激活的URL顯示應用組件。管理從一個組件到另外一個組件的導航

RouterModule(路由器模塊)

一個獨立的Angular模塊,用於提供所需的服務提供商,以及用來在應用視圖之間進行導航的指令。

Routes(路由數組)

定義了一個路由數組,每個都會把一個URL路徑映射到一個組件。

Route(路由)

定義路由器該如何根據URL模式(pattern)來導航到組件。大多數路由都由路徑和組件類構成。

RouterOutlet(路由出口)

該指令(<router-outlet>)用來標記出路由器該在哪裏顯示視圖。

RouterLink(路由連接)

該指令用來把一個可點擊的HTML元素綁定到路由。 點擊帶有綁定到字符串連接參數數組routerLink指令的元素就會觸發一次導航。

RouterLinkActive(活動路由連接)

當HTML元素上或元素內的routerLink變爲激活或非激活狀態時,該指令爲這個HTML元素添加或移除CSS類。

ActivatedRoute(激活的路由)

爲每一個路由組件提供提供的一個服務,它包含特定於路由的信息,好比路由參數、靜態數據、解析數據、全局查詢參數和全局碎片(fragment)。

RouterState(路由器狀態)

路由器的當前狀態包含了一棵由程序中激活的路由構成的樹。它包含一些用於遍歷路由樹的快捷方法。

連接參數數組

這個數組會被路由器解釋成一個路由操做指南。咱們能夠把一個RouterLink綁定到該數組,或者把它做爲參數傳給Router.navigate方法。

路由組件

一個帶有RouterOutlet的Angular組件,它根據路由器的導航來顯示相應的視圖。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

範例應用

實例源碼在router項目中,在此處記錄知識點

RouterOutlet指令,是一個來自路由庫的組件,路由器會在<router-outlet>標籤中顯示視圖。

一個模板只能一個未命名的<router-outlet>,可是路由器能夠支持多個命名的出口outlet。

重定向路由須要一個pathMatch屬性,告訴路由器如何用URL去匹配路由的路徑,路由器只在完整的URL等於空時才選擇,所以須要把pathMatch設置爲full

使用區域特性配置

能夠在子目錄中配置NgModel和路由,這樣能夠用於將模塊區分開。注意:NgModel配置和路由配置,必定要在一塊兒

//heroes-routing.module.ts

import { NgModule }             from '@angular/core';
import { RouterModule, Routes } from '@angular/router';

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

const heroesRoutes: Routes = [
  { path: 'heroes',  component: HeroListComponent },
  { path: 'hero/:id', component: HeroDetailComponent }
];

@NgModule({
  imports: [
    RouterModule.forChild(heroesRoutes)
  ],
  exports: [
    RouterModule
  ]
})
export class HeroRoutingModule { }

此處必定要exports導出RouterModule,否則路由訪問不到

特性模塊中註冊服務提供商,須要使用forChild,只有在根目錄時須要使用forRoot。

還須要在區域配置中,把路由配置暴露出來。

//heroes.module.ts

import { NgModule }       from '@angular/core';
import { CommonModule }   from '@angular/common';
import { FormsModule }    from '@angular/forms';

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

import { HeroService } from './hero.service';

import { HeroRoutingModule } from './heroes-routing.module';

@NgModule({
  imports: [
    CommonModule,
    FormsModule,
    HeroRoutingModule
  ],
  declarations: [
    HeroListComponent,
    HeroDetailComponent
  ],
  providers: [ HeroService ]
})
export class HeroesModule {}

最後還須要把區域特性,配置到全局特性中,才能完成總體配置

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

import { AppComponent }     from './app.component';
import { AppRoutingModule } from './app-routing.module';
import { HeroesModule }     from './heroes/heroes.module';

import { CrisisListComponent }   from './crisis-list.component';
import { PageNotFoundComponent } from './not-found.component';

@NgModule({
  imports: [
    BrowserModule,
    FormsModule,
    HeroesModule,
    AppRoutingModule
  ],
  declarations: [
    AppComponent,
    CrisisListComponent,
    PageNotFoundComponent
  ],
  bootstrap: [ AppComponent ]
})
export class AppModule { }

導入模塊的順序很重要 

注意,AppRoutingModule是最後一個,位於區域特性HeroesModule以後。

路由配置很重要,路由器會接受第一個匹配上導航所要求的路徑的那個路由。

當全部路由都在同一個AppRoutingModule時,要把默認路由和通配符路由放在最後,這樣纔會匹配到具體的路由。

當路由不是單一文件,每一個模塊都會根據導入的順序把本身的路由配置追加進去。

若是先列出AppRoutingModule,那麼通配符路由就會被註冊在Hero路由前面,會攔截每個請求。

Activated Route

Actvated Router能夠從路由器的URL中解析出路由參數

首先須要導入Router、ActivatedRoute、Params類

import { Router, ActivatedRoute, ParamMap } from '@angular/router';

導入switchMap操做符,將會處理路由參數的可觀察對象(Observable)

import 'rxjs/add/operator/switchMap';

一般會直接寫構造函數,讓Angular把組件所需的服務注入進來 

//hero-detail.component.ts
constructor(
private route: ActivatedRoute, private router: Router, private service: HeroService ) {}

而後在ngOnInit方法中,用ActivatedRoute服務來接收路由的參數,從參數中取得該英雄的id,並顯示此英雄

hero$: Observable<Hero>;

  ngOnInit() {
    this.hero$ = this.route.paramMap
      .switchMap((params: ParamMap) =>
        this.service.getHero(params.get('id')));
  } 

因爲參數是做爲Observable提供的,因此咱們得用switchMap操做符來根據名字取得id參數。

switchMap操做符也會取消之前未完成的在途請求,若是使用心得id再次導航,那麼會拋棄老的請求。

這個可觀察對象的Subscription將會由AsyncPipe處理,而且組件的hero屬性將會設置爲剛剛接收到的這個英雄。

ParamMap參數API

提供了一些方法來處理對路由參數(paramMap)和查詢參數(queryParamMap)中的訪問。

Member 成員 Description 描述
has(name)

若是參數名位於參數列表中,就返回 true 。

get(name)

若是這個map中有參數名對應的參數值(字符串),就返回它,不然返回null。若是參數值其實是一個數組,就返回它的第一個元素。

getAll(name)

若是這個map中有參數名對應的值,就返回一個字符串數組,不然返回空數組。當一個參數名可能對應多個值的時候,請使用getAll

keys

返回這個map中的全部參數名組成的字符串數組。

 

 

 

 

 

 

 

 

參數的可觀察對象(Observable)與組件複用

上面訂閱了路由參數的Observable對象,這種寫法暗示着路由參數在該組件的生存期內可能會變化。

默認狀況下,若是沒有訪問過其餘組件就導航到同一個組件實例,路由器傾向於複用組件實例。若是複用參數可能變化

假設父組件的導航欄有前進、後退按鈕,用來輪流顯示英雄列表中英雄的詳情。每次點擊都會強制導航到前一個或後一個id的HeroDetailComponent組件。

不但願路由器僅僅從DOM中移除當前的實例,而且用下一個id從新建立它,那可能致使頁面抖動,更好的方式是複用同一個組件實例,並更新這些參數。

可是ngOnInit對每一個實例只調用一次,須要一種方式來檢測在同一個實例中路由參數何時發生變化,而params屬性這個Observable處理了這種狀況。

當組件中訂閱一個可觀察對象時,一般老是要在組件銷燬時取消這個訂閱。

可是也有少數狀況不須要取消訂閱,ActivateRoute中的各類可觀察對象就是屬於這種狀況。

ActivateRoute及其可觀察對象都是由Router自己負責管理的。Router會在不在須要時銷燬這個路由組件,而注入的ActivateRoute也隨之銷燬。

Snapchat(快照):當不須要Observable時的替代品

若是肯定某個組件的實例,永遠不會被複用,就能夠使用快照來簡化這段代碼。

route.snapchat提供了路由參數的初始值,能夠經過它來直接訪問參數,而不用訂閱或者添加Observable的操做符

ngOnInit() {
  let id = this.route.snapshot.paramMap.get('id');

  this.hero$ = this.service.getHero(id);
}

用這種技巧,只獲得這些參數的初始值,若是有可能連續屢次導航到此組件,那麼就該用params可觀察對象的方式。

咱們這裏使用params可觀察對象策略,以防萬一。

navigate

使用router的navigate,能夠導航到對應的路由中,例如咱們作一個後退按鈕

gotoHeroes() {
  this.router.navigate(['/heroes']);
}

可選對象

傳送一個包含可選id參數的對象,在對象中定義一個沒用的額外參數(foo)

gotoHeroes(hero: Hero) {
  let heroId = hero ? hero.id : null;
  // Pass along the hero id if available
  // so that the HeroList component can select that hero.
  // Include a junk 'foo' property for fun.
  this.router.navigate(['/heroes', { id: heroId, foo: 'foo' }]);
}

此時瀏覽器地址欄的地址爲:localhost:3000/heroes;id=15;foo=foo 

id的值出如今URL中,但不在URL的路徑部分,可選的路由參數沒用使用?和&分隔,是用;分隔的。

ActivatedRoute服務中的路由參數

export class HeroListComponent implements OnInit {
  heroes$: Observable<Hero[]>;

  private selectedId: number;

  constructor(
    private service: HeroService,
    private route: ActivatedRoute
  ) {}

  ngOnInit() {
    this.heroes$ = this.route.paramMap
      .switchMap((params: ParamMap) => {
        // (+) before `params.get()` turns the string into a number
        this.selectedId = +params.get('id');
        return this.service.getHeroes();
      });
  }
}

ActivatedRoute.paramMap屬性是一個路由參數的可觀察對象,當用戶導航到這個組件時,會發出一個新的值其中包含id

在ngOnInit中,訂閱了這些值,設置到selectedId上,並獲取英雄數據。

用CSS類綁定更新模板,綁定到isSelected方法上,若是該方法返回true,此綁定就會添加CSS類selected,不然就移除它

template: `
  <h2>HEROES</h2>
  <ul class="items">
    <li *ngFor="let hero of heroes$ | async"
      [class.selected]="hero.id === selectedId">
      <a [routerLink]="['/hero', hero.id]">
        <span class="badge">{{ hero.id }}</span>{{ hero.name }}
      </a>
    </li>
  </ul>

  <button routerLink="/sidekicks">Go to sidekicks</button>
`

 子路由

Angular應用推薦的模式爲:每一個特性放在本身的目錄中

1.每一個特性都有本身的Angular特性模塊。

2.每一個特性區都有本身的根組件

3.每一個特性區的根組件中都有本身的路由出口及其子路由

4.特性區的路由不多(或徹底不)與其餘特性區的路由交叉

若是有更多的特性區,組件樹是這樣的:

特性區也能夠設置本身的特區殼和特區路由。

特區Component

//crisis-center.component.ts

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

@Component({
  template:  `
    <h2>CRISIS CENTER</h2>
    <router-outlet></router-outlet>
  `
})
export class CrisisCenterComponent { }

子路由配置

//crisis-center-routing.module.ts

import {NgModule} from '@angular/core';
import {Routes, RouterModule} from '@angular/router';

import {CrisisCenterComponent} from './crisis-center.component';
import {CrisisListComponent} from './crisis-list.component';
import {CrisisDetailComponent} from './crisis-detail.component';
import {CrisisCenterHomeComponent} from './crisis-center-home.component';

const crisisCenterRoutes: Routes = [
  {
    path: 'crisis-center',
    component: CrisisCenterComponent,
    children: [
      {
        path: '',
        component: CrisisListComponent,
        children: [
          {
            path: ':id',
            component: CrisisDetailComponent
          }, {
            path: '',
            component: CrisisCenterHomeComponent
          }
        ]
      }
    ]
  }
];

@NgModule({
  imports: [
    RouterModule.forChild(crisisCenterRoutes)
  ],
  exports: [
    RouterModule
  ]
})

export class CristsCenterRoutingModule {
}

父路由crisis-center有一個children屬性,包含一個List的路由。List路由還有一個children數組帶兩個路由。

路由器會把路由對應的組件放在CrisisCenter中的RouterOutlet中,而不是AppCommponent殼中。

//crisis-list.component

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

@Component({
  template: `
    <h3>CrisisList</h3>
    <router-outlet></router-outlet>
  `
})

export class CrisisListComponent {
}

List包含RouterOutlet,用來顯示CenterHome和Detail。

Detail是List的子路由,路由器默認會複用組件,當選擇另外一個時會被複用。

在頂級,以/開頭的路徑指向的老是應用的根,但這裏是子路由。是在父路由路徑的基礎上作出的擴展。

在路由樹中每深一步,就會在路由的路徑上添加一個斜線"/",除非路徑是空的。

若是把該邏輯應用到導航中,父路徑就是/cris-center

相對路徑

能夠把輸出路徑改成相對於當前URL的路徑,這樣就能夠把連接從依賴中解放出來。

當修改該特性區的父路由時,該特性區內部的導航仍然無缺如初。

在連接參數數組中,路由器支持目錄式語法來指導咱們如何查詢路由名。

./ 或 無前導線 形式是相對於當前級別的

../ 會回到當前路由路徑的上一級

this.router.navigate(['../']);

用Router.navigate方法導航到相對路徑時,必須提供當前的ActivatedRoute,讓路由器知道咱們如今的位置。

在連接參數數組中,添加一個帶有relativeTo的屬性對象,並設置爲當前的ActivatedRoute。

這樣路由器就會基於當前激活路由的位置來計算目標URL 。(當調用路由器的navigateByUrl時,老是要指定完整的絕對路徑。)

this.router.navigate(['../', { id: crisisId, foo: 'foo' }], { relativeTo: this.route });

命名路由 

模板能夠有多個命名路由,每一個命名出口都有本身的一組帶組件的路由。多重出口能夠在同一時間根據不一樣的路由來顯示不一樣內容。

例如,在app.component,添加一個命名路由

<router-outlet></router-outlet>
<router-outlet name="popup"></router-outlet>

第二路由很像主路由,配置方式也同樣,只有一些關鍵點不一樣

1.他們彼此不依賴,與其餘路由組合使用

2.顯示在命名出口中

在路由配置時,就能夠直接定義命名路由出口

{
  path: 'compose',
  component: ComposeMessageComponent,
  outlet: 'popup'
},

只配置路由還不行,routerLink也要直接配置

<a [routerLink]="[{ outlets: { popup: ['compose'] } }]">Contact</a>

連接參數數組包含一個outlets屬性,這個對象對應出口名。

清除第二路由

路由之間互不影響,若是想在主路由中清楚第二路由,能夠寫成以下形式

closePopup() {
  // Providing a `null` value to the named outlet
  // clears the contents of the named outlet
  this.router.navigate([{ outlets: { popup: null }}]);
}

路由守衛 

任何用戶都能在任什麼時候候導航到任何地方,這樣是不對的

1.該用戶可能無權導航到目標組件

2.可能用戶得先登陸(認證)

3.在顯示目標組件前,咱們可能得先獲取某些數據

4.在離開組件前,可能要先保存修改

5.可能須要詢問用戶,是否放棄本次修改

能夠往配置中添加守衛來處理這些場景。

守衛返回一個值,以控制路由器的行爲

1.若是它返回true,導航過程會繼續

2.若是它返回false,導航過程會終止,用戶會留在原地

守衛還能夠控制路由器導航到別的地方

路由守衛能夠返回一個Observable<boolean>或Promise<boolean>,而且路由器會等待這個可觀察對象被解析爲true或false

路由支持多個保護接口

用CanActivate來處理導航到某個路由的狀況

用CanActivateChild來處理導航到某個子路由的狀況

用CanDeactivate來處理從當前路由離開的狀況

用Resolve在路由激活以前獲取路由數據

用CanLoad來處理異步導航到某特性模塊的狀況

在分層路由的每一個層級上,均可以設置多個守衛,路由器會先按照從最深到子路由由下往上檢查的順序來檢查CanDeactivate和CanActivateChild守衛

而後會按照從上到下的順序檢查CanActivate守衛,若是特性模塊是異步加載的,加載以前還會檢查CanLoad守衛。

若是任何一個守衛返回false,其它還沒有完成的守衛會被取消,這樣整個導航就取消了。

CanActivate要求認證

應用程序一般會根據訪問者來決定是否授予某個特性區的訪問權。能夠只對已認證的用戶或具備特定角色的用戶授予訪問權。

還能夠阻止或限制用戶訪問權,直到用戶帳戶激活爲止。

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

@Component({
  template: `
    <h3>ADMIN</h3>
    <nav>
      <a routerLink="./" routerLinkActive="active"
         [routerLinkActiveOptions]="{ exact: true }">Dashboard</a>
      <a routerLink="./crises" routerLinkActive="active">Manage Crises</a>
      <a routerLink="./heroes" routerLinkActive="active">Manage Heroes</a>
    </nav>
    <router-outlet></router-outlet>
  `,
  styles: [
      `
      nav a.active {
        color: #039be5;
      }
    `
  ]
})
export class AdminComponent {
}

routerLink爲./的路由屬於空路由,會匹配到管理特性區的任何路由,可是咱們想只有Dashboard時,才激活routerLinkActive。

此時就須要添加綁定[routerLinkActiveOptions]="{ exact: true }",這樣只有導航到/admin時,纔會激活。

無組件路由

const adminRoutes: Routes = [
  {
    path: 'admin',
    component: AdminComponent,
    children: [
      {
        path: '',
        children: [
          {path: 'crises', component: ManageCrisesComponent},
          {path: 'heroes', component: ManageHeroesComponent},
          {path: '', component: AdminDashboardComponent}
        ]
      }
    ]
  }
];

咱們的目標是對admin路徑下的類進行路由分組,並不須要另外一個僅用來分組路由的組件

一個無組件路由就能讓咱們輕鬆地守衛子路由。

CanActivte:要求認證

咱們寫一個CanActivte守衛,將正在嘗試訪問管理組件的匿名用戶重定向登陸頁。

這是一種通用性的守護目標,咱們在應用的根目錄下建立一個auth-guard.ts文件。

第一個版本讓守衛先寫一個日誌

//auth-guard.service.ts
import { Injectable }     from '@angular/core';
import { CanActivate }    from '@angular/router';

@Injectable()
export class AuthGuard implements CanActivate {
  canActivate() {
    console.log('AuthGuard#canActivate called');
    return true;
  }
}

接下來,咱們把守衛註冊到路由裏面,讓該路由相關的請求,都通過守衛 

import { AuthGuard }                from '../auth-guard.service';

const adminRoutes: Routes = [
  {
    path: 'admin',
    component: AdminComponent,
    canActivate: [AuthGuard],
    children: [
      {
        path: '',
        children: [
          { path: 'crises', component: ManageCrisesComponent },
          { path: 'heroes', component: ManageHeroesComponent },
          { path: '', component: AdminDashboardComponent }
        ],
      }
    ]
  }
];

@NgModule({
  imports: [
    RouterModule.forChild(adminRoutes)
  ],
  exports: [
    RouterModule
  ]
})
export class AdminRoutingModule {}

這個時候無組件路由就派上用場了,能夠給一組路由設置一個守衛便可。

而後建立認證程序,來模擬用戶登陸相關操做。

//auth.service.ts
import { Injectable } from '@angular/core';

import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
import 'rxjs/add/operator/do';
import 'rxjs/add/operator/delay';

@Injectable()
export class AuthService {
  isLoggedIn = false;

  // store the URL so we can redirect after logging in
  redirectUrl: string;

  login(): Observable<boolean> {
    return Observable.of(true).delay(1000).do(val => this.isLoggedIn = true);
  }

  logout(): void {
    this.isLoggedIn = false;
  }
}

isLoggedIn用來標識用戶是否已經登陸過, login方法會仿真發起一個http請求,返回一個可觀察對象(observable)

而後咱們修改路由守衛來讓他調用auth.service

import {Injectable} from '@angular/core';
import {CanActivate, Router, ActivatedRouteSnapshot, RouterStateSnapshot} from '@angular/router';

import {AuthService} from './auth.service';

@Injectable()
export class AuthGuard implements CanActivate {
  constructor(private authService: AuthService, private router: Router) {
  }

  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
    const url: string = state.url;
    return this.checkLogin(url);
  }

  checkLogin(url: string): boolean {
    if (this.authService.isLoggedIn) {
      return true;
    }
    this.authService.redirectUrl = url;
    this.router.navigate(['/login']);
    return false;
  }
}

把AuthService和Router服務注入到構造函數中, 能夠往路由守衛中注入有用的服務。

ActivatedRouteSnashot包含了即將被激活的路由,RouterStateSnapshot包含了應用即將到達的狀態,要經過咱們的守衛進行檢查。

若是用戶沒有登陸,用RouterStateSnapshot.url保存來自用戶的URL並讓路由器導航到登陸頁。

間接致使了路由器自動停止了此次導航,checkLogin返回false並非必須的,可是可讓代碼結構更清楚。

CanActivateChild:保護子路由

CanActivateChild和CanActive守衛很像,區別在於,CanActivatedChild會在任何子路由被激活以前運行。

咱們要保護管理特性模塊,防止他被非受權訪問,還要保護這個特性模塊內部的那些子路由。

擴展AuthGuard,以便在admin路由之間導航時提供保護。打開auth-gurad.service並從路由庫導入CanActivateChild接口

import {Injectable} from '@angular/core';
import {CanActivate, CanActivateChild, Router, ActivatedRouteSnapshot, RouterStateSnapshot} from '@angular/router';

import {AuthService} from './auth.service';

@Injectable()
export class AuthGuard implements CanActivate, CanActivateChild {
  constructor(private authService: AuthService, private router: Router) {
  }

  canActivateChild(childRoute: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
    return this.canActivate(childRoute, state);
  }

  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
    const url: string = state.url;
    return this.checkLogin(url);
  }

  checkLogin(url: string): boolean {
    if (this.authService.isLoggedIn) {
      return true;
    }
    this.authService.redirectUrl = url;
    this.router.navigate(['/login']);
    return false;
  }
}

一樣把這個AuthGuard添加到無組件路由中,同時保護它的子路由 

const adminRoutes: Routes = [
  {
    path: 'admin',
    component: AdminComponent,
    canActivate: [AuthGuard],
    children: [
      {
        path: '',
        canActivateChild: [AuthGuard],
        children: [
          {path: 'crises', component: ManageCrisesComponent},
          {path: 'heroes', component: ManageHeroesComponent},
          {path: '', component: AdminDashboardComponent}
        ]
      }
    ]
  }
];

CanDeactivate:處理未保存的更改

把用戶的改動積累起來,這些改動保存成一種特定狀態,直到用戶把這些改動做爲一組進行確認或撤銷全部改動。

當用戶要導航到外側,應該暫停並讓用戶決定該怎麼作,若是選擇取消就留下來,不然進行保存。

咱們建立一個Guard,檢查這個組件中canDeactivate函數的工做現場。並不須要知道組件確認退出的激活狀態詳情,讓咱們的守衛能夠複用。

import { Injectable }    from '@angular/core';
import { CanDeactivate } from '@angular/router';
import { Observable }    from 'rxjs/Observable';

export interface CanComponentDeactivate {
 canDeactivate: () => Observable<boolean> | Promise<boolean> | boolean;
}

@Injectable()
export class CanDeactivateGuard implements CanDeactivate<CanComponentDeactivate> {
  canDeactivate(component: CanComponentDeactivate) {
    return component.canDeactivate ? component.canDeactivate() : true;
  }
}

能夠爲Detail組件建立一個特定的CanDeactivate守衛,須要訪問外部信息時,canDeactivate()方法提供了組件、ActivatedRoute和RouterStateSnapshot的當前實例。

若是想要爲這個組件使用該守衛,而且須要使用該組件屬性、或者須要路由器確認是否容許導航出去時,這個守衛就很重要。

import {Observable} from 'rxjs/observable';
import {Injectable} from '@angular/core';
import {CanDeactivate, ActivatedRouteSnapshot, RouterStateSnapshot} from '@angular/router';

import {CrisisDetailComponent} from './crisis-center/crisis-detail.component';

@Injectable()
export class CanDeactivateGuard implements CanDeactivate<CrisisDetailComponent> {
  canDeactivate(component: CrisisDetailComponent, route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | boolean {
    // Get the Crisis Center ID
    console.log(route.paramMap.get('id'));

    // Get the current URL
    console.log(state.url);

    // Allow synchronous navigation (`true`) if no crisis or the crisis is unchanged
    if (!component.crisis || component.crisis.name === component.editName) {
      return true;
    }
    // Otherwise ask the user with the dialog service and return its
    // observable which resolves to true or false when the user decides
    return component.dialogService.confirm('Discard changes?');
  }
}

注意,canDeactivate方法能夠同步返回,若是沒有未定的修改,就當即返回true,也能夠返回一個Promise或Observable。

路由器將等待它們被解析爲真值(繼續導航)或假值(留下)

如今往路由器中須要此守衛的地方,添加canDeactivate數組添加一個Guard守衛

const crisisCenterRoutes: Routes = [
  {
    path: '',
    redirectTo: '/crisis-center',
    pathMatch: 'full'
  },
  {
    path: 'crisis-center',
    component: CrisisCenterComponent,
    children: [
      {
        path: '',
        component: CrisisListComponent,
        children: [
          {
            path: ':id',
            component: CrisisDetailComponent,
            canDeactivate: [CanDeactivateGuard]
          },
          {
            path: '',
            component: CrisisCenterHomeComponent
          }
        ]
      }
    ]
  }
];

還須要把守衛添加到依賴注入裏面

@NgModule({
  imports: [
    RouterModule.forRoot(
      appRoutes,
      { enableTracing: true } // <-- debugging purposes only
    )
  ],
  exports: [
    RouterModule
  ],
  providers: [
    CanDeactivateGuard
  ]
})

Resolve:預先獲取組件數據 

https://angular.cn/guide/router#resolve-pre-fetching-component-data

異步路由

使用異步路由到應用程序中,並得到在請求時才惰性加載特性模塊的能力。這樣給咱們帶來如下好處:

1.能夠只在用戶請求時才加載某些特性區

2.對於那些只訪問應用程序某些區域的用戶,這樣能加快加載速度

3.能夠持續擴充惰性加載特性區的功能,而不用增長初始加載的包體積

有些模塊(AppModule)必須在啓動時加載,但其餘的均可以而且應該惰性加載,好比AdminModule就只有少數的人使用。

惰性加載路由配置

首先須要把特性模塊的默認路由改成空。Router支持空路徑路由,來進行分組路由,而且不用添加額外的路徑片斷。

//admin-routing.module

import {NgModule} from '@angular/core';
import {Routes, RouterModule} from '@angular/router';

import {AdminComponent} from './admin.component';
import {ManageCrisesComponent} from './manage-crises.component';
import {ManageHeroesComponent} from './manage-heroes.component';
import {AdminDashboardComponent} from './admin-dashboard.component';
import {AuthGuard} from '../auth-guard.service';

const adminRoutes: Routes = [
  {
    path: '',
    component: AdminComponent,
    canActivate: [AuthGuard],
    children: [
      {
        path: '',
        canActivateChild: [AuthGuard],
        children: [
          {path: 'crises', component: ManageCrisesComponent},
          {path: 'heroes', component: ManageHeroesComponent},
          {path: '', component: AdminDashboardComponent}
        ]
      }
    ]
  }
];

@NgModule({
  imports: [
    RouterModule.forChild(adminRoutes)
  ],
  exports: [
    RouterModule
  ]
})
export class AdminRoutingModule {
}

而後須要在主路由上,把一個新的admin路由,添加到appRouter數組中。在主路由上,控制路徑選項。記得在空路徑前添加 

給他一個loadChildren屬性(注意不是children屬性),把它設置爲AdminModule的地址。

該地址是AdminModule的文件路徑(相對於app目錄)加上一個#號分隔符,再加上導出模塊的類名AdminModule

//app-routing.module

import {NgModule} from '@angular/core';
import {RouterModule, Routes} from '@angular/router';

import {NotFoundComponent} from './not-found.component';

const appRoutes: Routes = [
  {
    path: 'admin',
    loadChildren: 'app/admin/admin.module#AdminModule',
  }, {
    path: '',
    redirectTo: '/demo',
    pathMatch: 'full'
  }, {
    path: '**',
    component: NotFoundComponent
  }
];

@NgModule({
  imports: [
    RouterModule.forRoot(appRoutes, {enableTracing: true})
  ],
  exports: [RouterModule]
})

export class AppRoutingModule {
}

當路由器導航到這個路由時,會用loadChildren字符串來動態加載AdminModule,而後把AdminModule添加到當前的路由配置中。

惰性加載和從新配置工做只會發生一次,也就是該路由首次被請求時。在後續的請求中,都是當即可用的。

最後一步,根模塊AppModule既不能加載也不能引用AdminModule及其文件

在app.module中,從頂部移出AdminModule,並從imports數組中刪除。

CanLoad守衛:保護對特性模塊的未受權加載

咱們已經使用CanActivate保護AdminModule了,它會阻止未受權用戶訪問管理特性區。

若是用戶未登陸,就會跳轉到登陸頁。可是路由依然會加載AdminModule,即便用戶沒法訪問。

理想狀況下,應該是用戶已登陸的狀況下,咱們才加載AdminModule。

添加一個CanLoad守衛,只在用戶已登陸而且嘗試訪問管理特性區時,才加載AdminModule一次。

如今AuthGuard的checkLogin方法中已經有了支持CanLoad守衛的基礎邏輯。

打開auth-guard.service從@angular/router中導入CanLoad接口,把它添加到AuthGuard類的接口列表中

canLoad(route: Route): Observable<boolean> | Promise<boolean> | boolean {
    let url = `/${route.path}`;
    return this.checkLogin(url);
  }

路由器會把canLoad方法的route參數設置爲準備訪問的目標URL,用戶已登陸,checkLogin()方法就會重定向URL。

而後須要導入AppRoutingModule中,添加到admin路由的canLoad數組中

{
    path: 'admin',
    loadChildren: 'app/admin/admin.module#AdminModule',
    canLoad: [AuthGuard]
  },

預加載:特性區的後臺加載 

AppModule在應用啓動時就被加載了,是當即加載。而AdminModule只有當用戶點擊某個連接時纔會加載,是惰性加載。

預加載是介於二者之間的一種方式,咱們應該對AppModule和首先加載的模塊加速。而後開始後臺加載。

這就是預加載

預加載的工做原理

每次成功導航後,路由器會在本身的配置中查找還沒有加載而且能夠預加載的模塊,是否加載以及要加載的模塊,取決於預加載策略。

Router內置了兩種加載策略:

1.徹底不預加載,這是默認值,惰性加載的特性區仍然會按需加載

2.預加載全部惰性加載的特性區

默認狀況下,路由器或徹底不預加載或者預加載每一個惰性加載模塊,路由器還支持自定義預加載策略,以便徹底控制。

預加載全部惰性加載特性區 

從Angular路由中導入PreloadAllModules

app-routing.module中RouterModule.forRoot方法的第二個參數接受一個附加配置選項對象。preloadingStrategy就是其中之一

把PreloadAllModules添加到forRoot調用中。 

@NgModule({
  imports: [
    RouterModule.forRoot(appRoutes, {enableTracing: true, preloadingStrategy: PreloadAllModules})
  ],
  exports: [RouterModule]
})

這會讓Router預加載器當即加載全部惰性加載路由(帶loadChildren屬性的路由) 

CanLoad會阻塞預加載

PreloadAllModule策略不會被加載,由於他被CanLoad守衛所保護的特性區。CanLoad守衛優先級高於預加載策略

自定義預加載策略

大多數場景下,預加載每一個惰性模塊就很好了。咱們能夠只選擇對某些特性模塊進行加載。

添加項目selective-preloading-strategy

import 'rxjs/add/observable/of';
import { Injectable } from '@angular/core';
import { PreloadingStrategy, Route } from '@angular/router';
import { Observable } from 'rxjs/Observable';

@Injectable()
export class SelectivePreloadingStrategy implements PreloadingStrategy {
  preloadedModules: string[] = [];

  preload(route: Route, load: () => Observable<any>): Observable<any> {
    if (route.data && route.data['preload']) {
      // add the route path to the preloaded module array
      this.preloadedModules.push(route.path);

      // log the route path to the console
      console.log('Preloaded: ' + route.path);

      return load();
    } else {
      return Observable.of(null);
    }
  }
}

SelectivePreloadingStrategy實現了PreloaddingStrategy,只有一個preload方法。

路由器會用兩個參數調用preload方法

1.要加載的路由

2.一個加載器函數,能異步加載帶路由的模塊

preload的實現必須返回一個Observable,若是該路由應該預加載,就會返回調用加載器函數所返回的Observable。

若是該路由不該該預加載,就返回一個null值的Observable對象。

preload方法只有在路由的data.preload標識爲真時纔會加載該路由。

import { Component, OnInit }    from '@angular/core';
import { ActivatedRoute }       from '@angular/router';
import { Observable }           from 'rxjs/Observable';

import { SelectivePreloadingStrategy } from '../selective-preloading-strategy';

import 'rxjs/add/operator/map';

@Component({
  template:  `
    <p>Dashboard</p>

    <p>Session ID: {{ sessionId | async }}</p>
    <a id="anchor"></a>
    <p>Token: {{ token | async }}</p>

    Preloaded Modules
    <ul>
      <li *ngFor="let module of modules">{{ module }}</li>
    </ul>
  `
})
export class AdminDashboardComponent implements OnInit {
  sessionId: Observable<string>;
  token: Observable<string>;
  modules: string[];

  constructor(
    private route: ActivatedRoute,
    private preloadStrategy: SelectivePreloadingStrategy
  ) {
    this.modules = preloadStrategy.preloadedModules;
  }

  ngOnInit() {
    // Capture the session ID if available
    this.sessionId = this.route
      .queryParamMap
      .map(params => params.get('session_id') || 'None');

    // Capture the fragment if available
    this.token = this.route
      .fragment
      .map(fragment => fragment || 'None');
  }
}

審查路由配置 

經過注入他(Router)並檢查它的config屬性,能夠隨時審查路由器的當前配置。

把AppModule修改爲這樣,就會在控制檯窗口中查看最終的路由配置。

import { Router } from '@angular/router';

export class AppModule {
  // Diagnostic only: inspect router configuration
  constructor(router: Router) {
    console.log('Routes: ', JSON.stringify(router.config, undefined, 2));
  }
}
相關文章
相關標籤/搜索