第一章 編寫第一個angular應用程式
一個簡單的reddit應用程式
在這一章,我們編寫一個能夠提交一篇文章(包含URL以及標題)並且可以對帖子進行投票的應用程式。
你可以認為這個應用程式是Reddit.com或者Product Hunt的原型。
在這個簡單的應用程式中,我們將會學習到angular2包含的一個必須的部分:
- 編寫一個自定義元件
- 從表單接受使用者輸入
- 在檢視上渲染一個物件列表
- 攔截使用者點選事件並響應它們
學完這章,你可以掌握怎麼去搭建一個基本的angular 2應用程式。
下面是我們App的截圖:
首先,使用者可以提交一個新的連結,之後可以對該文章進行新增投票或者減少投票。每一個連結都有一個分數並且我們可以找到我們感興趣的連結進行投票。
在這個專案乃至整本書中,我們使用Typescript。Typescript是ES6的超集,它增加了型別資訊。在本章,我們不會深入討論Typescript,但是如果你瞭解ES5或者ES6,你不會感覺到任何問題。我們將會在下一章深入學習Typescript,所以,如果你有任務語法方面的問題,不用擔心。
開始
Typescript
為了使用 Typescript,你需要安裝node.js,這裡有很多種方式去安裝node.js,所以請參閱Node.js的網站的詳細資訊。
:fa-info-circle:難道我必須使用Typescript嗎?在angular2中,你可以使用Typescript,但是不是必須的。但是ng2使用Typescript進行編寫,所以每一個人都必須瞭解。在這本書中,我們使用Typescript,因為他跟ng2結合起來更加簡單。意思就是說,這個不是必須的。
一旦你安裝了node.js,接下來就是安裝Typescript。保證你安裝的版本在1.7或者以上。為了安裝他,請執行下面的命令:
$ npm install -g typescript
npm是作為node.js的一部分安裝的,如果在你係統中沒有npm,請確認你安裝node.js的時候已經安裝了npm。
Windows使用者:在本書中,我們使用linux/mac系統裡面的命令列工具。在windows中,我們強烈建議你安裝Cygwin作為執行命令列的工具。
示例應用程式
既然環境已經準備好了,讓我們開始編寫第一個應用程式。開啟本書的程式碼並且解壓縮它,在你的終端裡面,使用cd進入first_app/angular2-reddit-base目錄。
$ cd first_app/angular2-reddit-base
如果你熟悉cd,它代表的是change directory(改變目錄),如果你使用的是mac,可以試一下下面的步驟:
- 開啟/Applications/Utilities/Terminal.app
- 鍵入cd
- 在finder中,拖動first_app/angular2-reddit-base目錄到你的終端;
- 鍵入Enter鍵,你就可以進入指定的目錄了。
讓我們使用下面的命令安裝所有的依賴:
$ npm install
在根目錄下面,建立一個新的index.html並且增加下面的基本結構:
<!doctype html>
<html>
<head>
<title>Angular 2 - Simple Reddit</title> </head>
<body>
</body>
</html>
你的angular2-reddit-base目錄看起來像下面這樣:
ng2本身就是一個js檔案。所以你需要新增一個script標籤到index.html,但是ng2有一些自己的依賴。
Ng2的依賴
為了使用ng2,你可以不需要理解這些依賴,但是你必須包含它們,如果你對它們沒有興趣,你可以跳過這一部分,但是記住,必須包含它們。
為了執行ng2,需要依賴下面的四個庫:
- es6-shim
- zone.js
- reflect-metadata
- SystemJS
為了包含它們,將下面的內容包含在你的head標籤裡面。
<script src="node_modules/es6-shim/es6-shim.js"></script>
<script src="node_modules/zone.js/dist/zone.js"></script>
<script src="node_modules/reflect-metadata/Reflect.js"></script>
<script src="node_modules/systemjs/dist/system.src.js"></script>
:fa-info-circle:注意,我們是直接從node_modules目錄載入這些.js檔案的。當你執行npm install的時候,該目錄就會被建立。如果沒有該目錄,確保你執行npm install是在angular2-reddit-base目錄下面執行的。
ES6 Shim
ES6 shim 提供墊片使傳統的JavaScript引擎表現得儘可能的ECMAScript 6。不是新版本的Safari、Chrome等嚴格要求,但它是舊版本的IE的要求.
Zones
在ng2的開發過程中,Angular團隊為我們帶來了一個新的庫 – zone.js。zone.js的設計靈感來源於Dart語言,它描述JavaScript執行過程的上下文,可以在非同步任務之間進行永續性傳遞,它類似於Java中的TLS(thread-local storage: 執行緒本地儲存)技術,zone.js則是將TLS引入到JavaScript語言中的實現框架。
在本文開篇提到zone.js為JavaScript提供了執行上下文,可以在非同步任務之間進行永續性傳遞。該是zone.js上場的時候了。zone.js採用猴子補丁(Monkey-patched)的暴力方式將JavaScript中的非同步任務都包裹了一層,使得這些非同步任務都將執行在zone的上下文中。每一個非同步的任務在zone.js都被當做為一個Task,並在Task的基礎上zone.js為開發者提供了執行前後的鉤子函式(hook)。這些鉤子函式包括:
- onZoneCreated:產生一個新的zone物件時的鉤子函式。
- zone.fork也會產生一個繼承至基類zone的新zone,形成一個獨立的zone上下文;
- beforeTask:zone Task執行前的鉤子函式;
- afterTask:zone Task執行完成後的鉤子函式;
- onError:zone執行Task時候的異常鉤子函式;
並且zone.js對JavaScript中的大多數非同步事件都做了包裹封裝,它們包括:
- zone.alert;
- zone.prompt;
- zone.requestAnimationFrame、
- zone.webkitRequestAnimationFrame、
- zone.mozRequestAnimationFrame;
- zone.addEventListener;
- zone.addEventListener、zone.removeEventListener;
- zone.setTimeout、zone.clearTimeout、zone.setImmediate;
- zone.setInterval、zone.clearInterval
Reflect Metadata
angular2是使用Typescript寫的,而Typescript又提供了annotation,使得可以向程式碼中新增元資料。嚴格意義上說,反射元資料包是一個讓我們可以使用註解的polyfill。
SystemJS
systemjs 是一個最小系統載入工具,用來建立外掛來處理可替代的場景載入過程,包括載入 CSS 場景和圖片,主要執行在瀏覽器和 NodeJS 中。它是 ES6 瀏覽器載入程式的的擴充套件,將應用在本地瀏覽器中。通常建立的外掛名稱是模組本身,要是沒有特意指定用途,則預設外掛名是模組的副檔名稱。
載入所有的依賴
現在我們載入了所有的依賴,我們的index.html看起來應該是下面這樣。
<!doctype html>
<html>
<head>
<title>Angular 2 - Simple Reddit</title>
<!-- Libraries -->
<script src="node_modules/es6-shim/es6-shim.js"></script>
<script src="node_modules/systemjs/dist/system.src.js"></script> <script src="node_modules/rxjs/bundles/Rx.js"></script>
<script src="node_modules/angular2/bundles/angular2.dev.js"></script>
</head>
<body>
</body>
</html>
新增CSS
我們也需要新增一寫CSS樣式去美化我們的應用,讓我們包含兩個CSS樣式。
<!doctype html>
<html>
<head>
<title>Angular 2 - Simple Reddit</title>
<!-- Libraries -->
<script src="node_modules/es6-shim/es6-shim.js"></script>
<script src="node_modules/zone.js/dist/zone.js"></script>
<script src="node_modules/reflect-metadata/Reflect.js"></script>
<script src="node_modules/systemjs/dist/system.src.js"></script>
<!-- Stylesheet -->
<link rel="stylesheet" type="text/css"
href="resources/vendor/semantic.min.css">
<link rel="stylesheet" type="text/css" href="styles.css">
</head>
<body>
</body>
</html>
我們的第一個Typescript檔案
讓我們建立我們的第一個Typescript檔案,建立一個app.ts的檔案在相同的目錄。並且新增下面的程式碼:
// your code goes here
import { bootstrap } from "@angular/platform-browser-dynamic";
import { Component } from "@angular/core";
@Component({
selector: 'hello-world',
template: `
<div>
Hello world
</div>
`
})
class HelloWorld { }
bootstrap(HelloWorld);
這個程式碼第一眼看起來可能有點怪異,但是別擔心。我們將會一步一步解釋它。
Typescript是一個具有型別的Javascript,為了在瀏覽器中使用angular,我們需要告訴Typescript編譯器我們發現了哪些型別檔案。那個reference標明瞭一些型別檔案的路徑。
import語句定義了我們程式碼中需要使用的模組,這裡我們import了兩個模組:Component和bootstrap.
我們從”@angular/core”匯入Component,那個”@angular/core”告訴我們應用程式去哪裡尋找模組。
我們從”@angular/platform-browser-dynamic”匯入bootstrap。
注意,import的格式為 import {thing} from wherever,在{thing}部分,我們叫著解構。解構是es6提供的功能,我們在下一章會講到。import特別像java中的import或者Ruby中的require.我們從特定模組中拉取這些依賴使得它在本檔案中可用。
構造一個元件
angular2最偉大的創意背後就是元件。
在我們的angular應用程式中,我們寫的HTML標籤使得我們的程式程式設計互動式的程式。但是瀏覽器中的標籤是很少的,比如:select,form,video等,如果我們希望教給瀏覽器一個新的標籤?比如我們希望一個標籤去顯示天氣或者我們希望一個標籤代表去建立一個登陸面板?
這個注意的背後就是元件,它教給瀏覽器一個新的功能,對應一個新的標籤。
:fa-info-circle:如果你有ng1的背景,那麼元件就是directive的新版本。
讓我們建立第一個元件。當我們建立完這個元件,我們可以在html中像下面這樣使用它:
<hello-world></hello-world>
那麼我們怎麼去定義一個元件呢?一個簡單的元件包含兩部分:
1.一個Component註解
2. 定義元件的類
讓我們花點時間研究下。
如果你之前使用javascript編寫過程式,那麼下面的程式碼看起來有點怪異:
@Component({
//...
})
這是什麼?如果你有java的背景,那麼你對這個東西比較熟悉,它是一個註解。
註解就是將元資料加入你的程式碼中。當我們使用@Component在一個HelloWorld類上面時,我們正在裝飾那個HelloWorld類使其成為一個元件。
我們想通過在Html中用標籤代表我們的元件怎麼弄?為了實現這個功能,我們用hello-world配置@Component中的selector。
@Component({
selector: 'hello-world'
})
如果你熟悉css/xpath/jquery選擇器,那麼你會知道有很多種方式去配置一個選擇器。
angular增加了它自己獨特的混合選擇器,我們將在後面的章節中講到。現在,我們只需要知道,這個就是定義了一個新的標籤。
selector屬性標誌了這個元件在html中怎麼使用。如果你有任何的標籤在html中,它將會被編譯為該元件類。
增加一個模板
我們通過template選項增加一個模板
@Component({
selector:'hello-world',
template:`
<div>
Hello world
</div>
`
})
注意我們在兩個反引號之間定義了我們的模板字串。這個是ES6的新特性,它允許我們使用多行字串。使用反引號編寫多行字串使得我們將模板直接放入程式碼中變得很方便。
啟動我們的應用程式
檔案中的最後一行 bootstrap(HelloWorld);將會啟動我們的應用程式,其中引數預示了我們的應用程式的主元件是HelloWorld。一旦應用程式啟動,HelloWorld元件將在index.html中的的地方被渲染。
載入應用程式
為了執行我們的應用程式,我們需要做下面兩件事
- 需要告訴Html文件去匯入我們的app檔案
- 需要使用元件
新增下面的程式碼到body標籤裡面。
<script src="resources/systemjs.config.js"></script>
<script>
System.import('app.js')
.then(null, console.error.bind(console));
</script>
我們增加了兩個script標籤去配置我們的system.js載入器。
- 我們載入resources/systemjs.config.js,這個檔案告訴System.js怎麼去載入庫或者檔案
- 匯入我們的app.js
下面這一行很重要:
System.import('app.js')
它告訴system.js,我們希望載入app.js作為我們的程式入口。這裡有一個問題,我們根本沒有app.js(我們只有app.ts,它是一個Typescript檔案)。
執行應用程式
編譯Typescript程式碼到.js
顯然,我們的應用程式是使用Typescript編寫的,我們使用一個叫著app.ts的檔案。接下來我們需要將他編譯為Javascript檔案,讓瀏覽器可以理解它。
為了完成這個任務,我們需要執行Typescript的編譯器工具,它叫著tsc:
tsc
如果沒有得到什麼錯誤資訊,它表示編譯已經成功了。
ls app.js
# app.js should exist
如果你執行沒有帶引數的tsc,它會像下面這樣做:
- 尋找當期資料夾(或者是用-p選項標誌的資料夾),從中查詢tsconfig.json
- 在這個目錄裡面編譯所有的.ts檔案
但是如果你標明瞭檔名稱,tsc不會去讀取tsconfig.json,為了更加正確的表一,你也可以標誌更多的選項。
Typescript需要型別定義檔案去知道更加確認的型別。在這本書中,我們會討論更多的型別或者型別檔案,但是現在,我們只需要知道,檔案@angular/platform-browser-dynamic被載入時因為我們在tsconfig.json中標明瞭它。
使用npm
如果你的tsc命令像上面一樣工作了,你也可以使用npm命令去編譯這些檔案,在例子中的package.json檔案裡,我們定義了一個快捷鍵。
試著去執行:
npm run tsc // compiles TypeScript code once and exits
npm run tsc:w // watches for changes and compiles on change
宿主應用程式
最後一步就是測試我們的應用程式,我們需要去執行一個本地伺服器作為我們應用程式的宿主。
如果你剛才運行了npm install,你已經安裝了一個本地伺服器,為了去執行它,只需要執行下面的命令:
npm run serve
然後開啟你的瀏覽器輸入http://localhost:8080
如果所有的事情都是正確的,那麼你會看到下面的內容:
每一次變化都編譯
在我們編寫程式碼的過程中有很多的變化。代替每次編號都重新執行一次tsc命令,我們可以新增–watch選項來提供效率。它告訴tsc,編譯ts檔案並且檢查它們的變化,當變化的時候,自動編譯這些檔案。
tsc --watch
實際上,我們通常會建立一個簡單的命令,它們能做下面兩件事情:
- 重新編譯變化的檔案
- 重新載入
npm run go
現在,你只管編寫你的程式碼,在瀏覽器中會自動反映那些變化。
增加資料到元件中
現在,我們的元件渲染了一個靜態的字元,意味著我們的元件並不有趣。
我們介紹我們元件的一個name屬性,這是一種針對不同輸入重複利用我們的元件的方式,像下面這樣修改:
// your code goes here
import {Component} from "@angular/core"
import {bootstrap} from "@angular/platform-browser-dynamic"
@Component({
selector:'hello-world',
template:`
<div>
Hello {name}
</div>
`
})
class HelloWorld {
name:string;
constructor() {
this.name = "pengchao.wang";
}
}
bootstrap(HelloWorld);
這裡有三個變化
1. name屬性
在HelloWorld類中增加了一個name屬性,注意跟ES5語法的區別,當我們輸入name:string的時候,我們希望name屬性代表的是一個字串。型別是被Typescript帶來的。
2. 一個構造器(constructor)
在HelloWorld類中,我們定義了一個constructor構造器,當我們建立一個新的類例項的時候這個函式被呼叫,在我們的構造器中,我們通過this.name使用我們的name屬性。
當我們寫:
constructor(){
this.name="pengchao.wang"
}
它的意思是當這個類建立新例項的時候,將name屬性設定成pengchao.wang
3. 模板變數
在模板中,我們增加了一個新的語法:{{name}},這個被稱為模板標籤(template-tags)。在模板標籤裡面的任何內容都會作為一個表示式展開,因為模板繫結到元件,所以name會被展開成this.name,在這裡例子中是pengchao.wang.
試試
當做了這些變化之後,重新載入頁面,會得到Hello pengchao.wang.
陣列
現在,我們可以實現Hello單個人,但是當多個人的時候怎麼做?
如果你使用過ng1,你可以使用ng-repeat指令,在ng2中,那個指令叫ngFor,這個語法有點不一樣。
讓我們像下面一樣修改我們的app.ts.
// your code goes here
import {Component} from "@angular/core"
import {bootstrap} from "@angular/platform-browser-dynamic"
@Component({
selector:'hello-world',
template:`
<ul>
<li *ngFor="let name of names">Hello {{ name }}</li>
</ul>
`
})
class HelloWorld {
names:string[];
constructor() {
this.names = ['Ari', 'Carlos', 'Felipe', 'Nate'];
}
}
bootstrap(HelloWorld);
第一個需要指出的是string[],它表示names屬性是一個string的陣列,也可以使用Array來表示。
*ngFor表示我們這個屬性是一個NgFor的指令,你可以認為NgFor就是for迴圈的包裝,我們會為每一個item建立一個Dom元素。
‘let name of names’表示迭代names,每次將其值設定到name中。
擴充套件我們的應用程式
既然知道怎麼去建立基本的元件,讓我們回過頭來看我們的Reddit.在我們開始編寫程式碼之前,仔細看看我們的應用程式,把它分解成邏輯元件。
在這個應用程式中,我們建立兩個元件:
- 整過應用程式,它包含使用者提交新文章的表單。
- 每篇新文章
應用程式元件
讓我們開始編寫應用程式頂層元件,這是一個元件,包含1.儲存文章列表,2.包含新文章的提交表單。我們會將我們的HelloWorld元件整體替換成RedditApp元件,如下:
// your code goes here
import {Component} from "@angular/core"
import {bootstrap} from "@angular/platform-browser-dynamic"
@Component({
selector:'reddit',
template:`
<form class="ui large form segment">
<h3 class="ui header">Add a Link</h3>
<div class="field">
<label for="title">Title:</label> <input name="title">
</div>
<div class="field">
<label for="link">Link:</label>
<input name="link">
</div>
</form>
`
})
/**
* RedditApp
*/
class RedditApp {
constructor() {
}
}
bootstrap(RedditApp);
我們定義了一個選擇器為reddit的RedditApp元件,在index.html中使用替換。再次載入的時候如下所示:
增加互動
現在我們有了表單,但是不能提交資料。讓我們增加一個提交按鈕來增加一些互動。
// your code goes here
import {Component} from "@angular/core"
import {bootstrap} from "@angular/platform-browser-dynamic"
@Component({
selector: 'reddit',
template: `
<form class="ui large form segment">
<h3 class="ui header">Add a Link</h3>
<div class="field">
<label for="title">Title:</label>
<input name="title" #newtitle>
</div>
<div class="field">
<label for="link">Link:</label>
<input name="link" #newlink>
</div>
<button (click)="addArticle(newtitle, newlink)"
class="ui positive right floated button">
Submit link
</button>
</form>
`
})
/**
* RedditApp
*/
class RedditApp {
constructor() {
}
addArticle(title: HTMLInputElement, link: HTMLInputElement): void {
console.log(`Adding article title: ${title.value} and link: ${link.value}`);
}
}
bootstrap(RedditApp);
這裡有四個不同
- 建立了一個用來提交的按鈕
- 建立了一個addArticle的函式,當提交按鈕被點選的時候被呼叫
- 增加了(click)屬性給按鈕,它的意思是當按鈕被點選時呼叫addArticle
- 給input增加了newtitle、newlink屬性
讓我們以相反的順序來覆蓋每一個步驟。
繫結input到值
注意第一個input標籤:
<input name="title" #newtitle>
這時一個新的語法。它的意思是告訴angular去繫結這個input標籤到變數newtitle。這種#newtitle的語法叫解決(resolve),他的意思是這個newtitle變數代表了這個input的view.newtitle是一個物件,代表了一個input的Dom元素(嚴格上來說是HtmlInputElement)。因為newtitle是一個變數,所以我們要通過newtitle.value獲取他的值。#newlink也是一樣的。
繫結行為到事件
在我們的按鈕中,我們增加了(click)屬性。它定義了當按鈕被點選的時候什麼應該發生。當click事件發生,我們就呼叫addArticle.這個函式的兩個引數:newtitle和newlink從哪裡來呢?
1.addArticle是來自於我們的元件RedditApp的函式
2. newtitle來自於resolve(#newtitle)
3. newlink來自於resolve(#newlink)
所有的放在一起就是:
<button (click)="addArticle(newtitle, newlink)" class="ui positive right floated button">
Submit link
</button>
定義一個行為邏輯
在我們的元件類中定義了一個行為邏輯:
addArticle(title: HTMLInputElement, link: HTMLInputElement): void {
console.log(`Adding article title: ${title.value} and link: ${link.value}`);
}
試試
現在,點選提交按鈕,你可以看到控制檯有輸出:
增加一個Article元件
現在我們有了一個表單提交新的文章,但是沒有任何地方顯示該文章。如果每一篇文章提交後都可以在頁面上顯示出來就好了。
讓我們建立一個新的元件去展示已經提交的文章。
我們可以在相同的檔案中插入下面的元件程式碼:
@Component({
selector: 'reddit-article',
host: {
class: 'row'
},
template: `
<div class="four wide column center aligned votes">
<div class="ui statistic">
<div class="value"> {{ votes }}
</div>
<div class="label">
Points
</div>
</div>
</div>
<div class="twelve wide column">
<a class="ui large header" href="{{ link }}"> {{ title }}
</a>
<ul class="ui big horizontal list voters">
<li class="item">
<a href (click)="voteUp()">
<i class="arrow up icon"></i> upvote
</a>
</li>
<li class="item">
<a href (click)="voteDown()">
<i class="arrow down icon"></i>
downvote
</a>
</li>
</ul>
</div>
` })
class ArticleComponent {
votes: number;
title: string;
link: string;
constructor() {
this.title = 'Angular 2';
this.link = 'http://angular.io';
this.votes = 10;
}
voteUp() {
this.votes
}
voteDown() {
this.votes
}
}
注意,這個元件有三部分:
- 描述元件屬性的註解@Component
- 描述元件檢視的template選項
- 建立一個元件類(ArticleComponent)用來儲存我們的元件邏輯
讓我們深入討論著三部分:
建立一個reddit-article元件
@Component({
selector: 'reddit-article',
host: {
class: 'row'
},
首先,我們使用@Component定義了一個新的元件,selector選項說明該元件的標籤為reddit-article。所以應該像下面這樣使用這個元件:
<reddit-article> </reddit-article>
當頁面渲染的時候這個標籤會保留下來。
我們希望每一個文字就是一行,也就是css的row。
在angular2中,一個元件的host表示這個元件關聯的元素。你會注意到,在這個元件中,我們傳遞了選項:host:{class:row},這個就是告訴angular那個host元素(reddit-article)我們希望設定他的class屬性為row。
建立一個reddit-article模板
第二,我們使用template選項定義了一個模板。
template: `
<div class="four wide column center aligned votes">
<div class="ui statistic">
<div class="value"> {{ votes }}
</div>
<div class="label">
Points
</div>
</div>
</div>
<div class="twelve wide column">
<a class="ui large header" href="{{ link }}"> {{ title }}
</a>
<ul class="ui big horizontal list voters">
<li class="item">
<a href (click)="voteUp()">
<i class="arrow up icon"></i> upvote
</a>
</li>
<li class="item">
<a href (click)="voteDown()">
<i class="arrow down icon"></i>
downvote
</a>
</li>
</ul>
</div>
`
這裡有許多標籤,具體就不解釋了。
建立一個reddit-article元件定義類ArticleComponent
class ArticleComponent {
votes: number;
title: string;
link: string;
constructor() {
this.title = 'Angular 2';
this.link = 'http://angular.io';
this.votes = 10;
}
voteUp() {
this.votes++;
}
voteDown() {
this.votes--;
}
}
使用元件
為了使用該元件去顯示資料,我們必須在某個地方放置我們的reddit-article標籤。
在這裡,我們希望RedditApp元件渲染這個新元件,因此,我們在form表單後面新增這個元件,修改如下:
<form class="ui large form segment">
<h3 class="ui header">Add a Link</h3>
<div class="field">
<label for="title">Title:</label>
<input name="title" #newtitle>
</div>
<div class="field">
<label for="link">Link:</label>
<input name="link" #newlink>
</div>
<button (click)="addArticle(newtitle, newlink)"
class="ui positive right floated button">
Submit link
</button>
</form>
<div class="ui grid posts">
<reddit-article>
</reddit-article>
</div>
如果我們開啟瀏覽器,你會發現標籤根本沒有編譯,是什麼問題呢?
當遇到這種問題的時候,我們需要開啟瀏覽器,然後開啟開發者工具,並且審查我們的元素,可以看到我們的標籤已經在我們的頁面中了,但是它沒有被編譯進標記裡面,這時為什麼呢?
這個是因為RedditApp不知道reddit-article的存在。為了告訴RedditApp,我們需要增加一個directives屬性到我們的元件中:
@Component({
selector: 'reddit',
directives:[ArticleComponent],
template: `
好,我們現在開啟我們的瀏覽器,就可以看到下面這個介面了:
如果你試著去點解voteup votedown連結,你會發現,頁面會異常的過載。
這時因為javascript會預設冒泡click事件給他的所有父元件。因為click事件會冒泡,所以父元件會去開啟一個空連線頁面,也就是重新載入當前頁面。為了解決這個問題,我們可以使用返回false的形式去阻止事件的冒泡。像下面這樣:
voteUp() :boolean{
this.votes++;
return false;
}
voteDown() :boolean{
this.votes--;
return false;
}
再次點選,你會發現票數增加或者減少了,但是頁面沒有重新整理。
渲染多行
現在,在頁面中只有一個文章,不能渲染更多的文章,除非我們複製貼上更多的標籤。即使我們這樣做了,但是所有的文章內容都一樣也不是很有趣。
建立一個Article類
寫angular的一個好的方式就是將資料與元件分離,所以我們建立一個Article類去代表一篇文章,增加下面的程式碼到ArticleComponent元件的前面:
class Article {
title: string;
link: string;
votes: number;
constructor(title: string, link: string, votes?: number) {
this.title = title;
this.link = link;
this.votes = votes || 0;
}
}
這裡,我們建立了一個代表文章的類Article。既然這是一個POJO類,不是一個元件。在MVC中,這個就是一個Model。
每篇文章都有一個title/link和票數綜合votes。當建立一個新文章時,我們需要title和link,votes預設為0.
現在,讓我們使用Article類修改ArticleComponent程式碼,使用儲存Article的例項去代替儲存每一個屬性。
class ArticleComponent {
article:Article;
constructor() {
this.article = new Article('Angular 2', 'http://angular.io', 10);
}
voteUp(): boolean {
this.article.votes++;
return false;
}
voteDown(): boolean {
this.article.votes--;
return false;
}
}
我們也需要去修改模板裡面的變數,將{{votes}}修改為{{article.votes}},其他類似,如下:
template: `
<div class="four wide column center aligned votes">
<div class="ui statistic">
<div class="value"> {{ article.votes }}
</div>
<div class="label">
Points
</div>
</div>
</div>
<div class="twelve wide column">
<a class="ui large header" href="{{ article.link }}"> {{ title }}
</a>
<ul class="ui big horizontal list voters">
<li class="item">
<a href (click)="voteUp()">
<i class="arrow up icon"></i> upvote
</a>
</li>
<li class="item">
<a href (click)="voteDown()">
<i class="arrow down icon"></i>
downvote
</a>
</li>
</ul>
</div>
`
重新載入,應該頁面沒有任何變化。
這是一個比較好的方式,但是我們在元件中直接修改了Article類的內部屬性,對於Article來說,ArticleComponent知道的太多了。應該將增加與減少直接放在Article類裡面去。
class Article {
title: string;
link: string;
votes: number;
constructor(title: string, link: string, votes?: number) {
this.title = title;
this.link = link;
this.votes = votes || 0;
}
voteUp():void{
this.votes++;
}
voteDown():void{
this.votes--;
}
}
然後,在元件中直接呼叫函式:
class ArticleComponent {
article:Article;
constructor() {
this.article = new Article('Angular 2', 'http://angular.io', 10);
}
voteUp(): boolean {
this.article.voteUp();
return false;
}
voteDown(): boolean {
this.article.voteDown();
return false;
}
}
重新整理瀏覽器,你會發現沒有任何變化,但是我們的程式碼更加清晰了。
儲存多個文章
讓我們在RedditApp中儲存一個文字列表,修改如下:
class RedditApp {
articles: Article[];
constructor() {
this.articles = [
new Article('Angular 2', 'http://angular.io', 3),
new Article('Fullstack', 'http://fullstack.io', 2),