本文将從架構角度簡單介紹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的子產品可以将元件、服務、指令、方法等封裝成一個子產品,如下圖所示(圖檔摘自官網):
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiI0gTMx81dsQWZ4lmZf1GLlpXazVmcvwFciV2dsQXYtJ3bm9CX9s2RkBnVHFmb1clWvB3MaVnRtp1XlBXe0xCMy81dvRWYoNHLwEzX5xCMx8FesU2cfdGLwMzX0xiRGZkRGZ0Xy9GbvNGLpZTY1EmMZVDUSFTU4VFRR9Fd4VGdsYTMfVmepNHLrJXYtJXZ0F2dvwVZnFWbp1zczV2YvJHctM3cv1Ce-cmbw5CM3UDN2QWYiZDZwIDM3UWZxYzX0QzMycTM0IzLcFDMyIDMy8CXn9Gbi9CXzV2Zh1WavwVbvNmLvR3YxUjLyM3Lc9CX6MHc0RHaiojIsJye.png)
比如我們開發一個系統,裡面包含一個”我的消息”的功能,這個功能包含一些元件,如消息清單、詳細詳情、回複、新消息和好友清單等。除了這些元件,我們又需要相應的服務來跟伺服器互動來提供資料。我們可能也需要一些環境變量等。我們可以把這些元件、服務等都封裝在一個方法裡面,像這樣:
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》中我們在設計系統的時候,就是從設計元件開始,得到的是一個元件樹:
每個元件(除了根元件)都會有一個父元件,每個元件定義中的
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”‘這個表述的似乎不太正确???):
它們之間的關系
為了描述這幾個功能之間的關系,先看看下面這張圖(圖檔摘自官網):
這張圖就比較清楚的說明了元件、服務、模闆、Directive(指令)、資料綁定和依賴注入的互相關系。結合這張圖和上面說的開發一個Angular2的應用的基本過程,這樣就更容易了解了。
下面,我們來看一下Angular應用的工作流程。
- 我們定義一個根子產品和一個根元件,然後在main.ts裡面用這個根子產品啟動應用。
- 根元件裡面的一個html标簽如果比對了某一個元件的’selector’,這個元件就會被加載在這個标簽裡面。這樣,整個應用就是一個元件樹,
- 我們定義的Component資訊,也就是類和Metadata(中繼資料),Angular會根據這個元件定義将元件中定義的模闆顯示到selector對應的标簽上,将樣式應用到模闆頁面上。元件和模闆之間又通過資料綁定聯系起來,元件中的變量通過資料綁定展示到模闆上,模闆又通過事件綁定,對應到元件裡的方法上。Directive的工作原理也跟上面類似。
- 最後,我們定義的service被Angular的Injector儲存在一個樹形結構的容器裡,在某個元件中當我們需要使用這個service時,就可以在構造函數中直接獲得這個service的執行個體,而不用手動建立。這樣,多個元件,或者子產品都可以共用一個service的執行個體。是以,service除了提供業務方法,也能提供共享資料、資料傳輸等功能。