我們知道,AngularJS並沒有自帶立等可用的資料建模方案。而是以相當抽象的方式,讓我們在controller中使用JSON資料作為模型。但是隨著時間的推移和專案的成長,我意識到這種建模的方式不再能滿足我們專案的需求。在這篇文章中我會介紹在我的AngularJS應用中處理資料建模的方式。
為Controller定義模型
讓我們從一個簡單的例子開始。我想要顯示一個書本(book)的頁面。下面是控制器(Controller):
BookController
1 2 3 4 5 6 7 8 9 10 11 12 |
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
1 2 3 4 5 6 7 |
<div ng-controller="BookController"> Id: <span ng-bind="book.id"></span> <br/> Name:<input type="text" ng-model="book.name" /> <br/> Author: <input type="text" ng-model="book.author" /> </div> |
BookController with $http
1 2 3 4 5 6 7 |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<div ng-controller="BookController"> <div ng-style="{ backgroundImage: 'url(' + getBookImageUrl(100, 100) + ')' }"></div> Id: <span ng-bind="book.id"></span> <br/> Name:<input type="text" ng-model="book.name" /> <br/> Author: <input type="text" ng-model="book.author" /> <br/> Is Available: <span ng-bind="isAvailable() ? 'Yes' : 'No' "></span> <br/> <button ng-click="deleteBook()">Delete</button> <br/> <button ng-click="updateBook()">Update</button> </div> |
如果書本的結構和方法只和一個控制器有關,那我們現在的工作已經可以應付。但是隨著應用的增長,會有其他的控制器也需要和書本打交道。那些控制器很多時候也需要獲取書本,更新它,刪除它,或者獲得它的圖片url以及看它是否有效。因此,我們需要在控制器之間共享這些書本的行為。我們需要使用一個返回書本行為的factory來實現這個目的。在動手寫一個factory之前,我想在這裡先提一下,我們建立一個factory來返回帶有這些book輔助方法的物件,但我更傾向於使用prototype來構造一個Book類,我覺得這是更正確的選擇:
Book model service
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
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
1 2 3 4 |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<div ng-controller="BookController"> <div ng-style="{ backgroundImage: 'url(' + book.getImageUrl(100, 100) + ')' }"></div> Id: <span ng-bind="book.id"></span> <br/> Name:<input type="text" ng-model="book.name" /> <br/> Author: <input type="text" ng-model="book.author" /> <br/> Is Available: <span ng-bind="book.isAvailable() ? 'Yes' : 'No' "></span> <br/> <button ng-click="book.delete()">Delete</button> <br/> <button ng-click="book.update()">Update</button> </div> |
在多個控制器中使用相同的書本模型
我們定義了一個書本模型,並且在多個控制器中使用了它。在使用了這種建模架構之後你會注意到有一個嚴重的問題。到目前為止,我們假設多個控制器對書本進行操作,但如果有兩個控制器同時處理同一本書會是什麼情況呢?
假設我們頁面的一塊區域我們所有書本的名稱,另一塊區域可以更新某一本書。對應這兩塊區域,我們有兩個不同的控制器。第一個載入書本列表,第二個載入特定的一本書。我們的使用者在第二塊區域中修改了書本的名稱並且點選“更新”按鈕。更新操作成功後,書本的名稱會被改變。但是在書本列表中,這個使用者始終看到的是修改之前的名稱!真實的情況是我們對同一本書建立了兩個不同的書本例項——一個在書本列表中使用,而另一個在修改書本時使用。當使用者修改書本名稱的時候,它實際上只修改了後一個例項中的屬性。然而書本列表中的書本例項並未得到改變。
解決這個問題的辦法是在所有的控制器中使用相同的書本例項。在這種方式下,書本列表和書本修改的頁面和控制器都持有相同的書本例項,一旦這個例項發生變化,就會被立刻反映到所有的檢視中。那麼按這種方式行動起來,我們需要建立一個booksManager服務(我們沒有大寫開頭的b字母,是因為這是一個物件而不是一個類)來管理所有的書本例項池,並且富足返回這些書本例項。如果被請求的書本例項不在例項池中,這個服務會建立它。如果已經在池中,那麼就直接返回它。請牢記,所有的載入書本的方法最終都會被定義在booksManager服務中,因為它是唯一的提供書本例項的元件。
booksManager service
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
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中建模資料的一種架構。首先,我展示了AngularJS預設的資料模型繫結,然後講了如何封裝模型的方法和操作從而可以在不同的控制其中重用它們,最後我解釋瞭如何管理模型例項從而使得所有的改變都能被反映到應用中各個相關的檢視上。
希望這篇文章能在如何實現資料建模上給你一些啟示。