如果說模塊系統是Angular2的靈魂,那其組件體系就是其軀體,在模塊的支持下渲染出所有用戶直接看得見的東西,一個項目最表層的東西就是組件呈現的視圖。
而除了直接看的見的軀體之外,一個完整的“生物”還需要有感覺器官,用來感知外界與其的交互,這就是指令要做的事情。
本文將使用Angular2提供的強大的組件與指令等功能制作出一個簡單的圖片輪播控件,繼續上文打的比方的話這就像是一個“器官”,功能是呈現圖片,並感知用戶的點擊或手勢來切換圖片。
一、創建組件
結束上文打的尷尬的比方,着眼於一個待開發的ng2項目,它有一個空白的特性頁面,現在需要在上面呈現一個圖片輪播窗口。
圖片輪播是一個需要給用戶看見的東西,所以應該使用ng2的組件(Component)來實現它,並且這個功能較為通用,可以將其獨立出來方便以后再次使用到。
所以在項目中的共享模塊(SharedModule)下創建這個組件名為 slide-img.component。
使用ng2提供的組建裝飾器來將這個TypeScript模塊正式變身成ng2的組件:

1 @Component({ 2 selector: 'my-slide-img', 3 templateUrl: 'slide-img.component.html', 4 styleUrls: ['slide-img.component.css'], 5 animations: [ 6 trigger('imgMove', [ 7 /** 不顯示 */ 8 state('off', style({'display': 'none', 'z-index': '0', 'transform': 'translateX(0)'})), 9 /** 上一張圖片 */ 10 state('prev', style({'z-index': '1', 11 'transform': 'translateX(-100%)'})), 12 /** 下一張圖片 */ 13 state('next', style({'z-index': '2', 'transform': 'translateX(100%)'})), 14 /** 當前圖片 */ 15 state('on', style({'z-index': '3', 'transform': 'translateX(0)'})), 16 transition('prev=>on', [ 17 animate('0.3s ease-in') 18 ]), 19 transition('next=>on', [ 20 animate('0.3s ease-in') 21 ]), 22 transition('on=>prev', [ 23 animate('0.3s ease-in') 24 ]), 25 transition('on=>next', [ 26 animate('0.3s ease-in') 27 ]) 28 ]) 29 ] 30 }) 31 export class SlideImgComponent { }
其參數其實已經不能再明確了:
selector就是其使用時的標簽名,
templateUrl即組件關聯的界面的模板,
styleUrls即僅在此組件內生效的樣式表,
animations定義的是一套ng2動畫規則。
講講最后的這個動畫規則:
ng2的動畫其實非常簡單,步驟為1.定義觸發器名,2.定義狀態,3.定義切換樣式,4.將此觸發器應用到具體的標簽中,狀態作為觸發器的值傳入。
當ng2檢測到動畫狀態的值更改了,就會套用定義的切換樣式,用法思路還算比較明確(當然實際使用時會有一些尷尬的小問題)
二、實現組件
既然是輪播圖片組件,要做的事情首先就得是傳入輪播圖片然后顯示出來。
使用過ng1的朋友一定還記得其在定義指令(angular.directive)的時候是通過scope參數(或者link)來傳入數據的,而ng2中使用的是Input裝飾器,使用的方法如下:
@Input() public imgs: SlideImg[];
使用了此裝飾器的變量imgs將可以在運行時接收其他組件傳入的圖片列表。使用方法如下:
<my-slide-img [imgs]="imgs"></my-slide-img>
關於這里的方括號“[]”,ng2其實提供了多種方式來進行組件模板中值的傳入,其中這種變量名用方括號包起來的方法就是其中之一,代表是輸入的值,而后面會見到的圓括號來包圍的方式,是代表輸出的值。
傳入了數據后,下一步就是要如何來顯示圖片到界面上了,沒錯就是ng1中ng-for指令的升級版*ngFor,除了寫法外表面上的差別不大。
關於輪播圖片邏輯的具體實現邏輯,筆者使用的方式就是利用ng2動畫的狀態切換,設置一個當前圖片索引值current,*ngFor渲染的圖片將其索引與當前索引比較,如果是相鄰的圖片則設為'prev'狀態與'next'狀態,ng2會為其加上位置屬性為-100%或者100%,如果是當前圖片則設為'on'狀態,ng2會將其的位置屬性設為0,其余均設為'off'狀態,ng2會直接將其隱藏,實現的邏輯很簡單,考慮也不算周全,筆者就不繼續解釋獻丑了。
最終的輪播圖片組件及其模板文件代碼如下:

1 <div class="imgs"> 2 <img src="{{img.Url}}" alt="" class="img" 3 *ngFor="let img of imgs;let i=index" 4 (mySwipe)="Change($event)" 5 [@imgMove]="ImgState(i)"> 6 </div> 7 8 <div class="btn" (click)="Prev()">Prev</div> 9 <div class="btn" (click)="Next()">Next</div>

1 .imgs{ 2 position: relative;width: 100%;height: 15em; 3 overflow: hidden; 4 } 5 .img{ 6 position: absolute; 7 width: 100%; 8 height: 100%; 9 background: pink; 10 transition: 0.2s; 11 } 12 .btn{ 13 display: inline-block; 14 padding: 1em 2em;font-size: 1em;border-radius: 0.5em; 15 border: 1px solid #ddd;cursor: pointer; 16 margin: 1em;background: #eee;box-shadow: 0.1em 0.1em 0.2em #aaa; 17 } 18 .btn:active{ 19 background: #eee; 20 box-shadow: none; 21 }

1 import { Component, OnInit, Input, 2 animate, 3 style, 4 transition, 5 trigger, 6 state, 7 HostListener 8 } from '@angular/core'; 9 import { SlideImg } from './slide-img.interface'; 10 11 @Component({ 12 selector: 'my-slide-img', 13 templateUrl: 'slide-img.component.html', 14 styleUrls: ['slide-img.component.css'], 15 animations: [ 16 trigger('imgMove', [ 17 /** 不顯示 */ 18 state('off', style({'display': 'none', 'z-index': '0', 'transform': 'translateX(0)'})), 19 /** 上一張圖片 */ 20 state('prev', style({'z-index': '1', 21 'transform': 'translateX(-100%)'})), 22 /** 下一張圖片 */ 23 state('next', style({'z-index': '2', 'transform': 'translateX(100%)'})), 24 /** 當前圖片 */ 25 state('on', style({'z-index': '3', 'transform': 'translateX(0)'})), 26 transition('prev=>on', [ 27 animate('0.3s ease-in') 28 ]), 29 transition('next=>on', [ 30 animate('0.3s ease-in') 31 ]), 32 transition('on=>prev', [ 33 animate('0.3s ease-in') 34 ]), 35 transition('on=>next', [ 36 animate('0.3s ease-in') 37 ]) 38 ]) 39 ] 40 }) 41 export class SlideImgComponent { 42 @Input() public imgs: SlideImg[]; 43 public current; 44 constructor() { 45 this.current = 0; 46 } 47 public ImgState(index) { 48 if (this.imgs && this.imgs.length) { 49 if (this.current === 0) { 50 return index === 0 ? 'on' : 51 index === 1 ? 'next' : 52 index === this.imgs.length - 1 ? 'prev' : 53 'off'; 54 } else if (this.current === this.imgs.length - 1) { 55 return index === this.imgs.length - 1 ? 'on' : 56 index === this.imgs.length - 2 ? 'prev' : 57 index === 0 ? 'next' : 58 'off'; 59 } 60 switch (index - this.current) { 61 case 0: 62 return 'on'; 63 case 1: 64 return 'next'; 65 case -1: 66 return 'prev'; 67 default: 68 return 'off'; 69 } 70 } else { 71 return 'off'; 72 } 73 } 74 public Next() { 75 this.current = (this.current + 1) % this.imgs.length; 76 } 77 public Prev() { 78 this.current = this.current - 1 < 0 ? this.imgs.length - 1 : this.current - 1; 79 } 80 81 public Change(e) { 82 if (e === 'left') { 83 this.Next(); 84 } else if (e === 'right') { 85 this.Prev(); 86 } 87 } 88 }
其中有兩個地方為講到,一個是組件代碼引入了一個slide-img.interface 模塊,這個僅僅用來規范一下輪播圖片的格式,二是在html中還有一個節點名為(mySwipe)這就是接下來要講的輸出屬性,目前知道的它的作用是,當用戶滑動圖片時,將觸發此節點配置的回調方法,所做的事情就是判斷發生了左滑事件還是右滑事件,分別觸發上一張圖或下一張圖的切換。
三、給輪播圖片控件加上手勢效果
輪播圖片在移動端很需要加上手勢滑動的效果,所以接下來要給這個輪播組件加上一個指令,用於響應用戶的滑動手勢。代碼如下:

1 import { Directive, Input, HostListener, Output, EventEmitter } from '@angular/core'; 2 3 @Directive({ selector: '[mySwipe]' }) 4 export class SwipeDirective { 5 @Output() public mySwipe = new EventEmitter<string>(); 6 7 private touchStartX; 8 private touchStartY; 9 @HostListener('touchstart', ['$event']) public onTouchStart(e) { 10 this.touchStartX = e.changedTouches[0].clientX; 11 this.touchStartY = e.changedTouches[0].clientY; 12 } 13 @HostListener('touchend', ['$event']) public onTouchEnd(e) { 14 let moveX = e.changedTouches[0].clientX - this.touchStartX; 15 let moveY = e.changedTouches[0].clientY - this.touchStartY; 16 if (Math.abs(moveY) < Math.abs(moveX)) { 17 /** 18 * Y軸移動小於X軸 判定為橫向滑動 19 */ 20 if (moveX > 50) { 21 this.mySwipe.emit('right'); 22 } else if (moveX < -50) { 23 this.mySwipe.emit('left'); 24 } 25 } else if (Math.abs(moveY) > Math.abs(moveX)) { 26 /** 27 * Y軸移動大於X軸 判定為縱向滑動 28 */ 29 if (moveY > 50) { 30 this.mySwipe.emit('down'); 31 } else if (moveY < -50) { 32 this.mySwipe.emit('up'); 33 } 34 } 35 this.touchStartX = this.touchStartY = -1; 36 } 37 }
指令的聲明甚至簡單過組建的聲明,因為指令不需要依賴於某個視圖模板,只需要有個指令名稱就足夠了。
需要關心的是指令中定義的輸出屬性:
@Output() public mySwipe = new EventEmitter<string>();
此屬性接收了上文組件中的Change($event)回調方法,在此指令中,所做的事情就是監聽組件的滑動,收到滑動事件后就觸發這個回調,監聽使用的是ng2的HostListener裝飾器,用法顯而易見了。
現在運行起項目來看看效果吧(比較懶就不截動圖了):
總結以及題外話:
本文主要使用了ng2幾個比較基本的功能——輸入屬性(Input裝飾器)、輸出屬性(Outut裝飾器)、HostListener裝飾器、幾個系統指令(ngFor)、ng2動畫實現了一個比較基本的圖片輪播控件。
使用好ng2的組件以及指令能完成很多的事情,其需要學習的東西絕不僅限與本文提到的,包括其底層的渲染,也很值得去研究。
最后提一個尷尬的問題點:
其實最初寫這個輪播圖片的時候想過要加上拖動的,也就是圖片會隨手勢的滑動實時更新位置。
但后來發現ng2的動畫有個尷尬的地方,那就是一定會從初始狀態按照定義好的轉換效果變化到目標狀態。實時滑動需要我在滑動過程中就改變圖片的位置,這樣的話在滑動結束需要切換圖片時,圖片居然強行回到了初始位置然后才開始轉換動畫,一時還想不到繼續使用ng2動畫來實現這種實時滑動的完美解決辦法,實在是尷尬。