AngularJS 프레임워크_AngularJS에서 데이터 모델링이 처리되는 방식 분석

WBOY
풀어 주다: 2016-05-16 15:12:01
원래의
1163명이 탐색했습니다.

우리는 AngularJS가 즉시 사용 가능한 데이터 모델링 솔루션을 제공하지 않는다는 것을 알고 있습니다. 대신 다소 추상적인 방식으로 JSON 데이터를 컨트롤러의 모델로 사용하겠습니다. 그러나 시간이 지나고 프로젝트가 커짐에 따라 이러한 모델링 방식이 더 이상 우리 프로젝트의 요구 사항을 충족하지 못한다는 것을 깨달았습니다. 이 글에서는 AngularJS 애플리케이션에서 데이터 모델링을 처리하는 방법을 소개하겠습니다.

컨트롤러 모델 정의

간단한 예부터 시작해 보겠습니다. 책의 페이지를 표시하고 싶습니다. 컨트롤러는 다음과 같습니다.

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}
    ]
  };
}]);
로그인 후 복사

이 컨트롤러는 나중에 템플릿에서 사용할 수 있는 책 모델을 생성합니다.

책 전시용 템플릿

<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>
로그인 후 복사

컨트롤러 간 모델 공유
책의 구조와 방법이 하나의 컨트롤러에만 관련되어 있다면 현재 작업을 처리할 수 있습니다. 그러나 애플리케이션이 성장함에 따라 다른 컨트롤러도 책을 처리해야 합니다. 이러한 컨트롤러는 책을 가져오고, 업데이트하고, 삭제하거나, 이미지 URL을 가져와서 유효한지 확인해야 하는 경우도 많습니다. 따라서 컨트롤러 간에 이러한 책의 동작을 공유해야 합니다. 이 목적을 달성하려면 책 동작을 반환하는 팩토리를 사용해야 합니다. 팩토리를 작성하기 전에 이러한 book 도우미 메서드를 사용하여 개체를 반환하는 팩토리를 생성한다는 점을 여기서 언급하고 싶습니다. 그러나 프로토타입을 사용하여 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;
}]);
로그인 후 복사

이런 방식으로 모든 도서 관련 동작이 도서 서비스 내에 캡슐화됩니다. 이제 우리는 BookController에서 이 눈부신 Book 서비스를 사용합니다.

Book 모델을 사용하는 BookController

app.controller('BookController', ['$scope', 'Book', function($scope, Book) {
  $scope.book = new Book();
  $scope.book.load(1);
}]);
로그인 후 복사

보시다시피 컨트롤러는 매우 간단해졌습니다. 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으로 문의하세요.
인기 튜토리얼
더>
최신 다운로드
더>
웹 효과
웹사이트 소스 코드
웹사이트 자료
프론트엔드 템플릿
회사 소개 부인 성명 Sitemap
PHP 중국어 웹사이트:공공복지 온라인 PHP 교육,PHP 학습자의 빠른 성장을 도와주세요!