在AngularJS架構中處理資料建模的方式解析_AngularJS

來源:互聯網
上載者:User

我們知道,AngularJS並沒有內建立等可用的資料建模方案。而是以相當抽象的方式,讓我們在controller中使用JSON資料作為模型。但是隨著時間的推移和項目的成長,我意識到這種建模的方式不再能滿足我們項目的需求。在這篇文章中我會介紹在我的AngularJS應用中處理資料建模的方式。

為Controller定義模型

讓我們從一個簡單的例子開始。我想要顯示一個書本(book)的頁面。下面是控制器(Controller):

BookController

app.controller('BookController', ['$scope', function($scope) {  $scope.book = {    id: 1,    name: 'Harry Potter',    author: 'J. K. Rowling',    stores: [      { id: 1, name: 'Barnes & Noble', quantity: 3},      { id: 2, name: 'Waterstones', quantity: 2},      { id: 3, name: 'Book Depository', quantity: 5}    ]  };}]);

這個控制器建立了一個書本的模型,我們可以在後面的模板中(templage)中使用它。

template for displaying a book

<div ng-controller="BookController">  Id: <span ng-bind="book.id"></span>     Name:<input type="text" ng-model="book.name" />     Author: <input type="text" ng-model="book.author" /></div>

假如我們需要從背景api擷取書本的資料,我們需要使用$http:
BookController with $http

app.controller('BookController', ['$scope', '$http', function($scope, $http) {  var bookId = 1;   $http.get('ourserver/books/' + bookId).success(function(bookData) {    $scope.book = bookData;  });}]);

注意到這裡的bookData仍然是一個JSON對象。接下來我們想要使用這些資料做一些事情。比如,更新書本資訊,刪除書本,甚至其他的一些不涉及到背景操作,比如根據請求的圖片大小產生一個書本圖片的url,或者判斷書本是否有效。這些方法都可以被定義在控制器中。

BookController with several book actions

app.controller('BookController', ['$scope', '$http', function($scope, $http) {  var bookId = 1;   $http.get('ourserver/books/' + bookId).success(function(bookData) {    $scope.book = bookData;  });   $scope.deleteBook = function() {    $http.delete('ourserver/books/' + bookId);  };   $scope.updateBook = function() {    $http.put('ourserver/books/' + bookId, $scope.book);  };   $scope.getBookImageUrl = function(width, height) {    return 'our/image/service/' + bookId + '/width/height';  };   $scope.isAvailable = function() {    if (!$scope.book.stores || $scope.book.stores.length === 0) {      return false;    }    return $scope.book.stores.some(function(store) {      return store.quantity > 0;    });  };}]);

然後在我們的模板中:

template for displaying a complete book

<div ng-controller="BookController">  <div ng-style="{ backgroundImage: 'url(' + getBookImageUrl(100, 100) + ')' }"></div>  Id: <span ng-bind="book.id"></span>     Name:<input type="text" ng-model="book.name" />     Author: <input type="text" ng-model="book.author" />     Is Available: <span ng-bind="isAvailable() ? 'Yes' : 'No' "></span>     <button ng-click="deleteBook()">Delete</button>     <button ng-click="updateBook()">Update</button></div>

在controllers之間共用Model
如果書本的結構和方法只和一個控制器有關,那我們現在的工作已經可以應付。但是隨著應用的增長,會有其他的控制器也需要和書本打交道。那些控制器很多時候也需要擷取書本,更新它,刪除它,或者獲得它的圖片url以及看它是否有效。因此,我們需要在控制器之間共用這些書本的行為。我們需要使用一個返回書本行為的factory來實現這個目的。在動手寫一個factory之前,我想在這裡先提一下,我們建立一個factory來返回帶有這些book輔助方法的對象,但我更傾向於使用prototype來構造一個Book類,我覺得這是更正確的選擇:

Book model service

app.factory('Book', ['$http', function($http) {  function Book(bookData) {    if (bookData) {      this.setData(bookData):    }    // Some other initializations related to book  };  Book.prototype = {    setData: function(bookData) {      angular.extend(this, bookData);    },    load: function(id) {      var scope = this;      $http.get('ourserver/books/' + bookId).success(function(bookData) {        scope.setData(bookData);      });    },    delete: function() {      $http.delete('ourserver/books/' + bookId);    },    update: function() {      $http.put('ourserver/books/' + bookId, this);    },    getImageUrl: function(width, height) {      return 'our/image/service/' + this.book.id + '/width/height';    },    isAvailable: function() {      if (!this.book.stores || this.book.stores.length === 0) {        return false;      }      return this.book.stores.some(function(store) {        return store.quantity > 0;      });    }  };  return Book;}]);

這種方式下,書本相關的所有行為都被封裝在Book服務內。現在,我們在BookController中來使用這個亮眼的Book服務。

BookController that uses Book model

app.controller('BookController', ['$scope', 'Book', function($scope, Book) {  $scope.book = new Book();  $scope.book.load(1);}]);

正如你看到的,控制器變得非常簡單。它建立一個Book執行個體,指派給scope,並從後台載入。當書本被載入成功時,它的屬性會被改變,模板也隨著被更新。記住其他的控制器想要使用書本功能,只要簡單地注入Book服務即可。此外,我們還要改變template使用book的方法。

template that uses book instance

<div ng-controller="BookController">  <div ng-style="{ backgroundImage: 'url(' + book.getImageUrl(100, 100) + ')' }"></div>  Id: <span ng-bind="book.id"></span>     Name:<input type="text" ng-model="book.name" />     Author: <input type="text" ng-model="book.author" />     Is Available: <span ng-bind="book.isAvailable() ? 'Yes' : 'No' "></span>     <button ng-click="book.delete()">Delete</button>     <button ng-click="book.update()">Update</button></div>

到這裡,我們知道了如何建模一個資料,把他的方法封裝到一個類中,並且在多個控制器中共用它,而不需要寫重複代碼。
在多個控制器中使用相同的書本模型

我們定義了一個書本模型,並且在多個控制器中使用了它。在使用了這種建模架構之後你會注意到有一個嚴重的問題。到目前為止,我們假設多個控制器對書本進行操作,但如果有兩個控制器同時處理同一本書會是什麼情況呢?

假設我們頁面的一塊地區我們所有書本的名稱,另一塊地區可以更新某一本書。對應這兩塊地區,我們有兩個不同的控制器。第一個載入書本列表,第二個載入特定的一本書。我們的使用者在第二塊地區中修改了書本的名稱並且點擊“更新”按鈕。更新操作成功後,書本的名稱會被改變。但是在書本列表中,這個使用者始終看到的是修改之前的名稱!真實的情況是我們對同一本書建立了兩個不同的書本執行個體——一個在書本列表中使用,而另一個在修改書本時使用。當使用者修改書本名稱的時候,它實際上只修改了後一個執行個體中的屬性。然而書本列表中的書本執行個體並未得到改變。

解決這個問題的辦法是在所有的控制器中使用相同的書本執行個體。在這種方式下,書本列表和書本修改的頁面和控制器都持有相同的書本執行個體,一旦這個執行個體發生變化,就會被立刻反映到所有的視圖中。那麼按這種方式行動起來,我們需要建立一個booksManager服務(我們沒有大寫開頭的b字母,是因為這是一個對象而不是一個類)來管理所有的書本執行個體池,並且富足返回這些書本執行個體。如果被請求的書本執行個體不在執行個體池中,這個服務會建立它。如果已經在池中,那麼就直接返回它。請牢記,所有的載入書本的方法最終都會被定義在booksManager服務中,因為它是唯一的提供書本執行個體的組件。

booksManager service

app.factory('booksManager', ['$http', '$q', 'Book', function($http, $q, Book) {  var booksManager = {    _pool: {},    _retrieveInstance: function(bookId, bookData) {      var instance = this._pool[bookId];       if (instance) {        instance.setData(bookData);      } else {        instance = new Book(bookData);        this._pool[bookId] = instance;      }       return instance;    },    _search: function(bookId) {      return this._pool[bookId];    },    _load: function(bookId, deferred) {      var scope = this;       $http.get('ourserver/books/' + bookId)        .success(function(bookData) {          var book = scope._retrieveInstance(bookData.id, bookData);          deferred.resolve(book);        })        .error(function() {          deferred.reject();        });    },    /* Public Methods */    /* Use this function in order to get a book instance by it's id */    getBook: function(bookId) {      var deferred = $q.defer();      var book = this._search(bookId);      if (book) {        deferred.resolve(book);      } else {        this._load(bookId, deferred);      }      return deferred.promise;    },    /* Use this function in order to get instances of all the books */    loadAllBooks: function() {      var deferred = $q.defer();      var scope = this;      $http.get('ourserver/books)        .success(function(booksArray) {          var books = [];          booksArray.forEach(function(bookData) {            var book = scope._retrieveInstance(bookData.id, bookData);            books.push(book);          });           deferred.resolve(books);        })        .error(function() {          deferred.reject();        });      return deferred.promise;    },    /* This function is useful when we got somehow the book data and we wish to store it or update the pool and get a book instance in return */    setBook: function(bookData) {      var scope = this;      var book = this._search(bookData.id);      if (book) {        book.setData(bookData);      } else {        book = scope._retrieveInstance(bookData);      }      return book;    },   };  return booksManager;}]);

下面是我們的EditableBookController和BooksListController兩個控制器的代碼:

EditableBookController and BooksListController that uses booksManager

app.factory('Book', ['$http', function($http) {  function Book(bookData) {    if (bookData) {      this.setData(bookData):    }    // Some other initializations related to book  };  Book.prototype = {    setData: function(bookData) {      angular.extend(this, bookData);    },    delete: function() {      $http.delete('ourserver/books/' + bookId);    },    update: function() {      $http.put('ourserver/books/' + bookId, this);    },    getImageUrl: function(width, height) {      return 'our/image/service/' + this.book.id + '/width/height';    },    isAvailable: function() {      if (!this.book.stores || this.book.stores.length === 0) {        return false;      }      return this.book.stores.some(function(store) {        return store.quantity > 0;      });    }  };  return Book;}]);

需要注意的是,模組(template)中還是保持原來使用book執行個體的方式。現在應用中只持有一個id為1的book執行個體,它發生的所有改變都會被反映到使用它的各個頁面上。

AngularJS 中的一些坑
UI的閃爍

Angular的自動資料繫結功能是亮點,然而,他的另一面是:在Angular初始化之前,頁面中可能會給使用者呈現出沒有解析的運算式。當DOM準備就緒,Angular計算並替換相應的值。這樣就會導致出現一個醜陋的閃爍效果。
上述情形就是在Angular教程中渲染範例程式碼的樣子:

<body ng-controller="PhoneListCtrl"> <ul>  <li ng-repeat="phone in phones">   {{ phone.name }}   <p>{{ phone.snippet }}</p>  </li> </ul></body>

如果你做的是SPA(Single Page Application),這個問題只會在第一次載入頁面的時候出現,幸運的是,可以很容易杜絕這種情形發生: 放棄{{ }}運算式,改用ng-bind指令

<body ng-controller="PhoneListCtrl"> <ul>  <li ng-repeat="phone in phones">   <span ng-bind="phone.name"></span>   <p ng-bind="phone.snippet">Optional: visually pleasing placeholder</p>  </li> </ul></body> 

你需要一個tag來包含這個指令,所以我添加了一個<span>給phone name.

那麼初始化的時候會發生什麼呢,這個tag裡的值會顯示(但是你可以選擇設定空值).然後,當Angular初始化並用運算式結果替換tag內部值,注意你不需要在ng-bind內部添加大括弧。更簡潔了!如果你需要符合運算式,那就用ng-bind-template吧,

如果用這個指令,為了區分字串字面量和運算式,你需要使用大括弧

另外一種方法就是完全隱藏元素,甚至可以隱藏整個應用,直到Angular就緒。

Angular為此還提供了ng-cloak指令,工作原理就是在初始化階段inject了css規則,或者你可以包含這個css 隱藏規則到你自己的stylesheet。Angular就緒後就會移除這個cloak樣式,讓我們的應用(或者元素)立刻渲染。

Angular並不依賴jQuery。事實上,Angular源碼裡包含了一個內嵌的輕量級的jquery:jqLite. 當Angular檢測到你的頁面裡有jQuery出現,他就會用這個jQuery而不再用jqLite,直接證據就是Angular裡的元素抽象層。比如,在directive中訪問你要應用到的元素。

angular.module('jqdependency', []) .directive('failswithoutjquery', function() {  return {   restrict : 'A',   link : function(scope, element, attrs) {        element.hide(4000)       }  }});

但是這個元素jqLite還是jQuery元素呢?取決於,手冊上這麼寫的:

Angular中所有的元素引用都會被jQuery或者jqLite封裝;他們永遠不是純DOM引用

所以Angular如果沒有檢測到jQuery,那麼就會使用jqLite元素,hide()方法值能用於jQuery元素,所以說這個範例程式碼只能當檢測到jQuery時才可以使用。如果你(不小心)修改了AngularJS和jQuery的出現順序,這個代碼就會失效!雖說沒事挪指令碼的順序的事情不經常發生,但是在我開始模組化代碼的時候確實給我造成了困擾。尤其是當你開始使用模組載入器(比如 RequireJS), 我的解決辦法是在配置裡顯示的聲明Angular確實依賴jQuery

另外一種方法就是你不要通過Angular元素的封裝來調用jQuery特定的方法,而是使用$(element).hide(4000)來表明自己的意圖。這樣依賴,即使修改了script載入順序也沒事。

壓縮

特別需要注意的是Angular應用壓縮問題。否則錯誤資訊比如 ‘Unknown provider:aProvider  <- a' 會讓你摸不到頭腦。跟其他很多東西一樣,這個錯誤在官方文檔裡也是無從查起的。簡而言之,Angular依賴參數名來進行依賴注入。壓縮器壓根意識不到這個這跟Angular裡普通的參數名有啥不同,儘可能的把指令碼變短是他們職責。咋辦?用“友好壓縮法”來進行方法注入。看這裡:

module.service('myservice', function($http, $q) {// This breaks when minified});to this:module.service('myservice', [ '$http', '$q', function($http, $q) {// Using the array syntax to declare dependencies works with minification<b>!</b>}]);

 

這個數組文法很好的解決了這個問題。我的建議是從現在開始照這個方法寫,如果你決定壓縮JavaScript,這個方法可以讓你少走很多彎路。好像是一個automatic rewriter機制,我也不太清楚這裡面是怎麼工作的。

最終一點建議:如果你想用數組文法複寫你的functions,在所有Angular依賴注入的地方應用之。包括directives,還有directive裡的controllers。別忘了逗號(經驗之談)

// the directive itself needs array injection syntax:module.directive('directive-with-controller', ['myservice', function(myservice) {  return {   controller: ['$timeout', function($timeout) {    // but this controller needs array injection syntax, too!    }],   link : function(scope, element, attrs, ctrl) {    }  }}]);

注意:link function不需要數組文法,因為他並沒有真正的注入。這是被Angular直接調用的函數。Directive層級的依賴注入在link function裡也是使用的。

 

 Directive永遠不會‘完成'

在directive中,一個令人迴轉發的事就是directive已經‘完成'但你永遠不會知道。當把jQuery外掛程式整合到directive裡時,這個通知尤為重要。假設你想用ng-repeat把動態資料以jQuery datatable的形式顯示出來。當所有的資料在頁面中載入完成後,你只需要調用$(‘.mytable).dataTable()就可以了。 但是,臣妾做不到啊!

為啥呢?Angular的資料繫結是通過持續的digest迴圈實現的。基於此,Angular架構雷根本沒有一個時間是‘休息'的。 一個解決方案就是將jQuery dataTable的調用放在當前digest迴圈外,用timeout方法就可以做到。

angular.module('table',[]).directive('mytable', ['$timeout', function($timeout) {  return {   restrict : 'E',   template: '<table class="mytable">' +          '<thead><tr><th>counting</th></tr></thead>' +          '<tr ng-repeat="data in datas"><td></td></tr>' +        '</table>',   link : function(scope, element, attrs, ctrl) {     scope.datas = ["one", "two", "three"]     // Doesn't work, shows an empty table:     // $('.mytable', element).dataTable()      // But this does:     $timeout(function() {      $('.mytable', element).dataTable();     }, 0)   }  }}]);

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.