AngularJS Phonecat (步驟13-步驟14)--完結篇

導言


最近在學AngularJS的實例教程PhoneCat Tutorial App,發現網上的中文教程都比較久遠,與英文版對應不上,而且缺少組件和文件重構兩節。所以決定自己整理一個中文簡明教程。

此篇為13-14節。

0-5節:AngularJS Phonecat (步驟0-步驟5)
6-7節:AngularJS Phonecat (步驟6-步驟7)
8-9節:AngularJS Phonecat (步驟8-步驟9)
10-12節:AngularJS Phonecat (步驟10-步驟12)

13 REST與定制服務


在這一步,我們會改變程序獲取數據的方法:
我們會自定義一個代表RESTful客戶端的服務。通過這個客戶端,我們可以更便捷地請求服務器數據,不需要處理底層的$httpAPI,HTTP方法以及URL。

REST在英語原文中未多做介紹,筆者在網上搜索了相關資料,推薦以下內容:
深入淺出REST
RESTful API 設計指南

依賴

RESTful功能由Angular的ngResource模塊提供,該模塊獨立于Angular框架的核心模塊,需要單獨安裝和引入。

前面我們使用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

注意:如果你是在全局環境中安裝bower,你可以使用bower install進行安裝。而這個項目我們已經預配使用npm install來運行bower install。

服務

我們創建了用于獲取服務器上手機數據的服務。我們會把該服務放到ngResource模塊中,并將該模塊放入核心模塊的依賴列表中。

app/core/phone/phone.module.js (核心模塊):

angular.module('core.phone', ['ngResource']);

app/core/phone/phone.service.js (獲取手機數據的服務):

angular.
  module('core.phone').
  factory('Phone', ['$resource',
    function($resource) {
      return $resource('phones/:phoneId.json', {}, {
        query: {
          method: 'GET',
          params: {phoneId: 'phones'},
          isArray: true
        }
      });
    }
  ]);

我們使用模塊API的factory()函數注冊了一個自定義服務。并使用'Phone'來代表這個服務,調用factory()函數。factory()函數類似于一個控制器的構造函數,通過函數參數可以聲明依賴注入。Phone服務聲明了對$resource服務功能的依賴。

$resource服務只需要幾行代碼就能創建一個RESTful客戶端。這個客戶端可以替代低層級的$http服務。

app/core/core.module.js:

angular.module('core', ['core.phone']);

我們需要增添core.phone模塊作為核心模塊的依賴。

模板

我們在app/core/phone/phone.service.js中定制resource服務,所以需要在布局模板中引入這個文件和關聯文件.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()函數,我們可以用Phone服務替代低層級的$http服務,這樣就簡化了組件控制器(PhoneListController 和 PhoneDetailController)。Angular的$resource服務利用RESTful資源,提供了比$http簡便的數據資源交互。現在,我們也更容易了解控制器的代碼是如何工作的。

app/phone-list/phone-list.module.js 手機列表模塊:

angular.module('phoneList', ['core.phone']);

app/phone-list/phone-list.component.js 手機列表組件:

angular.
  module('phoneList').
  component('phoneList', {
    templateUrl: 'phone-list/phone-list.template.html',
    controller: ['Phone',
      function PhoneListController(Phone) {
        this.phones = Phone.query();  //變化點
        this.orderProp = 'age';
      }
    ]
  });

app/phone-detail/phone-detail.module.js 手機詳情模塊:

angular.module('phoneDetail', ['core.phone']);

app/phone-detail/phone-detail.component.js 手機詳情組件:

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;
        };
      }
    ]
  });

注意在手機列表控制器中,我們將

$http.get('phones/phones.json').then(function(response) {
  self.phones = response.data;
});

替換成:

this.phones = Phone.query();

這是一個簡單的聲明:我們需要查詢所有手機。

注意:在上面代碼中,調用Phone服務方法時,沒有傳遞回調函數。雖然這看起來就像同步獲得了返回值,但實際并非如此。同步返回的是一個對象"future",在接收到XHR響應時,數據才會填充到"future"對象中。由于Angular的數據綁定,我們可以將該"future"對象綁定到模板上。這樣,當數據返回時,視圖就會自動更新。

有時,依靠future對象和數據綁定不能很好地滿足我們的需求。所以,我們添加了回調函數來處理服務器響應。比如,手機詳情組件的控制器就在回調函數中設置mainImageUrl。

測試

我們使用了ngResource模塊,所以需要更新Karma配置文件。

karma.conf.js:

files: [
  'bower_components/angular/angular.js',
  'bower_components/angular-resource/angular-resource.js',
  ...
],

我們增加一個單元測試驗證新服務是否能正確發出HTTP請求并返回預期的"future"對象/數組。

$resource服務擴充了響應對象:使用額外方法(如更新和刪除資源)、利用屬性(其中一些只能由Angular訪問)。如果我們使用Jasmine的.toEqual()進行匹配,測試將會失敗, 這是因為測試值不會與響應指完全匹配。

為了解決這個問題,我們使用自定義的等價測試用比較對象。自定義等價測試即angular.equals,它會忽略方法和帶$-前綴的屬性,比如由$resource服務注入的屬性(記住,Angular的專有API會使用$前綴)。

app/core/phone/phone.service.spec.js:

describe('Phone', function() {
  ...
  var phonesData = [...];

  // 每次測試前增加自定義等價測試
  beforeEach(function() {
    jasmine.addCustomEqualityTester(angular.equals);
  });

  // 每次測試前加載包含`Phone`服務的功能模塊
  ...

  // 每次測試前實例化服務和`$httpBackend`
  ...

  // 每次測試后確認沒有其他期望或請求。
  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()方法驗證所有預期的請求成功發送且后續沒有其他的請求
注意:我們還修改了組件測試,在適當的時候使用自定義匹配。

現在,你會看到命令窗口輸出下面信息:

Chrome 49.0: Executed 5 of 5 SUCCESS (0.123 secs / 0.104 secs)

14 動畫


在最后一節,我們要在模板代碼中增加CSS和JS實現動畫效果,增強我們的web程序。
我們使用ngAnimate模塊實現動畫。Angular內置指令通過鉤子(hooks)來觸發動畫,對應的DOM元素會執行操作,例如利用ngRepeat插入/刪除節點,利用ngClass添加/移除類。

依賴

動畫功能由Angular的ngAnimate模塊提供,它獨立于Angular框架核心。另外,我們會用jQuery來實現JavaScript動畫。
這一步我們會更新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-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函數庫,而是標準的jQuery庫。我們可以使用bower安裝各種第三方函數庫。
現在我們就讓bower下載和安裝依賴:

npm install

如何利用ngAnimate實現動畫

請參閱 Animations

模板

為了實現動畫,我們需要更新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可能無法檢測到jQuery并利用jQuery的方法。

動畫通過CSS代碼(app.animations.css)和JS代碼(app.animations.js)創建。在此之前我們需要創建一個ngAnimate模塊。

依賴

我們需要在主模塊中增加一個ngAnimate依賴:

app/app.module.js:

angular.
  module('phonecatApp', [
    'ngAnimate',
    ...
  ]);

現在我們的程序就可以應用動畫了,讓我們來寫些有趣的動畫。

CSS過渡動畫:Animating ngRepeat(用于手機列表頁面)

對于phoneList組件模板,我們會把CSS過渡動畫添加到ngRepeat指令中。我們需要給重復元素增加一個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類的,這是我們要實現動畫所需的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類通過下面幾個類來觸發動畫鉤子,實現顯示/隱藏元素的動畫:

  • ng-enter,用于顯示一個新加入頁面的手機元素。
  • ng-move,用于改變手機元素位置。
  • ng-leave,用于從頁面移除一個手機元素。

手機列表根據ng-repeat指令添加或者刪除元素。比如,轉換器數據改變,則列表中項目會有添加和刪除手機項目的動畫。

需要特別注意的是,當動畫發生時,兩套CSS類會被加入元素:

  • "starting"類,代表動畫的開始樣式
  • "active"類,代表動畫的結束樣式

starting類會觸發一些帶ng-前綴的事件(例如enter、move、leve),enter事件就會讓元素增加ng-enter類。

active類會觸發一些帶-active后綴的事件。

這兩套CSS類允許開發者指定動畫的實現,是開始還是結束。

上面的例子中,在列表添加手機項目時,元素的高度會從0px變為120px;當刪除手機項目時,元素高度則從120px變為0px,同時有淡入淡出的效果。這些都是由CSS過渡動畫實現的。

盡管許多現代瀏覽器都能很好的支持CSS過渡和CSS動畫,但IE9及以前版本是不支持的。如果你想對兼容老瀏覽器,可以使用JavaScript動畫,我們會在后面講到。

CSS關鍵幀動畫:Animating ngView

這一步,我們要在ngView中增加切換動畫。

在HTML模板中添加新的CSS類到ng-view元素中。為了讓動畫更具表現力,我們還需將ng-view元素放入contianer元素中。

app/index.html:

<div class="view-container">
  <div ng-view class="view-frame"></div>
</div>

將CSS樣式用.view-container包裹起來,這樣我們會更容易在動畫過程中改變.view-frame元素的位置。

一切準備就緒,我們可以增加過渡動畫的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; }
}
/* 舊版本瀏覽器需要在幀和動畫前面加前綴*/

代碼并不復雜,只是實現簡單的淡入淡出效果。比較特別的是,我們使用絕對定位將新頁面(有ng-enter類的標識)放到舊頁面(有ng-leave類的標識)的上方。當舊頁面淡出時,新頁面也會淡入(而下一個頁面也放到了新頁面的上方)。

當淡出動畫結束時,該元素就重DOM樹中移除了。而淡入動畫完成時,ng-enter和ng-enter-active類都會從該元素中刪除,讓該元素以默認CSS樣式重繪、回流(即不再有絕對定位樣式)。這個過程很流暢,讓頁面在路由改變時自然地切換,不會有跳躍感。

在ngRepeat中使用這些CSS類也是一樣的。每次頁面加載,ngView都會創建一個副本,下載模板并添加內容。這就保證所有視圖都包含在一個HTML元素中,也更容易實現動畫控制。

JavaScript實現ngClass動畫(用于手機詳情頁面)

在phone-detail.template.html視圖,我們有一個不錯的縮略圖切換效果:點擊縮略圖,手機大圖進行切換。現在我們需要給它加一個動畫效果。

先想一下整體過程:當用戶點擊縮略圖,大圖就切換最新點擊的圖片。而在HTML中改變圖片狀態的最好方式是使用CSS類。就像前面一樣,我們可以用一個CSS類來驅動動畫,這一次會在CSS類改變時進行動畫。每當選中一個手機縮略圖,.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>
...

和縮略圖一樣,我們用一個迭代器顯示所有的概要文件列表。但是我們沒有重復關聯動畫。相反的,我們會著眼與每個元素的類,特別是selected類,因為該類決定了元素處于可見/不可見狀態。selected類由ngClass指令管理,根據特定的條件(img === $ctrl.mainImageUrl)。在這個例子中,總會有一個元素是selected的,并顯示在視圖中。

當一個元素添加selected類,在selected-add和selected-add-active類被添加之前,AngularJS會觸發一個動畫。當selected類移除時,selected-remove 和 selected-remove-active類也會添加到該元素中,這樣就觸發了另一個動畫。

最后,為了確保頁面第一次加載時手機圖片可以正確顯示,我們也修改了詳情頁的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;
}

...

你可能在想,我們是不是要創建另一個CSS動畫?好吧,雖然可以這么做,但我們還是看一下怎么使用.animation()方法創建基于JS的動畫吧。

app/app.animations.js:

angular.
  module('phonecatApp').
  animation('.phone', function phoneAnimationFactory() {
    return {
      addClass: animateIn,
      removeClass: animateOut
    };

    function animateIn(element, className, done) {  //注意:done參數
      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)和一個動畫工廠函數(phoneAnimationFactory())為目標元素創建自定義動畫。工廠函數會返回一個對象,該對象關聯著特定事件(object keys)和動畫回調函數(object values)。事件的DOM操作由ngAnimate識別并鉤住(執行),如addClass/removeClass/setClass、 enter/move/leave 和動畫。相關的回調函數也由ngAnimate適時調用。

更多動畫工廠函數的信息,請查看API Reference.

例子中,當一個元素通過ngClass指令添加了selected類時,會執行animateIn()這個回調函數,其中元素作為參數傳遞進來。animateIn()函數的最后一個參數是done函數。調用done(),會通知Angular自定義的JS動畫已經結束。移除seleted類時,則執行animateOut()函數,原理一樣。

注意,這里我們使用jQuery實現動畫。在AngularJ中實現動畫,jQuery并不是必須的,只是用原生JS實現動畫其實已經超出了本教程的范圍。如需了解jQuery.animat請查看jQuery animate

通過事件回調函數,我們操作DOM并創建了動畫。上面的代碼中,使用的是.css()和.element.animate()操作DOM。結果是,新圖片移動了500px,且前后兩張圖片同步移動500px,這樣就實現了傳送帶動畫。在animate()函數完成后,調用done函數通知Angular動畫結束。

你可能已經注意到,每個動畫回調函數都返回了一個函數,這是一個可選項。如果有設置這一項,當動畫結束(完成/取消)時,它將被調用。該函數有一個布爾值參數,用于讓開發者了解動畫是否被取消了。該函數常用于在動畫結束后執行必要的清理工作。

結語


我們的程序已經完成了。你可以使用 git checkout命令跳到某個步驟,隨意試驗你的代碼。
更多的Angular概念,請參閱Developer Guide
如果你準備用AngularJS開發一個項目,建議你先從angular-seed項目開始。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 173,381評論 25 708
  • 本文從 這里 翻譯過來的。 2048這個游戲有一段時間特別火,Github上有其原始版本,游戲看起來很簡單,但是...
    江楓閱讀 1,507評論 2 7
  • 家禽生產中的理念更新與發展策略…… 養雞業經過幾十年的發展,在廣大農村經歷了從散養到養殖專業戶,再到家庭化規模養殖...
    04534cdd7064閱讀 372評論 0 0
  • 時間,仿佛是越走越快了。 在這個繁忙的城市中,每個人都走得那么快,像是決心要在一代人的時間里活千百次。夾竹桃的花開...
    云笙丨寒楓閱讀 417評論 0 0
  • 林州市永和希望小學非常注重師生寫字水平的提升。對學生,不但開設了每周兩次的“寫字提升班”,而且讓學生堅持“每日午寫...
    甲午之印閱讀 70評論 0 0