Angular 從 0 到 1 (九):史上最簡單的 Angular 教程

接灰的電子產品發表於2017-01-09

第一節:初識Angular-CLI
第二節:登入元件的構建
第三節:建立一個待辦事項應用
第四節:進化!模組化你的應用
第五節:多使用者版本的待辦事項應用
第六節:使用第三方樣式庫及模組優化用
第七節:給元件帶來活力
Rx--隱藏在 Angular 中的利劍
Redux 你的 Angular 應用
第八節:查缺補漏大合集(上)
第九節:查缺補漏大合集(下)

第九章:查缺補漏大合集(下)

Angular2 動畫再體驗

State和Transition

我寫文章的習慣是先試驗再理論,所以我們接下來梳理下Angular2提供的動畫技能。還是從最簡單的例子開始,一個非常簡單的模版:

<div class="traffic-light"></div>複製程式碼

同樣非常簡單的樣式(其實就是畫一個小黑塊):

.traffic-light{  
  width: 100px;  
  height: 100px;  
  background-color: black;
}複製程式碼

現在的效果就是這個樣子,如圖所示,一點都不酷啊,沒關係,我們一點點來,越簡單的越容易弄懂概念。

Angular 從 0 到 1 (九):史上最簡單的 Angular 教程
一點也不酷的小黑塊

下面我們為元件新增一個animations的後設資料描述:

import { 
  Component, 
  trigger,
  state,
  style
} from '@angular/core';

@Component({
  selector: 'app-playground',
  templateUrl: './playground.component.html',
  styleUrls: ['./playground.component.css'],
  animations: [
    trigger('signal', [
      state('go', style({
        'background-color': 'green' 
      }))
    ])
  ]
})
export class PlaygroundComponent {

  constructor() { }

}複製程式碼

我們注意到animations中接受的是一個陣列,這個陣列裡面我們使用了一個叫trigger的函式,trigger接受的第一個引數是觸發器的名字,第二個引數是一個陣列。這個陣列是由一種叫state的函式和叫transition的函式組成的。

那麼什麼是state?state表示一種狀態,當這種狀態啟用時,state所附帶的樣式就會附著在應用trigger的那個控制元件上。transition又是什麼呢?tranistion描述了一系列動畫的步驟,在狀態遷移時這些動畫步驟就會執行。
我們現在的這個版本中暫時只有state而沒有transition,讓我們先來看看效果,當然在可以看到效果前我們先要把這個trigger應用到某個控制元件上。那在我們的例子裡就是模版中的那個div了。

<div
    [@signal]="'go'"
    class="traffic-light">
</div>複製程式碼

返回瀏覽器,你會發現那個小黑塊變成小綠塊了,如圖所示

Angular 從 0 到 1 (九):史上最簡單的 Angular 教程
小黑塊變成小綠塊

這說明什麼?我們的state的樣式附著在div上了。為什麼呢?因為 [@signal]="'go'" 定義了trigger的狀態是go。但這一點也不酷是嗎?是的,暫時是這樣,還是那句話,不要急。
接下來,我們再加一個狀態 stop,在stop啟用時我們要把小方塊的背景色設為紅色,那麼我們需要把animations改成下面的樣子:

animations: [
    trigger('signal', [
      state('go', style({
        'background-color': 'green' 
      })),
      state('stop', style({
          'background-color':'red'
      }))
    ])
  ]複製程式碼

同時我們需要給模板加兩個按鈕Go和Stop。現在的模版看起來是下面的樣子

<div
  [@signal]="signal"
  class="traffic-light">
</div>
<button (click)="onGo()">Go</button>
<button (click)="onStop()">Stop</button>複製程式碼

當然你看得到,我們點選按鈕時需要處理對應的點選事件。在這裡我們希望點選Go時,方塊變綠,點選Stop時方塊變紅。如果要達成這個目的,我們需要一個叫signal的成員變數,在點選的處理函式中更改相應的狀態。

export class PlaygroundComponent {

  signal: string;

  constructor() { }

  onGo(){
    this.signal = 'go';
  }
  onStop(){
    this.signal = 'stop';
  }
}複製程式碼

現在開啟瀏覽器,試驗一下,我們會發現點選Go變綠,而點選Stop變紅。但是還是沒動起來啊,是的,這是因為我們還沒加transition呢,我們只需把animations改寫一下,你分別點Go和Stop就能看到動畫效果了。為了讓效果更明顯一些,我們為兩種狀態指定一下高度。

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

@Component({
  selector: 'app-playground',
  templateUrl: './playground.component.html',
  styleUrls: ['./playground.component.css'],
  animations: [
    trigger('signal', [
      state('void', style({
        'transform':'translateY(-100%)'
      })),
      state('go', style({
        'background-color': 'green', 
        'height':'100px'
      })),
      state('stop', style({
          'background-color':'red',
          'height':'50px'
      })),
      transition('void => *', animate(5000))
    ])
  ]
})
export class PlaygroundComponent {

  signal: string;

  constructor() { }

  onGo(){
    this.signal = 'go';
  }
  onStop(){
    this.signal = 'stop';
  }
}複製程式碼

那麼 transition('* => *', animate(500)) 這句什麼意思呢?前面那個 '* => *' 是一個狀態遷移表示式,* 表示任意狀態,所以這個表示式告訴我們,只要有狀態的變化就會激發後面的動畫效果。後面的就是告訴Angular做500毫秒的動畫,這個動畫預設是從一個狀態過渡到另一個狀態。現在大家開啟瀏覽器體驗一下,分別點選Go和Stop,會發現我們的小方塊從一個正方形變成一個長方形,紅色變成綠色的過程。體驗完之後再來看這句話:動畫其實就是由若干個狀態組成,由transition定義狀態過渡的步驟。

Angular 從 0 到 1 (九):史上最簡單的 Angular 教程
有了形狀和顏色變化的動畫

那麼下面我們介紹一個void 狀態(空狀態),為什麼會有void狀態呢?其實剛剛我們也體驗了,只不過沒有定義這個void 狀態而已。我們在元件中並沒有給signal賦初始值,這就意味著一開始trigger的狀態就是void。我們往往在實現進場或離場動畫時需要這個void狀態。void狀態就是描述沒有狀態值時的狀態。

animations: [
    trigger('signal', [
      state('void', style({
        'transform':'translateY(-100%)'
      })),
      state('go', style({
        'background-color': 'green', 
        'height':'100px'
      })),
      state('stop', style({
          'background-color':'red',
          'height':'50px'
      })),
      transition('* => *', animate(500))
    ])
  ]複製程式碼

上面程式碼定義了一個void狀態,而且樣式上有一個按Y軸做的-100%的位移,其實這就是一開始讓小方塊從場景外進入場景內,這樣就是實現了一種進場動畫,大家可以在瀏覽器中試驗一下。

Angular 從 0 到 1 (九):史上最簡單的 Angular 教程
用void狀態實現的進場動畫

奇妙的animate函式

上面的我們的實驗中,你會發現transition中有個animate函式,可能你認為它就是指定一個動畫的時間的函式。它的身手可不止那麼簡單呢,我們來仔細挖掘一下。
首先呢,我們來對上面的程式碼做一個小改造,把animations陣列改成下面的樣子:

animations: [
    trigger('signal', [
      state('void', style({
        'transform':'translateY(-100%)'
      })),
      state('go', style({
        'background-color': 'green', 
        'height':'100px'
      })),
      state('stop', style({
          'background-color':'red',
          'height':'50px'
      })),
      transition('* => *', animate('.5s 1s'))
    ])
  ]複製程式碼

我們其實只對animate中的引數做了一點小改動,就是把animate(500) 改成animate('.5s 1s')。那麼.5s表示動畫過渡時間為0.5秒(其實和上面設定的500毫秒是一樣的),1s表示動畫延遲1秒後播放。現在我們開啟瀏覽器,看看效果如何吧。

當然還有更狠的大招,這個字串表示式還可以變成 '.5s 1s ease-out',後面的這個ease-out是一種緩動函式,它是可以讓動畫效果更真實的一種方式。
現實世界中物體照著一定節奏移動,並不是一開始就移動很快的,也不可能是一直勻速運動的。怎麼理解呢?當皮球往下掉時,首先是越掉越快,撞到地上後回彈,最終才又碰觸地板。而緩動函式可以使動畫的過渡效果按照這樣的真實場景抽象出的對應函式來進行繪製。ease-out只是眾多的緩動函式的其中一種,我們當然可以指定其他函式。
另外需要說明的一點是諸如ease-out只是真實函式的一個友好名稱,我們當然可以直接指定背後的函式:cubic-bezier(0, 0, 0.58, 1) 。我們下個小例子不用這個ease-out,因為效果可能不是特別明顯,我們找一個明顯的,使用 cubic-bezier(0.175, 0.885, 0.32, 1.275) 。現在我們開啟瀏覽器,你仔細觀察一下是否看到了小方塊回彈的效果

animations: [
    trigger('signal', [
      state('void', style({
        'transform':'translateY(-100%)'
      })),
      state('go', style({
        'background-color': 'green', 
        'height':'100px'
      })),
      state('stop', style({
          'background-color':'red',
          'height':'50px'
      })),
      transition('* => *', animate('.5s 1s cubic-bezier(0.175, 0.885, 0.32, 1.275)'))
    ])
  ]複製程式碼

Angular 從 0 到 1 (九):史上最簡單的 Angular 教程
加上了緩動函式的進場動畫

關於緩動函式的更多資料可以訪問 easings.net/zh-cn 在這裡可以看到各種函式的曲線和效果,以及cubic-bezier函式的各種引數

Angular 從 0 到 1 (九):史上最簡單的 Angular 教程
easing.net上列出了各種緩動函式的曲線和效果

需要注意的一點是Angular2實現動畫的機制其實是基於W3C的Web Animation標準,這個標準暫時無法支援所有的cubic-bezier函式,只有部分函式被支援。這樣的話我們如果要實現某些不被支援的函式怎麼辦呢?那就得有請我們的關鍵幀出場了。

關鍵幀

何謂關鍵幀?首先需要知道什麼是幀?百度百科給了定義:
幀——就是動畫中最小單位的單幅影像畫面,相當於電影膠片上的每一格鏡頭。在動畫軟體的時間軸上幀表現為一格或一個標記。
關鍵幀——相當於二維動畫中的原畫。指角色或者物體運動或變化中的關鍵動作所處的那一幀。關鍵幀與關鍵幀之間的動畫可以由軟體來建立,叫做過渡幀或者中間幀。
先來做一個小實驗,我們把入場動畫改造成關鍵幀形式。

import { 
  Component, 
  OnDestroy,
  trigger,
  state,
  style,
  transition,
  animate,
  keyframes
} from '@angular/core';

@Component({
  selector: 'app-playground',
  templateUrl: './playground.component.html',
  styleUrls: ['./playground.component.css'],
  animations: [
    trigger('signal', [
      state('void', style({
        'transform':'translateY(-100%)'
      })),
      state('go', style({
        'background-color': 'green', 
        'height':'100px'
      })),
      state('stop', style({
          'background-color':'red',
          'height':'50px'
      })),
      transition('void => *', animate(5000, keyframes([
        style({'transform': 'scale(0)'}),
        style({'transform': 'scale(0.1)'}),
        style({'transform': 'scale(0.5)'}),
        style({'transform': 'scale(0.9)'}),
        style({'transform': 'scale(0.95)'}),
        style({'transform': 'scale(1)'})
      ]))),
      transition('* => *', animate('.5s 1s cubic-bezier(0.175, 0.885, 0.32, 1.275)'))
    ])
  ]
})
export class PlaygroundComponent {
  // clock = Observable.interval(1000).do(_=>console.log('observable created'));
  signal: string;

  constructor() { }

  onGo(){
    this.signal = 'go';
  }
  onStop(){
    this.signal = 'stop';
  }
}複製程式碼

儲存後返回瀏覽器,你應該可以看到一個正方形由小變大的進場動畫。

Angular 從 0 到 1 (九):史上最簡單的 Angular 教程
關鍵幀實現的入場動畫

現在我們來分析一下程式碼,這個入場動畫是5秒的時間,我們給出6個關鍵幀,也就是0s,1s,2s,3s,4s和5s這幾個。對於每個關鍵幀,我們給出的樣式都是放縮,而放縮的比例逐漸加大,而且是先快後慢,也就是說我們可以模擬出緩動函式的效果。

如果我們不光做放縮,而且在style中還指定位置的話,這個動畫就會出現邊移動邊變大的效果了。把入場動畫改成下面的樣子試試看吧。

transition('void => *', animate(5000, keyframes([
        style({'transform': 'scale(0)', 'padding': '0px'}),
        style({'transform': 'scale(0.1)', 'padding': '50px'}),
        style({'transform': 'scale(0.5)', 'padding': '100px'}),
        style({'transform': 'scale(0.9)', 'padding': '120px'}),
        style({'transform': 'scale(0.95)', 'padding': '135px'}),
        style({'transform': 'scale(1)', 'padding': '140px'})
]))),複製程式碼

Angular 從 0 到 1 (九):史上最簡單的 Angular 教程
加上位移的效果

最後的結果可能還是不酷,但是這樣的話利用關鍵幀我們如果結合好CSS樣式,就會做出比較複雜的動畫了。

方便的管道--PIPE

我們一直沒有提到的一點就是管道,雖然我們的例子中沒有用到,但其實這是Angular 2中提供非常方便的一個特性。這個特性可以讓我們很快的將資料在介面上以我們想要的格式輸出出來。還是拿例子說話,比如我們在頁面上顯示一個日期,先建立一個簡單的模版:

<p> Without Pipe: Today is {{ birthday }} </p>
<p> With Pipe: Today is {{ birthday | date:"MM/dd/yy" }} </p>複製程式碼

再來建立對應的元件檔案:

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

@Component({
  selector: 'app-playground',
  templateUrl: './playground.component.html',
  styleUrls: ['./playground.component.css']
})
export class PlaygroundComponent {
  birthday = new Date();
  constructor() { }

}複製程式碼

Angular 從 0 到 1 (九):史上最簡單的 Angular 教程
無管道和有管道的日期輸出

上面的例子可能還沒太明顯,我們 進一步改造一下模板:

<p> Without Pipe: Today is {{ birthday }} </p>
<p> With Pipe: Today is {{ birthday | date:"MM/dd/yy" }} </p>
<p>The time is {{ birthday | date:'shortTime' }}</p>
<p>The time is {{ birthday | date:'medium' }}</p>複製程式碼

Angular 從 0 到 1 (九):史上最簡單的 Angular 教程
同一資料可以顯示成不同樣子

而且更牛的是多個Pipes可以串起來使用,比如說上圖中最下面那個日期我們希望把Dec大寫,就可以這樣使用:

<p>The time is {{ birthday | date:'medium' | uppercase }}</p>複製程式碼

Angular 從 0 到 1 (九):史上最簡單的 Angular 教程
多個Pipe連用

自定義一個Pipe

那麼自己寫一個Pipe是怎樣的體驗呢?建立一個Pipe非常簡單,我們來體會一下。首先建立一個 src/app/playground/trim-space.pipe.ts 的檔案:

import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
  name: 'trimSpace'
})
export class TrimSpacePipe implements PipeTransform {
  transform(value: any, args: any[]): any {
    return value.replace(/ /g, '');
  }
}複製程式碼

在Module檔案中宣告這個Pipe:declarations: [PlaygroundComponent, TrimSpacePipe] 以便於其他控制元件可以使用這個Pipe:

import { NgModule } from '@angular/core';
import { SharedModule } from '../shared/shared.module';
import { PlaygroundRoutingModule } from './playground-routing.module';
import { PlaygroundComponent }   from './playground.component';
import { PlaygroundService } from './playground.service';
import { TrimSpacePipe } from './trim-space.pipe';

@NgModule({
    imports: [
        SharedModule,
        PlaygroundRoutingModule
    ],
    providers:[
        PlaygroundService
    ],
    declarations: [PlaygroundComponent, TrimSpacePipe]
})
export class PlaygroundModule { }複製程式碼

然後在元件的模板檔案中使用即可 {{ birthday | date:'medium' | trimSpace}}

<p> Without Pipe: Today is {{ birthday }} </p>
<p> With Pipe: Today is {{ birthday | date:"MM/dd/yy" }} </p>
<p>The time is {{ birthday | date:'shortTime' }}</p>
<p>The time is {{ birthday | date:'medium' | trimSpace}} with trim space pipe applied</p>
<p>The time is {{ birthday | date:'medium' | uppercase }}</p>複製程式碼

開啟瀏覽器看一下效果,我們看到應用了trimSpace管道的日期的空格被移除了,如圖所示:

Angular 從 0 到 1 (九):史上最簡單的 Angular 教程
自定義一個移除空格的Pipe

內建的Pipe

Decimal Pipe

DatePipe和UpperCase Pipe我們剛剛已經見識過了,現在我們看一看內建的其他Pipe。首先是用於數字格式化的DecimalPipe。DecimalPipe的引數是以 {minIntegerDigits}.{minFractionDigits}-{maxFractionDigits} 的表示式形式體現的。其中:

  1. minIntegerDigits 是最小的整數位數,預設是1。
  2. minFractionDigits 表示最小的小數位數,預設是0。
  3. maxFractionDigits 表示最大的小數位數,預設是3。
<p>pi (no formatting): {{pi}}</p>
<p>pi (.5-5): {{pi | number:'.5-5'}}</p>
<p>pi (2.10-10): {{pi | number:'2.10-10'}}</p>
<p>pi (.3-3): {{pi | number:'.3-3'}}</p>複製程式碼

如果我們在元件中定義 pi: number = 3.1415927; 的話,上面的數字會被格式化成下圖的樣子

Angular 從 0 到 1 (九):史上最簡單的 Angular 教程
Decimal Pipe用於數字的格式化

Currency Pipe

顧名思義,這個Pipe是格式化貨幣的,這個Pipe的表示式形式是這樣的: currency[:currencyCode[:symbolDisplay[:digitInfo]]],也就是說在currency管道後用分號分隔不同的屬性設定:

<p>A in USD: {{a | currency:'USD':true}}</p>
<p>B in CNY: {{b | currency:'CNY':false:'4.2-2'}}</p>複製程式碼

上面的程式碼中 USDCNY 表面貨幣程式碼,truefalse 表明是否使用該貨幣的預設符號,後面如果再有一個表示式就是規定貨幣的位數限制。這個限制的具體規則和上面Decimal Pipe的類似,如下圖所示。

Angular 從 0 到 1 (九):史上最簡單的 Angular 教程
Currecy Pipe用於格式化貨幣

Percent Pipe

這個管道當然就是用來格式化百分數的,百分數的整數位和小數位的規則也和上面提到的Decimal Pipe和Currency Pipe一致。如果在元件中定義 myNum: number = 0.1415927; 下面的程式碼會輸出成下圖的樣子:

<p>myNum : {{myNum | percent}}</p>
<p>myNum (3.2-2) : {{myNum | percent:'3.2-2'}}</p>複製程式碼

Angular 從 0 到 1 (九):史上最簡單的 Angular 教程
Percent Pipe用來格式化百分數

Json Pipe

這個管道個人感覺更適合在除錯中使用,它可以把任何物件格式化成JSON格式輸出。如果我們在元件中定義了一個物件:

object: Object = {
  foo: 'bar', 
  baz: 'qux', 
  nested: {
    xyz: 3, 
    numbers: [1, 2, 3, 4, 5]
  }
};複製程式碼

那麼下面的模板會輸出下圖的樣子,在除錯階段,這個特性很好幫助你輸出可讀性很強的物件格式。當然如果你使用了現代化的IDE,這麼使用的意義就不是很大了:

<div>
  <p>Without JSON pipe:</p>
  <pre>{{object}}</pre>
  <p>With JSON pipe:</p>
  <pre>{{object | json}}</pre>
</div>複製程式碼

Angular 從 0 到 1 (九):史上最簡單的 Angular 教程
Json Pipe用於以Json形式格式化物件

指令——Directive

另一個我們一直沒有提到的重要概念就是指令了,但這個雖然我們沒提到,卻已經用過了。比如 *ngFor*ngIf 等,這些都叫做結構性指令,而像 *ngModel 等屬於屬性型指令。
Angular 2中的指令分成三種:結構型(Structural)指令和屬性型(Attribute)指令,還有一種是什麼呢?就是Component,元件本身就是一個帶模板的指令。
結構型指令可以通過新增、刪除DOM元素來更改DOM樹的佈局,比如我們前面使用 *ngFor在todo-list的模板中新增了多個todo-item。而屬性型指令可以改變一個DOM元素的外觀或行為,比如我們利用 *ngModel 進行雙向繫結,改變了該元件的預設行為(我們在元件中改變某個變數值,這種改變會直接反應到元件上,這並不是元件自身定義的行為,而是我們通過 *ngModel 來改變的)。
Angular 2中給出的內建結構型指令如下表所示:

名稱 用法 說明
ngIf <div*ngIf="canShow"> 基於canShow表示式的值移除或重新建立部分DOM樹。
ngFor <li *ngFor="let todo of todos"> 把li元素及其內容轉化成一個模板,並用它來為列表中的每個條目初始化檢視。
ngSwitch, ngSwitchCase, ngSwitchDefault <div [ngSwitch]="someCondition"></div> 基於someCondition的當前值,從內嵌模板中選取一個,有條件的切換div的內容。

自定義一個指令也很簡單,我們動手做一個。這個指令非常簡單就是使任何控制元件加上這個指令後,其點選動作都會在console中輸出 “I am clicked”。由於我們要監視其宿主的click事件,所以我們引入了 HostListener,在onClick方法上用 @HostListen(‘click’) ,表明在檢測到宿主發生click事件時呼叫這個方法。

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

@Directive({
    selector: "[log-on-click]",
})
export class LogOnClickDirective {

    constructor() {}
    @HostListener('click')
    onClick() { console.log('I am clicked!'); }
}複製程式碼

在模板中簡單寫一句就可以看效果了

<button log-on-click>Click Me</button>

Angular 從 0 到 1 (九):史上最簡單的 Angular 教程
自定義指令使得點選按鈕會log一條訊息

程式碼: github.com/wpcfan/awes…

紙書出版了,比網上內容豐富充實了,歡迎大家訂購!
京東連結:item.m.jd.com/product/120…

Angular 從 0 到 1 (九):史上最簡單的 Angular 教程
Angular從零到一

相關文章