導(dǎo)言
最近在學(xué)AngularJS的實例教程PhoneCat Tutorial App,發(fā)現(xiàn)網(wǎng)上的中文教程都比較久遠(yuǎn),與英文版對應(yīng)不上,而且缺少組件和文件重構(gòu)兩節(jié)。所以決定自己整理一個中文簡明教程。
此篇為13-14節(jié)。
0-5節(jié):AngularJS Phonecat (步驟0-步驟5)
6-7節(jié):AngularJS Phonecat (步驟6-步驟7)
8-9節(jié):AngularJS Phonecat (步驟8-步驟9)
10-12節(jié):AngularJS Phonecat (步驟10-步驟12)
13 REST與定制服務(wù)
在這一步,我們會改變程序獲取數(shù)據(jù)的方法:
我們會自定義一個代表RESTful客戶端的服務(wù)。通過這個客戶端,我們可以更便捷地請求服務(wù)器數(shù)據(jù),不需要處理底層的$httpAPI,HTTP方法以及URL。
REST在英語原文中未多做介紹,筆者在網(wǎng)上搜索了相關(guān)資料,推薦以下內(nèi)容:
深入淺出REST
RESTful API 設(shè)計指南
依賴
RESTful功能由Angular的ngResource模塊提供,該模塊獨(dú)立于Angular框架的核心模塊,需要單獨(dú)安裝和引入。
前面我們使用Bower安裝客戶端依賴,這一步就更新bower.json配置以安裝新的依賴:
bower.json:
{
"name": "angular-phonecat",
"description": "A starter project for AngularJS",
"version": "0.0.0",
"homepage": "https://github.com/angular/angular-phonecat",
"license": "MIT",
"private": true,
"dependencies": {
"angular": "1.5.x",
"angular-mocks": "1.5.x",
"angular-resource": "1.5.x", //增加ngResource模塊
"angular-route": "1.5.x",
"bootstrap": "3.3.x"
}
}
更新了bower.json,我們就可以用命令行安裝新模塊:
npm install
注意:如果你是在全局環(huán)境中安裝bower,你可以使用bower install進(jìn)行安裝。而這個項目我們已經(jīng)預(yù)配使用npm install來運(yùn)行bower install。
服務(wù)
我們創(chuàng)建了用于獲取服務(wù)器上手機(jī)數(shù)據(jù)的服務(wù)。我們會把該服務(wù)放到ngResource模塊中,并將該模塊放入核心模塊的依賴列表中。
app/core/phone/phone.module.js (核心模塊):
angular.module('core.phone', ['ngResource']);
app/core/phone/phone.service.js (獲取手機(jī)數(shù)據(jù)的服務(wù)):
angular.
module('core.phone').
factory('Phone', ['$resource',
function($resource) {
return $resource('phones/:phoneId.json', {}, {
query: {
method: 'GET',
params: {phoneId: 'phones'},
isArray: true
}
});
}
]);
我們使用模塊API的factory()函數(shù)注冊了一個自定義服務(wù)。并使用'Phone'來代表這個服務(wù),調(diào)用factory()函數(shù)。factory()函數(shù)類似于一個控制器的構(gòu)造函數(shù),通過函數(shù)參數(shù)可以聲明依賴注入。Phone服務(wù)聲明了對$resource服務(wù)功能的依賴。
$resource服務(wù)只需要幾行代碼就能創(chuàng)建一個RESTful客戶端。這個客戶端可以替代低層級的$http服務(wù)。
app/core/core.module.js:
angular.module('core', ['core.phone']);
我們需要增添core.phone模塊作為核心模塊的依賴。
模板
我們在app/core/phone/phone.service.js中定制resource服務(wù),所以需要在布局模板中引入這個文件和關(guān)聯(lián)文件.module.js 。另外,我們也要加載angular-resource.js,它包含了ngRsource模塊。
app/index.html:
<head>
...
<script src="bower_components/angular-resource/angular-resource.js"></script>
...
<script src="core/phone/phone.module.js"></script>
<script src="core/phone/phone.service.js"></script>
...
</head>
組件控制器
通過factory()函數(shù),我們可以用Phone服務(wù)替代低層級的$http服務(wù),這樣就簡化了組件控制器(PhoneListController 和 PhoneDetailController)。Angular的$resource服務(wù)利用RESTful資源,提供了比$http簡便的數(shù)據(jù)資源交互。現(xiàn)在,我們也更容易了解控制器的代碼是如何工作的。
app/phone-list/phone-list.module.js 手機(jī)列表模塊:
angular.module('phoneList', ['core.phone']);
app/phone-list/phone-list.component.js 手機(jī)列表組件:
angular.
module('phoneList').
component('phoneList', {
templateUrl: 'phone-list/phone-list.template.html',
controller: ['Phone',
function PhoneListController(Phone) {
this.phones = Phone.query(); //變化點(diǎn)
this.orderProp = 'age';
}
]
});
app/phone-detail/phone-detail.module.js 手機(jī)詳情模塊:
angular.module('phoneDetail', ['core.phone']);
app/phone-detail/phone-detail.component.js 手機(jī)詳情組件:
angular.
module('phoneDetail').
component('phoneDetail', {
templateUrl: 'phone-detail/phone-detail.template.html',
controller: ['$routeParams', 'Phone',
function PhoneDetailController($routeParams, Phone) {
var self = this;
self.phone = Phone.get({phoneId: $routeParams.phoneId}, function(phone) {
self.setImage(phone.images[0]);
});
self.setImage = function setImage(imageUrl) {
self.mainImageUrl = imageUrl;
};
}
]
});
注意在手機(jī)列表控制器中,我們將
$http.get('phones/phones.json').then(function(response) {
self.phones = response.data;
});
替換成:
this.phones = Phone.query();
這是一個簡單的聲明:我們需要查詢所有手機(jī)。
注意:在上面代碼中,調(diào)用Phone服務(wù)方法時,沒有傳遞回調(diào)函數(shù)。雖然這看起來就像同步獲得了返回值,但實際并非如此。同步返回的是一個對象"future",在接收到XHR響應(yīng)時,數(shù)據(jù)才會填充到"future"對象中。由于Angular的數(shù)據(jù)綁定,我們可以將該"future"對象綁定到模板上。這樣,當(dāng)數(shù)據(jù)返回時,視圖就會自動更新。
有時,依靠future對象和數(shù)據(jù)綁定不能很好地滿足我們的需求。所以,我們添加了回調(diào)函數(shù)來處理服務(wù)器響應(yīng)。比如,手機(jī)詳情組件的控制器就在回調(diào)函數(shù)中設(shè)置mainImageUrl。
測試
我們使用了ngResource模塊,所以需要更新Karma配置文件。
karma.conf.js:
files: [
'bower_components/angular/angular.js',
'bower_components/angular-resource/angular-resource.js',
...
],
我們增加一個單元測試驗證新服務(wù)是否能正確發(fā)出HTTP請求并返回預(yù)期的"future"對象/數(shù)組。
$resource服務(wù)擴(kuò)充了響應(yīng)對象:使用額外方法(如更新和刪除資源)、利用屬性(其中一些只能由Angular訪問)。如果我們使用Jasmine的.toEqual()進(jìn)行匹配,測試將會失敗, 這是因為測試值不會與響應(yīng)指完全匹配。
為了解決這個問題,我們使用自定義的等價測試用比較對象。自定義等價測試即angular.equals,它會忽略方法和帶$-前綴的屬性,比如由$resource服務(wù)注入的屬性(記住,Angular的專有API會使用$前綴)。
app/core/phone/phone.service.spec.js:
describe('Phone', function() {
...
var phonesData = [...];
// 每次測試前增加自定義等價測試
beforeEach(function() {
jasmine.addCustomEqualityTester(angular.equals);
});
// 每次測試前加載包含`Phone`服務(wù)的功能模塊
...
// 每次測試前實例化服務(wù)和`$httpBackend`
...
// 每次測試后確認(rèn)沒有其他期望或請求。
afterEach(function () {
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
});
it('should fetch the phones data from `/phones/phones.json`', function() {
var phones = Phone.query();
expect(phones).toEqual([]);
$httpBackend.flush();
expect(phones).toEqual(phonesData);
});
});
這里,我們使用$httpBackend的verifyNoOutstandingExpectation() 和verifyNoOutstandingRequest()方法驗證所有預(yù)期的請求成功發(fā)送且后續(xù)沒有其他的請求
注意:我們還修改了組件測試,在適當(dāng)?shù)臅r候使用自定義匹配。
現(xiàn)在,你會看到命令窗口輸出下面信息:
Chrome 49.0: Executed 5 of 5 SUCCESS (0.123 secs / 0.104 secs)
14 動畫
在最后一節(jié),我們要在模板代碼中增加CSS和JS實現(xiàn)動畫效果,增強(qiáng)我們的web程序。
我們使用ngAnimate模塊實現(xiàn)動畫。Angular內(nèi)置指令通過鉤子(hooks)來觸發(fā)動畫,對應(yīng)的DOM元素會執(zhí)行操作,例如利用ngRepeat插入/刪除節(jié)點(diǎn),利用ngClass添加/移除類。
依賴
動畫功能由Angular的ngAnimate模塊提供,它獨(dú)立于Angular框架核心。另外,我們會用jQuery來實現(xiàn)JavaScript動畫。
這一步我們會更新bower.json配置文件來包含新的依賴關(guān)系:
bower.json:
{
"name": "angular-phonecat",
"description": "A starter project for AngularJS",
"version": "0.0.0",
"homepage": "https://github.com/angular/angular-phonecat",
"license": "MIT",
"private": true,
"dependencies": {
"angular": "1.5.x",
"angular-animate": "1.5.x",//新的依賴,動畫模塊
"angular-mocks": "1.5.x",
"angular-resource": "1.5.x",
"angular-route": "1.5.x",
"bootstrap": "3.3.x",//bootstrap
"jquery": "2.2.x" //jquery
}
}
我們配置"angular-animate"為 "1.5.x"版本,"jquery"為 "2.2.x"版本。這里引入的jQuery并不是Angular函數(shù)庫,而是標(biāo)準(zhǔn)的jQuery庫。我們可以使用bower安裝各種第三方函數(shù)庫。
現(xiàn)在我們就讓bower下載和安裝依賴:
npm install
如何利用ngAnimate實現(xiàn)動畫
請參閱 Animations
模板
為了實現(xiàn)動畫,我們需要更新index.html,加載必要的依賴(angular-animate.js 和 jquery.js)、CSS和JS文件。ngAnimate包含了程序使用動畫的必要代碼。
app/index.html:
...
<!-- 引入CSS-->
<link rel="stylesheet" href="app.animations.css" />
...
<!-- 用于JS動畫,在angular.js之前引入-->
<script src="bower_components/jquery/dist/jquery.js"></script>
...
<!-- 增加AngularJS的動畫支持-->
<script src="bower_components/angular-animate/angular-animate.js"></script>
<!-- 定義JS動畫 -->
<script src="app.animations.js"></script>
...
重要提醒:在Angular 1.5中必須要使用jQuery 2.1以上版本,jQuery1.x版本不被正式支持的。一定要在所有AngularJS腳本之前加載jQuery,否則AngularJS可能無法檢測到j(luò)Query并利用jQuery的方法。
動畫通過CSS代碼(app.animations.css)和JS代碼(app.animations.js)創(chuàng)建。在此之前我們需要創(chuàng)建一個ngAnimate模塊。
依賴
我們需要在主模塊中增加一個ngAnimate依賴:
app/app.module.js:
angular.
module('phonecatApp', [
'ngAnimate',
...
]);
現(xiàn)在我們的程序就可以應(yīng)用動畫了,讓我們來寫些有趣的動畫。
CSS過渡動畫:Animating ngRepeat(用于手機(jī)列表頁面)
對于phoneList組件模板,我們會把CSS過渡動畫添加到ngRepeat指令中。我們需要給重復(fù)元素增加一個CSS類,這樣就可以將它與CSS動畫代碼掛鉤。
app/phone-list/phone-list.template.html:
...
<ul class="phones">
//新增class
<li ng-repeat="phone in $ctrl.phones | filter:$ctrl.query | orderBy:$ctrl.orderProp"
class="thumbnail phone-list-item">
<a href="#!/phones/{{phone.id}}" class="thumb">
<img ng-src="{{phone.imageUrl}}" alt="{{phone.name}}" />
</a>
<a href="#!/phones/{{phone.id}}">{{phone.name}}</a>
<p>{{phone.snippet}}</p>
</li>
</ul>
...
注意我們是怎么添加phone-list-item類的,這是我們要實現(xiàn)動畫所需的HTML代碼。
CSS過渡動畫代碼:
app/app.animations.css:
.phone-list-item.ng-enter,
.phone-list-item.ng-leave,
.phone-list-item.ng-move {
transition: 0.5s linear all;
}
.phone-list-item.ng-enter,
.phone-list-item.ng-move {
height: 0;
opacity: 0;
overflow: hidden;
}
.phone-list-item.ng-enter.ng-enter-active,
.phone-list-item.ng-move.ng-move-active {
height: 120px;
opacity: 1;
}
.phone-list-item.ng-leave {
opacity: 1;
overflow: hidden;
}
.phone-list-item.ng-leave.ng-leave-active {
height: 0;
opacity: 0;
padding-bottom: 0;
padding-top: 0;
}
正如你看到的,phone-list-item類通過下面幾個類來觸發(fā)動畫鉤子,實現(xiàn)顯示/隱藏元素的動畫:
- ng-enter,用于顯示一個新加入頁面的手機(jī)元素。
- ng-move,用于改變手機(jī)元素位置。
- ng-leave,用于從頁面移除一個手機(jī)元素。
手機(jī)列表根據(jù)ng-repeat指令添加或者刪除元素。比如,轉(zhuǎn)換器數(shù)據(jù)改變,則列表中項目會有添加和刪除手機(jī)項目的動畫。
需要特別注意的是,當(dāng)動畫發(fā)生時,兩套CSS類會被加入元素:
- "starting"類,代表動畫的開始樣式
- "active"類,代表動畫的結(jié)束樣式
starting類會觸發(fā)一些帶ng-前綴的事件(例如enter、move、leve),enter事件就會讓元素增加ng-enter類。
active類會觸發(fā)一些帶-active后綴的事件。
這兩套CSS類允許開發(fā)者指定動畫的實現(xiàn),是開始還是結(jié)束。
上面的例子中,在列表添加手機(jī)項目時,元素的高度會從0px變?yōu)?20px;當(dāng)刪除手機(jī)項目時,元素高度則從120px變?yōu)?px,同時有淡入淡出的效果。這些都是由CSS過渡動畫實現(xiàn)的。
盡管許多現(xiàn)代瀏覽器都能很好的支持CSS過渡和CSS動畫,但I(xiàn)E9及以前版本是不支持的。如果你想對兼容老瀏覽器,可以使用JavaScript動畫,我們會在后面講到。
CSS關(guān)鍵幀動畫:Animating ngView
這一步,我們要在ngView中增加切換動畫。
在HTML模板中添加新的CSS類到ng-view元素中。為了讓動畫更具表現(xiàn)力,我們還需將ng-view元素放入contianer元素中。
app/index.html:
<div class="view-container">
<div ng-view class="view-frame"></div>
</div>
將CSS樣式用.view-container包裹起來,這樣我們會更容易在動畫過程中改變.view-frame元素的位置。
一切準(zhǔn)備就緒,我們可以增加過渡動畫的CSS樣式了。
app/app.animations.css:
...
.view-container {
position: relative;
}
.view-frame.ng-enter,
.view-frame.ng-leave {
background: white;
left: 0;
position: absolute;
right: 0;
top: 0;
}
.view-frame.ng-enter {
animation: 1s fade-in;
z-index: 100;
}
.view-frame.ng-leave {
animation: 1s fade-out;
z-index: 99;
}
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes fade-out {
from { opacity: 1; }
to { opacity: 0; }
}
/* 舊版本瀏覽器需要在幀和動畫前面加前綴*/
代碼并不復(fù)雜,只是實現(xiàn)簡單的淡入淡出效果。比較特別的是,我們使用絕對定位將新頁面(有ng-enter類的標(biāo)識)放到舊頁面(有ng-leave類的標(biāo)識)的上方。當(dāng)舊頁面淡出時,新頁面也會淡入(而下一個頁面也放到了新頁面的上方)。
當(dāng)?shù)鰟赢嫿Y(jié)束時,該元素就重DOM樹中移除了。而淡入動畫完成時,ng-enter和ng-enter-active類都會從該元素中刪除,讓該元素以默認(rèn)CSS樣式重繪、回流(即不再有絕對定位樣式)。這個過程很流暢,讓頁面在路由改變時自然地切換,不會有跳躍感。
在ngRepeat中使用這些CSS類也是一樣的。每次頁面加載,ngView都會創(chuàng)建一個副本,下載模板并添加內(nèi)容。這就保證所有視圖都包含在一個HTML元素中,也更容易實現(xiàn)動畫控制。
JavaScript實現(xiàn)ngClass動畫(用于手機(jī)詳情頁面)
在phone-detail.template.html視圖,我們有一個不錯的縮略圖切換效果:點(diǎn)擊縮略圖,手機(jī)大圖進(jìn)行切換?,F(xiàn)在我們需要給它加一個動畫效果。
先想一下整體過程:當(dāng)用戶點(diǎn)擊縮略圖,大圖就切換最新點(diǎn)擊的圖片。而在HTML中改變圖片狀態(tài)的最好方式是使用CSS類。就像前面一樣,我們可以用一個CSS類來驅(qū)動動畫,這一次會在CSS類改變時進(jìn)行動畫。每當(dāng)選中一個手機(jī)縮略圖,.selected類就會添加到匹配的圖片上,并播放動畫。
首先,修改phone-detail.template.html中的HTML代碼。注意,我們改變了顯示大圖的方式:
app/phone-detail/phone-detail.template.html:
<div class="phone-images">
<img ng-src="{{img}}" class="phone"
ng-class="{selected: img === $ctrl.mainImageUrl}"
ng-repeat="img in $ctrl.phone.images" />
</div>
...
和縮略圖一樣,我們用一個迭代器顯示所有的概要文件列表。但是我們沒有重復(fù)關(guān)聯(lián)動畫。相反的,我們會著眼與每個元素的類,特別是selected類,因為該類決定了元素處于可見/不可見狀態(tài)。selected類由ngClass指令管理,根據(jù)特定的條件(img === $ctrl.mainImageUrl)。在這個例子中,總會有一個元素是selected的,并顯示在視圖中。
當(dāng)一個元素添加selected類,在selected-add和selected-add-active類被添加之前,AngularJS會觸發(fā)一個動畫。當(dāng)selected類移除時,selected-remove 和 selected-remove-active類也會添加到該元素中,這樣就觸發(fā)了另一個動畫。
最后,為了確保頁面第一次加載時手機(jī)圖片可以正確顯示,我們也修改了詳情頁的CSS樣式:
app/app.css:
...
.phone {
background-color: white;
display: none;
float: left;
height: 400px;
margin-bottom: 2em;
margin-right: 3em;
padding: 2em;
width: 400px;
}
.phone:first-child {
display: block;
}
.phone-images {
background-color: white;
float: left;
height: 450px;
overflow: hidden;
position: relative;
width: 450px;
}
...
你可能在想,我們是不是要創(chuàng)建另一個CSS動畫?好吧,雖然可以這么做,但我們還是看一下怎么使用.animation()方法創(chuàng)建基于JS的動畫吧。
app/app.animations.js:
angular.
module('phonecatApp').
animation('.phone', function phoneAnimationFactory() {
return {
addClass: animateIn,
removeClass: animateOut
};
function animateIn(element, className, done) { //注意:done參數(shù)
if (className !== 'selected') return;
element.
css({
display: 'block',
position: 'absolute',
top: 500,
left: 0
}).
animate({
top: 0
}, done);
return function animateInEnd(wasCanceled) {
if (wasCanceled) element.stop();
};
}
function animateOut(element, className, done) {
if (className !== 'selected') return;
element.
css({
position: 'absolute',
top: 0,
left: 0
}).
animate({
top: -500
}, done);
return function animateOutEnd(wasCanceled) {
if (wasCanceled) element.stop();
};
}
});
我們將通過CSS類選擇器(.phone)和一個動畫工廠函數(shù)(phoneAnimationFactory())為目標(biāo)元素創(chuàng)建自定義動畫。工廠函數(shù)會返回一個對象,該對象關(guān)聯(lián)著特定事件(object keys)和動畫回調(diào)函數(shù)(object values)。事件的DOM操作由ngAnimate識別并鉤?。▓?zhí)行),如addClass/removeClass/setClass、 enter/move/leave 和動畫。相關(guān)的回調(diào)函數(shù)也由ngAnimate適時調(diào)用。
更多動畫工廠函數(shù)的信息,請查看API Reference.
例子中,當(dāng)一個元素通過ngClass指令添加了selected類時,會執(zhí)行animateIn()這個回調(diào)函數(shù),其中元素作為參數(shù)傳遞進(jìn)來。animateIn()函數(shù)的最后一個參數(shù)是done函數(shù)。調(diào)用done(),會通知Angular自定義的JS動畫已經(jīng)結(jié)束。移除seleted類時,則執(zhí)行animateOut()函數(shù),原理一樣。
注意,這里我們使用jQuery實現(xiàn)動畫。在AngularJ中實現(xiàn)動畫,jQuery并不是必須的,只是用原生JS實現(xiàn)動畫其實已經(jīng)超出了本教程的范圍。如需了解jQuery.animat請查看jQuery animate。
通過事件回調(diào)函數(shù),我們操作DOM并創(chuàng)建了動畫。上面的代碼中,使用的是.css()和.element.animate()操作DOM。結(jié)果是,新圖片移動了500px,且前后兩張圖片同步移動500px,這樣就實現(xiàn)了傳送帶動畫。在animate()函數(shù)完成后,調(diào)用done函數(shù)通知Angular動畫結(jié)束。
你可能已經(jīng)注意到,每個動畫回調(diào)函數(shù)都返回了一個函數(shù),這是一個可選項。如果有設(shè)置這一項,當(dāng)動畫結(jié)束(完成/取消)時,它將被調(diào)用。該函數(shù)有一個布爾值參數(shù),用于讓開發(fā)者了解動畫是否被取消了。該函數(shù)常用于在動畫結(jié)束后執(zhí)行必要的清理工作。
結(jié)語
我們的程序已經(jīng)完成了。你可以使用 git checkout命令跳到某個步驟,隨意試驗?zāi)愕拇a。
更多的Angular概念,請參閱Developer Guide。
如果你準(zhǔn)備用AngularJS開發(fā)一個項目,建議你先從angular-seed項目開始。
