天天看點

Angular2 架構淺析

本文将從架構角度簡單介紹Angular2的基本架構,如果你對Angular2還不太了解,請先閱讀另兩篇文章Angular2入門教程1 和 ​​

Angular2入門教程-2 實作TodoList App​​ ,然後再閱讀此文。

Angular2是一個完整的單頁應用開發架構。很多人拿它跟React比,相比來說,React是一個基礎架構,更像是一個庫,你需要很多第三方的庫才能友善的開發一個完整的應用。而Angular2則提供了很多元件,或者叫庫,比如Directive(指令)、元件架構、模闆、依賴注入、綁定、路由等,在這些庫的幫助下,你更多的隻需要關注具體業務的開發。

當你編寫一個Angular2的應用的時,你從一個Component開始,編寫這個Component需要展示的頁面模闆,并在Component中控制模闆上顯示的内容和使用者的互動。一般還需要編寫一個service來實作業務邏輯。你還需要針對這個模闆編寫一個樣式。最後,你需要把這個Component和service定義到一個module(子產品)裡。

這樣就完成了這個應用子產品的開發。剩下的就是用Angular提供的方法去啟動應用的 ​

​root module​

​ ,也就是根子產品。如果你的應用有幾個子產品需要互動或跳轉,就需要定義路由。

是以,你隻需要開發業務相關的Component元件,以及元件之間的資料互動和頁面跳轉,剩下的像頁面檔案的渲染、變量的雙向綁定、使用者事件的響應、頁面的跳轉和參數傳遞等等,都由架構來完成。

在這篇文章裡,我們就來看看Angular2架構背後提供的一些功能,了解了這些,有助于我們了解一個Angular應用是怎麼工作的,進而幫助我們更好的利用架構,開發出更高品質的應用。

Angular2架構,提供了以下幾個基本的功能:

  • 子產品
  • 元件
  • 服務
  • 模闆
  • 資料綁定
  • 依賴注入

子產品是一個業務功能的集合,我們可以把幾個元件、服務和其他一些業務模型的定義都加到一個子產品裡,他的功能更多的是幫助我們更好的組織我們的代碼,友善代碼重用。模闆、資料綁定、依賴注入是定義一套這個架構的規則和文法,我們用這套規則和文法編寫的代碼,就能夠享受Angular2給我們帶來的便利。

子產品

Angular2的子產品可以将元件、服務、指令、方法等封裝成一個子產品,如下圖所示(圖檔摘自官網):

Angular2 架構淺析

比如我們開發一個系統,裡面包含一個”我的消息”的功能,這個功能包含一些元件,如消息清單、詳細詳情、回複、新消息和好友清單等。除了這些元件,我們又需要相應的服務來跟伺服器互動來提供資料。我們可能也需要一些環境變量等。我們可以把這些元件、服務等都封裝在一個方法裡面,像這樣:

import{ NgModule }from'@angular/core';
import{ CommonModule }from'@angular/common';
import{ FormsModule }from'@angular/forms';

import{ FriendModule }from'./friend/friend.module';

import{ MessageListComponent }from'./message/list.component';
import{ MessageDetailComponent }from'./message/detail.component';
import{ MessageFormComponent }from'./message/form.component';
import{ MessageService }from'./todo.service';

@NgModule({
 imports: [CommonModule, FormsModule, FriendModule ],
 exports: [MessageListComponent],
 declarations: [MessageListComponent, MessageDetailComponent, MessageFormComponent],
 providers: [MessageService]
})
exportclassMessageModule{}      

在上面的子產品定義中,我們又引入了一個 ​

​FriendModule​

​ ,因為好友的功能在另一個子產品裡,我們不需要重新實作,而隻是引入他既可以。我們也可以隻引入好友子產品裡面的某一個元件,也可以隻引入服務。

在MessageModule中,在 ​

​exports: [MessageListComponent]​

​ 中導出了MessageListComponent,這樣,别的子產品可以使用這個元件,來顯示消息清單。

在應用的根子產品中,除了上面的這些’imports’,’providers’這些定義以外,還有一個 ​

​bootstrap: [AppComponent]​

​ ,表示這個應用一開始會加載這個元件到index頁面中。

在Angular2中,提供了很多子產品,例如上面的 ​

​FormsModule​

​​ 、 ​

​CommonModule​

​ , 還有Router等。他們都是一個個子產品,也算是庫,可以單獨引入使用,也可以隻引入需要的部分。

元件和指令

Angular2的元件是一個可重用的單元,包含模版、樣式,還有資料、事件等互動邏輯。

下面之前文章中TodoList應用中的一個元件:

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

import{ Todo }from'../todo';
import{ TodoService }from'../todo.service';

@Component({
 selector: 'todo-list',
 templateUrl: 'app/todo/list/list.component.html',
 styleUrls: ['app/todo//list/list.component.css']
})
exportclassTodoListComponent{
 newTodo: Todo = newTodo();
constructor(private todoService: TodoService) { }
 addTodo() {
this.todoService.addTodo(this.newTodo);
this.newTodo =newTodo();
 }
 get todos() {
returnthis.todoService.getAllTodos();
 }
}      

Angular2的元件通過一個@Component的Decorator(裝飾器)定義一個元件TodoListComponent,元件中有2個方法,一個用于初始化任務清單資料,一個用于相應頁面上的建立任務的事件。

在@Component中,定義了這個元件使用的模闆、樣式,和在它的父元件中所在的位置,也就是html标簽 ​

​<todo-list>​

​ 。

在Angular2裡,Directive(指令)跟元件類似,工作原理也跟上面類似。它跟元件一樣,也是定義一種可重用的結構,添加使用者互動。實際上,在Angular2中,Component繼承自Directive接口,并提供了模闆相關的屬性和功能。

雖然元件和指令差別不大,但是,因為它在Angular2裡面非常重要,而且是我們開發應用的基本機關,是以它被獨立出來,并被放在Angular2架構的核心位置。

有關元件,需要說明的是,在一個Angular2的應用中,元件是一個屬性的結構,就好像html的DOM樹一樣,每個Angular2應用都有一個根元件,然後它會有一個個的子元件。在我們之前的一篇文章《Angular2入門教程-2》中我們在設計系統的時候,就是從設計元件開始,得到的是一個元件樹:

Angular2 架構淺析

每個元件(除了根元件)都會有一個父元件,每個元件定義中的 ​

​selector​

​ 的值,對應父元件中的一個html标簽。

中繼資料

中繼資料就是在定義子產品、元件、服務的時候,Decorator方法裡面的參數内容,例如一個AppComponent的中繼資料,就是 ​

​@Component​

​ 裡面的參數,如下:

{
 selector: 'root-app',
 templateUrl: 'app/app.component.html',
 styleUrls: ['app/app.component.css']
}      

在Angular2中,Decorator被大量使用,當我們定義模闆、元件、服務、指令時,都是使用Decorator來定義。顧名思義,Decorator(裝飾器)就是在一個類上面添加一些額外的屬性或方法。

舉個例子,上面的根元件AppComponent,在定義它的時候,通過 ​

​@Component​

​ 才能把它定義成一個Angular的元件。然後我們在這個中繼資料裡面,設定了這個元件對應的selector,模闆和樣式。這樣Angular架構在解析這個類的時候,就會按照元件的規則去解析并初始化。當在一個頁面裡面遇到這個selector設定的标簽(如這個例子中的 )時,就會初始化這個元件,渲染模闆生成html顯示到對應的标簽裡面,并應用樣式。

服務

在Angular2中,服務是一個很寬泛的定義,任何的類都可以被定義成服務,這個類中可以包含一些業務方法,可以包含環境配置變量。Angular2也沒有對服務的定義做任何的規則限制。下面就是一個最簡單的服務:

exportclassSomeService{
 someConfig: {foo: 'bar'}
 getConfig() { returnsomeConfig; }
 handle(msg: any) { console.log(msg); }
}      

我們隻需要定義一個class,并把它export就可以。但是,一般我們都是結合依賴注入來使用服務。

依賴注入

從Angular1的版本開始,依賴注入就是一個很核心的概念,在版本2中,主要是用于管理service執行個體的注入。在上面講的service中,我們建立了一個SomeService,在傳統的用法中,我們都是在需要用他的地方手動建立一個這個類的執行個體,然後調用他的相應方法或屬性,例如:

letmyService =newSomeService();
myService.handle('the message');      

但是,當我們的系統中有很多service類,甚至這些service類互相之間又需要引用的時候,我們就沒有辦法都通過手動建立的方式來擷取service執行個體。更重要的是,這對于系統的解耦非常不便,不同的服務之間直接建立和引用,會讓系統變得非常難以維護。

Angular給我們提供了一個非常好的解決方案,它借用了java等語言中某些容器庫的概念,将所有service執行個體的建立都由容器來完成,當一個service需要引用另一個service的時候,不是自己建立另一個service的執行個體,而是從容器中擷取那個service的執行個體。

要使用依賴注入的功能,首先我們的service必須由一個裝飾器 ​

​@Injectable​

​ 來定義:

@Injectable()
exportclassSomeService{
// 跟之前一樣,省略...
}      

然後,在Component中需要加一個providers,也就是服務的建立者:

@Component({
 selector: 'some-list',
 templateUrl: 'some.component.html',
 providers: [ SomeService ]
})
exportclassSomeComponent{
constructor(private theService: SomeService) { }
// 省略其他方法。
},      

這樣這個服務就可以在SomeComponent中自動注入了。它的構造函數中有一個參數theService,類型是SomeService,Angular在建立這個Component的時候,就會從容器裡面查找SomeService類的執行個體,如果有就用這個執行個體去初始化SomeComponent對象;如果沒有就先建立一個,再初始化。這個過程,就是Angular的依賴注入。

有關依賴注入,需要注意的一點就是依賴注入的作用範圍。Angular2的依賴注入是一個樹形的結構,就好像元件樹一樣。在上面的例子中,我們在 ​

​SomeComponent​

​​ 的​

​providers​

​​ 裡面設定了

​​

​SomeService​

​ ,也就是說,在SomeComponent這個節點,以及它所有的子節點的元件上,SomeService類的執行個體是共用的,它們都共享一個執行個體。但是,在這個SomeComponent的父元件裡,它如果也想注入SomeService來使用的話,就沒有辦法從容器中獲得,除非在它的父元件中的providers中也添加了這個服務。

在我們之前教程《Angular2入門教程-2》的TodoList應用執行個體中,我們把todo應用相關的類都添加到一個子產品裡,内容如下:

import{ NgModule }from'@angular/core';
import{ CommonModule }from'@angular/common';
import{ FormsModule }from'@angular/forms';

import{ TodoListComponent }from'./list/list.component';
import{ TodoDetailComponent }from'./detail/detail.component';
import{ TodoItemComponent }from'./item/item.component';
import{ TodoService }from'./todo.service';

@NgModule({
 imports: [CommonModule, FormsModule ],
 declarations: [TodoListComponent, TodoDetailComponent, TodoItemComponent],
 providers: [TodoService]
})
exportclassTodoModule{}      

我們在這個子產品的定義中定義了 ​

​providers: [TodoService]​

​ ,這樣,這個子產品的幾個Component都可以共用這個TodoService的執行個體。

資料綁定

在JQuery或者更早的時代,當我們需要更新頁面的内容的時候,我們一般都需要自己獲得頁面的DOM,然後,設定他的值。當頁面上的内容需要更新到js端的時候,又需要設定一些事件,如onclick, onblur等,然後在響應事件裡面再從頁面獲得這個值。這不僅需要些很多代碼、浪費時間,還非常容易出錯。終于,Angular把我們從這些枯燥的工作中解放出來,提供了資料綁定的功能。

在Angular1.x的版本中,資料綁定是通過輪詢實作的。在Angular1裡,所有需要綁定的資料都會在$scope中,Angular1.x有一個輪訓機制,每隔一段時間就檢查所有綁定的變量,檢查他們現在的值跟上次檢查的值是否一緻,如果不一緻,就觸發相應的方法更新頁面的内容。這雖然給我們開發帶來了便利,但是如果有太多的變量需要監聽,就會造成很大的性能問題。

在Angular2裡面,綁定的資料的監聽是通過zone.js實作。通俗來講,zone給所有有可能更新資料的方法加了一個更新檔,就像AOP,或者說代理模式。每當這些更新資料的方法被調用的時候,就會觸發另一個方法,告訴Angular有資料修改,Angular再去判斷變量是否修改,如果有修改,就更新DOM。

而且,Angular2的資料更新檢測是在每個元件上有一個檢測器。這樣,就算應用中有再多綁定的變量,當有一個資料修改的事件以後,也隻是對應的那個元件的檢測器被觸發,來檢查它以及它所有的子元件的資料修改。

這兩方面結合,就使得Angular2應用的性能能夠有很大的提升。

說了那麼多原理,我們再來看看Angular2是資料綁定的幾種方式,結合下面的代碼看看每種方式的用途。這個模闆中包括一個輸入框用于建立,包含一個清單顯示,以及一個子元件:

<headerclass="header">
<inputclass="new-hero"placeholder="輸入名字"[(ngModel)]="newHero.name"(keyup.enter)="addHero()">
</header>
<ul>
<li>{{hero.name}}</li>
<hero-detail[hero]="selectedHero"></hero-detail>
<li(click)="selectHero(hero)"></li>
</ul>      

我們看看這個模闆裡面包含的4種資料綁定的方式:

  • 插值表達式

    這種方式是将元件中的資料hero.name顯示到頁面上那個

  • 标簽裡。
  • [user] 屬性綁定

    這種方式用于将目前元件中的變量傳遞到子元件,也就是從list元件中,對于每一個hero,将它傳遞到子元件HeroDetailComponent中。在子元件中,就需要這樣來擷取:

@Input() todo: Todo;      
  • (click),(keyup.enter)

    這就是事件綁定,将頁面上的DOM事件綁定到元件中的某個方法上。也就是當使用者點選(click),或者敲Enter鍵後彈起(keyup.enter)時,調用元件中的某個方法。

  • [(ngModel)]

    這種是雙向的資料綁定,上面3個都是單向的。雙向綁定就是使用者在頁面上修改這個輸入框的值時,這個值就會直接回報到元件中,同樣,如果在元件中通過某種方式修改了這個值,頁面上的輸入框中,也會顯示最新的值。

對于上面的 ​

​[]​

​​ 和 ​

​()​

​​ 兩種類型的綁定,可以了解成’輸入’和’輸出’。 ​

​[]​

​​ 相當于一個元件的輸入,一般這個輸入從它的父元件獲得;​

​()​

​​ 相當于元件的輸出。上面的例子是用事件綁定,将資料”輸出”到元件裡。實際上,我們還可以用一種

​​

​EventEmitter​

​ 把資料”輸出”到父元件。

下面的圖直覺的描述了上面4種資料綁定方式的作用方式:(圖檔來自官網,其中’[property] = “value”‘這個表述的似乎不太正确???):

Angular2 架構淺析

它們之間的關系

為了描述這幾個功能之間的關系,先看看下面這張圖(圖檔摘自官網):

Angular2 架構淺析

這張圖就比較清楚的說明了元件、服務、模闆、Directive(指令)、資料綁定和依賴注入的互相關系。結合這張圖和上面說的開發一個Angular2的應用的基本過程,這樣就更容易了解了。

下面,我們來看一下Angular應用的工作流程。

  1. 我們定義一個根子產品和一個根元件,然後在main.ts裡面用這個根子產品啟動應用。
  2. 根元件裡面的一個html标簽如果比對了某一個元件的’selector’,這個元件就會被加載在這個标簽裡面。這樣,整個應用就是一個元件樹,
  3. 我們定義的Component資訊,也就是類和Metadata(中繼資料),Angular會根據這個元件定義将元件中定義的模闆顯示到selector對應的标簽上,将樣式應用到模闆頁面上。元件和模闆之間又通過資料綁定聯系起來,元件中的變量通過資料綁定展示到模闆上,模闆又通過事件綁定,對應到元件裡的方法上。Directive的工作原理也跟上面類似。
  4. 最後,我們定義的service被Angular的Injector儲存在一個樹形結構的容器裡,在某個元件中當我們需要使用這個service時,就可以在構造函數中直接獲得這個service的執行個體,而不用手動建立。這樣,多個元件,或者子產品都可以共用一個service的執行個體。是以,service除了提供業務方法,也能提供共享資料、資料傳輸等功能。