AngularJS フレームワークでデータ モデリングが処理される方法の分析_AngularJS

WBOY
リリース: 2016-05-16 15:12:01
オリジナル
1179 人が閲覧しました

AngularJS には、すぐに利用できるデータ モデリング ソリューションが付属していないことは承知しています。代わりに、かなり抽象的な方法で、JSON データをコントローラーのモデルとして使用してみましょう。しかし、時間が経ち、プロジェクトが成長するにつれて、このモデリング方法はもはやプロジェクトのニーズを満たしていないことに気づきました。この記事では、AngularJS アプリケーションでデータ モデリングを処理する方法を紹介します。

コントローラーのモデルを定義します

簡単な例から始めましょう。本のページを表示したい。コントローラーは次のとおりです:

ブックコントローラー

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}
    ]
  };
}]);
ログイン後にコピー

このコントローラーは、後でテンプレートで使用できる書籍モデルを作成します。

本を表示するためのテンプレート

<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:
を使用する必要があります。 $http

を使用した BookController
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

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;
    });
  };
}]);
ログイン後にコピー

そしてテンプレートでは次のようになります:

完全な書籍を表示するためのテンプレート

<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() &#63; 'Yes' : 'No' "></span>
   
  <button ng-click="deleteBook()">Delete</button>
   
  <button ng-click="updateBook()">Update</button>
</div>
ログイン後にコピー

コントローラー間でモデルを共有する
本の構造とメソッドが 1 つのコントローラーにのみ関連している場合は、現在の作業を処理できます。しかし、アプリケーションが成長するにつれて、他のコントローラーもブックを処理する必要が出てきます。これらのコントローラーは、多くの場合、書籍の取得、更新、削除、または画像の URL を取得して有効かどうかを確認する必要があります。したがって、これらのブックの動作をコントローラー間で共有する必要があります。この目的を達成するには、本の動作を返すファクトリを使用する必要があります。ファクトリを記述する前に、これらのブック ヘルパー メソッドを使用してオブジェクトを返すファクトリを作成することを述べておきたいと思いますが、私はプロトタイプを使用して Book クラスを構築することを好みます:

の方が正しいと思います。

予約モデルサービス

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 サービス内にカプセル化されます。さて、この素晴らしい Book サービスを BookController で使用します。

Book モデルを使用する BookController

app.controller('BookController', ['$scope', 'Book', function($scope, Book) {
  $scope.book = new Book();
  $scope.book.load(1);
}]);
ログイン後にコピー

ご覧のとおり、コントローラーは非常にシンプルになります。 Book インスタンスを作成し、それをスコープに割り当て、バックグラウンドから読み込みます。ブックが正常に読み込まれると、そのプロパティが変更され、テンプレートが更新されます。ブック機能を使用したい他のコントローラーは、単純に Book サービスを挿入できることに注意してください。さらに、テンプレートがブックを使用する方法も変更する必要があります。

ブックインスタンスを使用するテンプレート

<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() &#63; '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来包含这个指令,所以我添加了一个给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)
   }
  }
}]);
ログイン後にコピー

ソース:php.cn
このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート