前言:本文是我在工做中應用和實踐Angular時的一系列知識總結,本文是做爲我在團隊內作的一次技術分享的輔助和大綱。雖然沒辦法分享現場的音頻,可是我也花了很多心思來準備這篇文章,但願能對剛接觸Angular的同窗有所幫助。 本次先帶來第一階段的分享:Angular初探-應用架構,適合在通讀Angular官方文檔(中文)至少1遍後參考本文來梳理一些比較大的概念。html
理解 Angular,首先須要理解三大核心概念:模塊、組件和服務,其他的特性都是基於這三大概念衍生出來的。好比組件與服務之間有依賴注入特性,模塊爲組件和服務提供了編譯的上下文以及一些功能(指令、管道等)支持。視圖的更新依賴於雙向綁定,視圖的變換對應着組件的切換,而組件的切換須要路由機制......git
Angular 應用是模塊化的,它擁有本身的模塊化系統,稱做 NgModule。github
一個 NgModule 就是一個容器,用於存放一些內聚的代碼塊,這些代碼塊專一於某個應用領域、某個工做流或一組緊密相關的功能。 它能夠包含一些組件、服務或其它代碼文件,他們的做用域由包含它們的 NgModule 定義。編程
他做爲模塊還能夠導入一些由其它模塊中導出的功能,同時自身也能夠導出一些指定的功能供其它 NgModule 使用。bootstrap
實現上,NgModule 是一個帶有 @NgModule 裝飾器的類:數組
關於 es7 裝飾器,能夠參考我翻譯的這篇文章:探索 EcmaScript 裝飾器瀏覽器
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
@NgModule({
imports: [ BrowserModule ], // 導入了本模塊中的組件模板所需的類的其它模塊
providers: [ Logger ], // 本模塊嚮應用全局貢獻的服務。 這些服務能被本應用中的任何部分使用。
declarations: [ AppComponent ], // 屬於本 NgModule 的組件、指令、管道
// 每一個組件都應該(且只能)聲明在一個 NgModule 類中。
exports: [ AppComponent ], // 能在其它模塊的組件模板中使用的可聲明對象的子集
bootstrap?: [ AppComponent ] // 應用的主視圖,稱爲根組件。它是應用中全部其它視圖的宿主。
// Angular 建立它並插入 index.html 宿主頁面。
// 只有根模塊才應該設置這個 bootstrap 屬性。
entryComponents?: [SomeComponent]
})
export class AppModule { }
複製代碼
@NgModule 的參數是一個元數據對象,用於描述如何編譯組件的模板,以及如何在運行時建立注入器。緩存
它會標出該模塊本身的組件、指令和管道,經過 exports 屬性公開其中的一部分,以便外部組件使用它們。 NgModule 還能把一些服務提供商添加到應用的依賴注入器中。bash
Angular 應用中使用的模塊其實有兩種,一種是 JavaScript 模塊,一種是 NgModule。服務器
關於 js 模塊系統,能夠參考我翻譯的這篇文章:模塊系統
在 angular 內部,將 NgModule 劃分爲了兩種(注意,不管根模塊仍是特性模塊,其 NgModule 結構都是同樣的,只是從功能上劃分出這兩個概念):
根模塊:顧名思義,「根」模塊必然爲整個 Angular 應用的基礎和核心,他的功能就是將全部開發者自創的特性模塊組織起來,接收一些全局的配置項,以及引導應用在瀏覽器中啓動。根模塊與其餘任何特性模塊的一個定義方式上的差別就是根模塊裝飾器的元數據中多了一個 bootstrap 屬性,經過 bootstrap 屬性聲明一個應用的根組件(入口組件),而後基於這個根組件來生長整個應用的組件樹,呈現出一個完整的應用。
這裏插播一個組件的概念:入口組件(entry component)。他是命令式生成的一種組件,區別於聲明式(開發者顯式地在模板中引用某一個組件)的方式,入口組件多是根組件(由 Angular 的啓動機制自動加載到 index.html 模板中),也能夠是定義在路由中的組件(此時由路由器根據相應的路由規則來動態插入到當前視圖中)。
理論上說,全部的入口組件都應該聲明到@NgModule 裝飾器的 entryComponents 元數據對象屬性中,可是 Angular 編譯器會隱式地把根組件和路由定義的組件添加爲 entryComponents,因此咱們不須要顯示聲明這一類入口組件了。
Angular 編譯器只會爲那些能夠從 entryComponents 中直接或間接訪問到的組件生成代碼,內部使用搖樹優化(tree shaker)來剝離那些無關的組件,保持應用的精簡和高效。基於這個原理,有時候咱們可能會使用到一些 UI 庫提供的組件(彈窗),爲了不這類組件被編譯器排除,咱們就須要手動在 entryComponents 中添加這些組件的引用聲明。
特性模塊:能夠理解爲開發者建立的其餘 NgModule 的統稱。根據不一樣特性模塊的功能特徵,又能夠分爲:
這個分類其實只是爲了區分不一樣 NgModule 的功能,是一個組織應用的最佳實踐準則,並不須要嚴格劃分。舉個例子,幾乎每個複雜的模塊都須要定義路由,可是咱們能夠把路由這個關注點專門抽離出來,統一到一個路由特性模塊中,這樣方便咱們統必定義和劃分應用路由層次。這樣就能夠在應用不斷成長時保持應用的良好結構,而且當複用本模塊時,你能夠輕鬆的讓其路由保持無缺。
組件控制屏幕上被稱爲視圖的一小片區域。咱們在類中定義組件的應用邏輯,爲視圖提供數據和行爲支持。 組件經過一些由屬性和方法組成的 API 與視圖交互。
實現上,經過@Component 裝飾器和元數據來將一個類標記爲組件:
@Component({
selector: 'app-hero-list',
templateUrl: './hero-list.component.html',
providers: [HeroService]
})
export class HeroListComponent implements OnInit {
/* . . . */
}
複製代碼
模板很像標準的 HTML,可是它還包含 Angular 的模板語法,這些模板語法能夠根據應用邏輯、應用狀態和 DOM 數據來修改這些 HTML。 模板可使用數據綁定來協調應用和 DOM 中的數據,使用管道在顯示出來以前對其進行轉換,使用指令來把程序邏輯應用到要顯示的內容上。
下面的例子:
<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></app-hero-detail>
。這些模板語法元素告訴 Angular 該如何根據程序邏輯和數據在屏幕上渲染 HTML。
模板語法分紅三類:數據綁定,管道和指令。
經過特定的綁定語法,Angular 能夠自動地將組件中的屬性和方法與模板對應起來,創建一種帶有方向的映射關係。
上面的例子中,{{hero.name}}
的語法稱爲插值表達式,他是一種從組件類綁定屬性到 DOM 的模板語法。
[hero]="selectedHero"
的語法稱爲屬性綁定,他也是一種將組件類的屬性綁定到 DOM 的模板語法。他們之間的區別在於,使用插值表達式是爲了顯示對應的數據到視圖中,而屬性綁定是一種數據的傳遞,將宿主組件的屬性傳遞到子組件或者指令中,而後由接受者決定如何使用這部分數據,某種意義上說,屬性綁定是一種通訊機制。
(click)="selectHero(hero)"
的語法稱爲事件綁定,這裏的事件是一系列用戶觸發的 UI 行爲,鼠標操做,鍵盤行爲等,這些行爲附帶一些信息。咱們須要從 DOM 監聽到這些行爲和信息,而後傳遞給咱們的組件類中相應的處理函數,在組件類中來處理這些行爲,好比更新視圖中的部分屬性,向服務器請求一些數據,甚至是引發視圖的變換等。很明顯,它的綁定方向是從 DOM 到組件的,與前兩種相反。
實際上事件綁定一樣支持開發者本身定義的事件,這部分會在深刻使用部分詳細介紹。
還有一種綁定形式是 angular 最爲著名的雙向綁定。他是屬性綁定和事件綁定的結合,語法以下:
<input [(ngModel)]="hero.name">
複製代碼
在 Angular 內部會將這種語法作即時、自動的處理(隱式),用戶的輸入會自動更新到組件類中對應的屬性上,而組件類中對應屬性的變化也會當即反映到視圖中來。
管道是一類轉換函數,他接收一個輸入,經過預先設定的處理規則對輸入作加工和轉化,而後輸出結果,並用結果替換掉本來的輸入。 管道的使用須要與插值表達式和管道操做符( | )結合起來:
<p>Today is {{today | date}}</p>
經過管道咱們能夠很方便地對視圖內一些數據進行統一轉化,以更爲友好的方式來展現這些數據。同時他又具備通用性,他從本該作這部分工做的組件類中分離出來,讓咱們的組件類更加精簡和專一於業務。
指令就是一個帶有 @Directive 裝飾器的類。和組件同樣,指令的元數據把指令類和一個選擇器關聯起來,選擇器用來把該指令插入到 HTML 中。 他們都是 Angular 編譯器對模板作解析和編譯的基礎,Angular 編譯器找到這些選擇器 指令分爲結構型指令和屬性性指令,結構型指令經過添加、移除或替換 DOM 元素來修改佈局,屬性型指令會修改現有元素的外觀或行爲。
上文中提到的*ngIf
就是一個結構型指令,而 ngModel 則是一個屬性型指令。
模板會把 HTML 和 Angular 的模板標記語法組合起來,這些模板標記語法能夠在 HTML 元素顯示出來以前修改它們。
模板中的指令會提供程序邏輯,而綁定語法會把你應用中的數據和 DOM 鏈接在一塊兒。
在視圖顯示出來以前,Angular 會先根據你的應用數據和邏輯來運行模板中的指令並解析綁定表達式,以修改 HTML 元素和 DOM。
Angular 支持雙向數據綁定,這意味着 DOM 中發生的變化(好比用戶的選擇)一樣能夠反映回你的程序數據中。
模板也能夠用管道轉換要顯示的值以加強用戶體驗。
在用戶使用應用程序時,Angular 的路由器能讓用戶從一個視圖導航到另外一個視圖。
首先分析一下瀏覽器的導航模式:
Angular 的 Router(即「路由器」)借鑑了這個模型:
每一個帶路由的 Angular 應用都有一個 Router(路由器)服務的單例對象。 當瀏覽器的 URL 變化時,路由器會查找對應的 Route(路由),並據此決定該顯示哪一個組件。
2.2.1 路由配置
咱們使用路由配置來聲明應用中咱們預設的一些路由規則,這樣 Angular 的路由器就能夠按照這些預設的規則將 URL 的變化與咱們的應用視圖變換對應起來。
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 }
];
複製代碼
2.2.2 路由出口
路由出口就是用於告訴 Angular 路由器,當 URL 的變化匹配到路由配置裏的某一項的path
時,在模板中的何處插入component
對應的組件實例。
<router-outlet></router-outlet>
<!-- 路由命中時的組件會被插入到此處 -->
複製代碼
2.2.3 路由連接
稍微瞭解過 html 的話,就會知道頁面中的跳轉、連接是以<a href="SOME_URL">
標籤的形式來實現的,這種方式在 Angular 裏有另外一種形式,經過一個叫routerLink
的指令來作這件事,由於 Angular 是單頁應用,儘管看起來在一個 Angular 應用裏能夠處處穿梭,頁面不斷變化,但這只是一個障眼法,是 Angular 經過不斷修改同一個頁面中的 DOM,不斷建立、更新、隱藏、銷燬一個個組件來實現的。因此要想在 Angular 裏作「跳轉」的操做,也須要一個障眼法機制,他就是路由連接。
<a routerLink="/heroes" routerLinkActive="active">Heroes</a>
複製代碼
理解服務這個概念,能夠從他的詞性來入手。名詞性質的服務是服務這個概念的具體實現,狹義地講,他就是一個類,這個類有一些明確的用途。
服務作了一類事情,好比從服務器獲取數據並預處理,驗證用戶的輸入,或者是寫日誌,在內存中緩存一些能夠被全局應用共享的數據或狀態,還能夠做爲組件之間通訊的中間介質。
動詞性質的服務則與組件關聯起來,依賴注入(Dependency Injection)是他們之間的橋樑,服務是生產者,組件是服務的消費者。
Angular 裏將組件和服務區分開,是爲了提升模塊性和複用性,同時進一步將組件的功能劃分出來,讓組件更加專一於特定的業務。
依賴注入是 Angular 另外一個著名的特性,他是不少面嚮對象語言框架設計原則中的「控制反轉」的一種實現方式。在依賴注入模式中,應用組件無需關注所依賴對象的建立和初始化過程,能夠認爲框架已初始化好了,開發者只管調用便可。
依賴注入有利於應用程序中各模塊之間的解耦,使得代碼更容易維護。這種優點可能一開始體現不出來,但隨着項目複雜度的增長,各模塊、組件、第三方服務等相互調用更頻繁時,依賴注入的優勢就體現出來了。開發者能夠專一於所依賴對象的消費,無需關注這些依賴對象的產生過程,這將大大提高開發效率。
舉個例子:
export class Car {
public engine: Engine;
public tires: Tires;
public description = 'No DI';
constructor() {
this.engine = new Engine();
this.tires = new Tires();
}
}
複製代碼
Car 類在本身的構造函數中建立了它所需的一切。這樣作的話 Car 類是脆弱、不靈活以及難於測試的。
爲何?
Car 類須要一個引擎 (engine) 和一些輪胎 (tire),它沒有去請求現成的實例, 而是在構造函數中用具體的 Engine 和 Tires 類實例化出本身的副本。若是 Engine 類升級了,它的構造函數要求傳入一個參數,此時這個 Car 類就被破壞了,在更新 Engine 實例化方式以前 Car 類都不可用,原本是 Engine 類的更改,引發了連鎖反應,連累了 Car 類也要跟着修改,Car 類變得脆弱了。 另外一方面,測試的時候更麻煩了,你會受制於它背後的那些依賴,你須要考慮 Engine 類的建立是否成功了,若是 Engine 類還有依賴,那就得一層一層繼續深刻,原本測試一個 Car 類,結果你無法控制這輛車背後隱藏的依賴。 當不能控制依賴時,類就會變得難以測試。
如何讓 Car 類更強壯、可測試?
答案是把 Car 的構造函數改形成 DI 的版本:
constructor(public engine: Engine, public tires: Tires) { } 複製代碼
而後咱們在建立一輛車的時候就能夠往構造函數中傳入 Engine 和 Tire 的實例來實例化 Car 類:
let car = new Car(new Engine(), new Tires()); 複製代碼
這樣一來引擎和輪胎這兩個依賴的定義與 Car 類自己解耦了。若是有人擴展了 Engine 類,那就再也不是 Car 類的煩惱了。
剛剛瞭解了什麼是依賴注入:它是一種編程模式,可讓類從外部源中得到它的依賴,而沒必要親自建立它們。他是用來建立對象及其依賴的其它對象的一種方式。 當依賴注入系統建立某個對象實例時,會負責提供該對象所依賴的對象(稱爲該對象的依賴)。
可是上面的嘗試,只作到了 Car 類的可維護性,可是對於要實例化 Car 類的外部環境來講,工做量反而多了:本來只關心 Car 類的構造,如今連 Engine 和 Tire 類的構造工做也要作了,WTF?
若是,有一種機制,咱們只須要直接列出想要的東西,而不用管這些東西如何建造,直接拿現成的,該多好。
像這樣:let car = injector.get(Car);
,使用 Car 的消費者不須要知道如何建立 Car 類,Car 類也不須要知道如何建立 Engine 和 Tire 類,這些工做都交給注入器,無論是消費者仍是 Car 類,都直接向注入器索要須要的依賴。
這種機制就是「依賴注入框架」。A 依賴於 B,那 B 就是 A 的依賴,同時 A 和 B 都會被這個 DI 框架維護起來。
首先介紹幾個簡單的概念。
在把 Angular 中的服務類註冊進依賴注入器以前,它只是個普通類而已。Angular 自己無法自動判斷你是打算自行建立服務類的實例,仍是等注入器來建立它。你必須經過爲每一個服務指定服務提供商來配置它。提供商會告訴注入器如何建立該服務,而後 Angular 的依賴注入器負責建立服務的實例,並把它們注入到組件類中。你不多須要本身建立 Angular 的依賴注入器。 當 Angular 運行本應用時,它會爲你建立這些注入器,首先會在引導過程當中建立一個根注入器。
有不少方式能夠爲注入器註冊服務提供商。
3.2.1 在服務類前加上裝飾器@Injectable 和相應的元數據來標記出該服務類是用來注入的,也即配置了一個提供商。
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root' // providedIn 告訴 Angular,它的根注入器要負責調用 HeroService 類的構造函數來建立一個實例,並讓它在整個應用中都是可用的。
})
export class HeroService {
constructor() {}
}
複製代碼
3.2.2 在 NgModule 的 providers 元數據中註冊提供商
providers: [
UserService, // { provide: UserService, useClass: UserService }
{ provide: APP_CONFIG, useValue: SOME_DI_CONFIG }
],
複製代碼
3.2.3 在組件中注入服務
Angular 在底層作了大量的初始化工做,這大大簡化了建立依賴注入的過程,在組件中使用依賴注入須要完成如下三個步驟:
當 Angular 建立組件類的新實例時,它會經過查看該組件類的構造函數,來決定該組件依賴哪些服務或其它依賴項。
當 Angular 發現某個組件依賴某個服務時,它會首先檢查是否注入器中已經有了那個服務的任何現有實例。若是所請求的服務尚不存在,注入器就會使用之前註冊的服務提供商來製做一個,並把它加入注入器中,而後把該服務返回給 Angular。
當全部請求的服務已解析並返回時,Angular 能夠用這些服務實例爲參數,調用該組件的構造函數。
import { Component } from '@angular/core';
import { SomeService } from './some-service.service';
@Component({
selector: 'app-some-component',
templateUrl: './some-component.component.html',
styleUrls: ['./some-component.component.less']
})
export class SomeComponent {
constructor(private _someService: SomeService) {}
}
複製代碼
基於以上知識,咱們來分析一個最小化的 Angular 應用,看看以上內容是怎麼有機結合起來的。
這部份內容不便於文字展現,這裏就省略了。