AngularJS -- AngularJS应用骨架 liujj@golden-tech liujj@golden-tech
调用Angular 为了使用Angular,所有应用都必须首先做两件事情: 加载angular.js库 使用ng-app指令告诉Angular应该管理DOM的哪一部分 liujj@golden-tech
加载脚本 从Google的内容分发网络(CDN)中加载类库 在国内的话最好使用本地的路径 <script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.0.8/angular.min.js"></script> 在国内的话最好使用本地的路径 liujj@golden-tech
使用ng-app声明Angular的边界 可以用ng-app指令告诉Angular应该管理页面中的哪一块(html标签、某个div标签等) liujj@golden-tech
Model View Controller Model View Controller mvc.01.text.html 使用对象的属性来创建数据模型,甚至只用基本数据类型来存储数据。 var messages = { someText : ‘You have started your journey.’ }; View HTML页面(DOM) <p>{{someText}}</p> <!-- 双花括号插值语法 --> Controller 编写的类或者类型 function TextController($scope) { $scope.messages = messages; } 上面这种写法是在全局作用域中创建了TextController。虽然对于示例代码来说,这没有上面问题,但是其实定义控制器的正确方式是,把它定义成模块的一部分,控制器可以为应用里相关的部分提供命名空间机制。 liujj@golden-tech mvc.01.text.html mvc.02.model.html
在模块中定义控制器 mvc.03.myApp.html <html ng-app=‘myApp’> <!-- 1.把ng-app属性设置成模块的名称myApp --> <body ng-controller='TextController'> <p>{{messages.someText}}</p> <script type="text/javascript" src="angular.js"></script> <script type="text/javascript"> // 2.调用angular对象创建一个名为myApp的模块 var myAppModule = angular.module('myApp', []); // 3.把控制器函数传递给myApp模块的controller函数 myAppModule.controller('TextController', function ($scope) { var messages = { someText : 'Your have started your journey.' }; $scope.messages = messages; } ); </script> </body> </html> liujj@golden-tech mvc.03.myApp.html
模板和数据绑定 基本的运作流程: 用户请求应用起始页。 用户的浏览器向服务器发起一次HTTP连接,然后加载index.html页面,这个页面里包含了模板。 Angular被加载到页面中,等待页面加载完成,然后查找ng-app指令,用来定义模板边界。 Angular遍历模板,查找指令和绑定关系,这将触发一系列动作:注册监听器、执行一些DOM操作、从服务器获取初始化数据。这项工作的最后结果是,应用将会启动起来,并且模板被转换成DOM视图。 连接都服务器去加载需要给用户的其他数据。 对于每一个Angular应用来说,步骤1到3都是标准化的,步骤4和5是可选的。 为了提升性能,对于应用中的第一个视图,可以把数据和HTML模板一起加载进来,从而避免发起多次请求。 使用Angular构建应用的时候,可以将应用中的模板和数据分离开来。这样就可以把模板缓存起来。在第一次请求之后,只需要把新的数据下载到浏览器中即可。 liujj@golden-tech
显示文本 使用ng-bind指令,可以在UI中的任何地方显示并刷新文本。它有两种等价形式: 使用双花括号的形式 <p>{{greeting.text}}</p> 使用基于属性的指令 <p ng-bind='greeting.text'></p> 创建双花括号语法的目的是为了阅读起来更加自然,并且需要输入的内容更少。 虽然两种形式的输出内容相同,但是使用双花括号语法的时候,在Angular使用数据替换这些花括号之前,第一个加载页面,也就是应用中的index.html,其未被渲染好的模板可能会被用户看到。 造成这种现象的原因是,浏览器需要首先加载HTML页面,渲染它,然后Angular才有机会把它解释成你期望看到的内容 好消息是,在大多数模板中你依然可以使用{{ }}。但是,对于index.html页面中的数据绑定操作,建议使用ng-bind。这样一来,在数据加载完之前你的用户就不会看到任何内容。 liujj@golden-tech ng-bind.html
表单输入 可以使用ng-model属性把元素绑定到模型属性上 这一机制对所有标准的表单元素都可以起作用(如文本框,单选框,复选框等) <input type="checkbox" ng-model="youCheckedIt"> <p>{{youCheckedIt}}</p> liujj@golden-tech
ng-change ng-change属性指定一个控制器方法,一旦修改了输入值,这个方法就会被调用 <form ng-controller='StartUpController'> Starting: <input ng-change='computeNeeded()' ng-model='funding.startingEstimate'> Recommendation: {{funding.needed}} </form> <script type="text/javascript" src="angular.js"></script> <script type="text/javascript"> function StartUpController ($scope) { $scope.funding = {startingEstimate: 0}; $scope.computeNeeded = function() { $scope.funding.needed = $scope.funding.startingEstimate * 10; }; } </script> liujj@golden-tech form.input.02.compute.needed.html
$watch()函数 使用ng-change会有一个潜在的问题,即,只有当用户在文本框中输入的时候才去计算所需的金额。 function StartUpController ($scope) { $scope.funding = {startingEstimate: 0}; computeNeeded = function() { $scope.funding.needed = $scope.funding.startingEstimate * 10; }; $scope.$watch('funding.startingEstimate', computeNeeded); } liujj@golden-tech form.input.04.compute.needed.watch.html
列表、表格以及其它迭代性元素 ng-repeat将会生成标签内部所有HTML元素的一份拷贝,包括放指令的标签。 $index返回当前引用的元素序号 $first、$middle、$last返回布尔值,表示当前元素是否是集合中的第一个元素、中间的某个元素,或者最后一个元素 <div ng-controller='AlbumController'> <table> <tr ng-repeat='track in album'> <td>{{$index + 1}}</td> <td>{{track.name}}</td> <td>{{track.duration}}</td> </tr> </table> </div> liujj@golden-tech ng-repeat.03.album.index.html
隐藏和显示 ng-show、ng-hide 这两个命令是等价的,但是运行效果正好相反 ng-show为true时将显示元素,为false时将会隐藏元素;ng-hide正好相反。 <div ng-controller='DeathrayMenuController'> <button ng-click="toggleMenu()">Toggle Menu</button> <ul ng-show='menuState'> <li ng-click="stun()">Stun</li> <li ng-click="disintegrate()">Disintegrate</li> <li ng-click="erase()">Erase from history</li> </ul> </div> function DeathrayMenuController($scope) { $scope.menuState = false; $scope.toggleMenu = function() { $scope.menuState = !$scope.menuState; }; } ng-show.html
CSS类和样式 ng-class、ng-style <div ng-controller='HeaderController'> 这两个都可以接受一个表达式: 表示CSS类名的字符串,以空格分隔 CSS类名数组 CSS类名到布尔值的映射 <div ng-controller='HeaderController'> <div ng-class='{error: isError, warning: isWarning}'>{{messageText}}</div> <button ng-click="showError()">Simulate Error</button> <button ng-click="showWarning()">Simulate Warning</button> </div> function HeaderController($scope) { $scope.isError = false; $scope.isWarning = false; ...... } liujj@golden-tech ng-class.01.error.warning.html
src和href属性 src/ng-src href/ng-href 推荐使用ng-src和ng-href。 <img src = "./images/{{favoriteCat}}"> <img ng-src = "./images/{{favoriteCat}}"> href/ng-href <a href = "./shop/category={{numberOfBalloons}}">numberOfBalloons</a> <a ng-href = "./shop/category={{numberOfBalloons}}">numberOfBalloons</a> 推荐使用ng-src和ng-href。 liujj@golden-tech
表达式 在模板中使用表达式是为了以充分的灵活性在模板、业务逻辑和数据之间建立联系,同时又能避免让业务逻辑渗透到模板中。 表达式 数学运算(+、-、/、*、%) 比较运算(==、!=、>、<、>=、<=) 布尔运算(&&、||、!) 位运算(\^、&、|) 还可以调用控制器中$scope对象上所暴露的函数,引用数组和对象符合([]、{}、.) liujj@golden-tech
<div ng-controller=‘SomeController'> <div>{{recompute() / 10}}</div> <ul ng-repeat=‘thing in things’> <li ng-class=‘{highlight: $index % 4 >= threshold($index)}’> {{otherFunction($index)}} </li> </ul> </div> 【 recompute() / 10 】虽然是合法的,但是它也是把业务逻辑放到模板中的一个典型例子,应该避免这种做法。 liujj@golden-tech
区分UI和控制器的职责 在应用中控制器有三种职责 为了让控制器保持小巧和可管理状态,建议为视图中的每一块功能区域创建一个控制器。 为应用中的模型设置初始状态 通过$scope对象把数据模型和函数暴露给视图(UI模板) 监视模型其余部分的变化,并采取相应的动作 为了让控制器保持小巧和可管理状态,建议为视图中的每一块功能区域创建一个控制器。 有两种方法可以把控制器关联到DOM节点 在模板中通过ng-controller属性来声明 通过路由把它绑定到一个动态加载的DOM模板片段上,这个模板叫做视图 如果UI中带有一个非常复杂的区域,可以创建嵌套的控制器,它们可以通过继承树结构来共享数据模型和函数,这样就可以保持代码的简单和可维护性。 <div ng-controller=“ParentController”> <div ng-controller=“ChildController”>...</div> </div> 虽然把这种方式叫做控制器嵌套,但真实的嵌套发生在$scope对象上。通过内部的原型继承机制,父控制器对象上的$scope会被传递给内部嵌套控制器的$scope。
利用$scope暴露模型数据 利用向控制器传递$scope对象的机制,可以把模型数据暴露给视图。 在你的应用中可能还有其他数据,但是只有通过$scope触及这些数据,Angular才把它当成数据模型的一部分。 对于显示的创建$scope属性(例如:$scope.count = 5),还可以通过模板自身创建数据模型。 通过表达式。 <button ng-click=‘count=5’>Set count to three</button> 在表单输入项上使用ng-model(建立双向绑定关系)。 liujj@golden-tech
使用$watch 监控数据模型的变化 当数据模型中某一部分发生变化时,$watch函数可以发出通知。 函数签名: $watch(watchFn, watchAction, deepWatch) watchFn:一个带有Angular表达式或者函数的字符串 watchAction:一个函数或者表达式,当watchFn发生变化时会被调用。 如果是函数,其函数签名为 function(newValue, oldValue, scope) deepWatch:如果设置为true,将会命令Angular去检查被监控对象的每个属性是否发生了变化。 $watch函数会返回一个函数,当不在需要接收变更通知时,可以用这个返回的函数注销监控。 var dereg = $scope.$watch(‘someModel.someProperty’,callbackOnChange()); dereg(); liujj@golden-tech
// $scope.totalCart总共被执行了几次? <div ng-repeat='item in items'> <span>{{item.title}}</span> <input ng-model='item.quantity'> <span>{{item.price | currency}}</span> <span>{{item.price * item.quantity | currency}}</span> </div> <div>Total:{{totalCart() | currency}}</div> <div>Discount:{{bill.discount | currency}}</div> <div>Subtotal:{{subtotal() | currency}}</div> function CartController ($scope) { $scope.bill = {}; $scope.items = [ {title: 'Paint pots', quantity: 8, price: 3.95}, {title: 'Polka dots', quantity: 17, price: 12.95}, {title: 'Pebbles', quantity: 5, price: 6.95} ]; $scope.totalCart = function() { var total = 0; for (var i = 0, len = $scope.items.length; i < len; i++) { total = total + $scope.items[i].price * $scope.items[i].quantity; } return total; }; $scope.subtotal = function() { return $scope.totalCart() - $scope.bill.discount; function calculateDiscount (newValue, oldValue, scope){ $scope.bill.discount = newValue > 100 ? 10 : 0; $scope.$watch($scope.totalCart, calculateDiscount); // $scope.totalCart总共被执行了几次? wacth.01.html
$watch中的性能注意事项 上面的例子存在潜在的性能问题 为什么是6次呢? 该函数被调用了6次 虽然在这个例子中性能问题并不明显,但在更复杂的应用中,运行6次就会成为一个问题 为什么是6次呢? liujj@golden-tech
一回目 二回目 三回目 wacth.01.html <div ng-repeat='item in items'> <span>{{item.title}}</span> <input ng-model='item.quantity'> <span>{{item.price | currency}}</span> <span>{{item.price * item.quantity | currency}}</span> </div> <div>Total:{{totalCart() | currency}}</div> <div>Discount:{{bill.discount | currency}}</div> <div>Subtotal:{{subtotal() | currency}}</div> function CartController ($scope) { $scope.bill = {}; $scope.items = [ {title: 'Paint pots', quantity: 8, price: 3.95}, {title: 'Polka dots', quantity: 17, price: 12.95}, {title: 'Pebbles', quantity: 5, price: 6.95} ]; $scope.totalCart = function() { var total = 0; for (var i = 0, len = $scope.items.length; i < len; i++) { total = total + $scope.items[i].price * $scope.items[i].quantity; } return total; }; $scope.subtotal = function() { return $scope.totalCart() - $scope.bill.discount; function calculateDiscount (newValue, oldValue, scope){ $scope.bill.discount = newValue > 100 ? 10 : 0; $scope.$watch($scope.totalCart, calculateDiscount); 一回目 二回目 三回目 wacth.01.html
$watch中的性能注意事项 为什么是6次呢? Angular这样做的目的 Angluar的做法 如何解决这个问题? 其中有3次很容易跟踪到 {{totalCart() | currency}} {{subtotal() | currency}} $scope.$watch($scope.totalCart, calculateDiscount); 然后Angular把以上整个过程又重复了一遍,最终就是6次了。 Angular这样做的目的 检测模型中的变更已经被完整的进行了传播,并且模型已经被设置好。 Angluar的做法 把所有被监控的属性都拷贝一份,然后把他们和当前的值进行比较,看看是否发生了变化。实际上,Angular可能会把以上程序运行10次,从而确保进行了完整的传播。如果经过10次重复之后被监控的属性还在发生变化,Angular将会报错并退出。 如何解决这个问题? liujj@golden-tech
$watch 1.监控items数组的变化 监控items数组的变化,然后重新计算$scope属性中的总价、折扣和小计值。 缺点 <div>Total:{{bill.totalCart | currency}}</div> <div>Discount:{{bill.discount | currency}}</div> <div>Subtotal:{{bill.subtotal | currency}}</div> var calculateTotals = function() { var total = 0; for (var i = 0, len = $scope.items.length; i < len; i++) { total = total + $scope.items[i].price * $scope.items[i].quantity; } $scope.bill.totalCart = total; $scope.bill.discount = total > 100 ? 10 : 0; $scope.bill.subtotal = total - $scope.bill.discount; }; $scope.$watch('items', calculateTotals, true); 缺点 既然在监控items数组,Angular就会制作一份数组的拷贝,用来进行比较操作。 对于大型的items数组来说,如果每次在Angular显示页面时只需要重新计算bill属性,那么性能会好很多。 liujj@golden-tech wacth.02.html
$watch 2.只重新计算bill属性 这种写法为什么只计算bill属性? wacth.03.html <div>Total:{{bill.totalCart | currency}}</div> <div>Discount:{{bill.discount | currency}}</div> <div>Subtotal:{{bill.subtotal | currency}}</div> function CartController ($scope) { $scope.bill = {}; $scope.items = [ {title: 'Paint pots', quantity: 8, price: 3.95}, {title: 'Polka dots', quantity: 17, price: 12.95}, {title: 'Pebbles', quantity: 5, price: 6.95} ]; $scope.$watch(function() { var total = 0; for (var i = 0, len = $scope.items.length; i < len; i++) { total = total + $scope.items[i].price * $scope.items[i].quantity; } $scope.bill.totalCart = total; $scope.bill.discount = total > 100 ? 10 : 0; $scope.bill.subtotal = total - $scope.bill.discount; }); } 这种写法为什么只计算bill属性? liujj@golden-tech wacth.03.html
$watch 监控多个东西 监控把这些属性连接起来之后的值 把他们放到一个数组或者对象中,然后给deepWatch参数传递给一个true值 $scope.$watch(‘things.a + things.b’, callMe(...)); a、b可以属于不同的对象,只要需要,这个列表可以无限长。 如果非常长,你就需要写一个函数来返回连接之后的值,而不是依赖一个表达式来完成这一逻辑。 把他们放到一个数组或者对象中,然后给deepWatch参数传递给一个true值 $scope.$watch(‘things’, callMe(...), true); Angular遍历things属性,然后当其中任何一个属性发生变化时就调用callMe(). liujj@golden-tech
使用Module(模块) 组织依赖关系 业务代码放在哪里? 前面的例子中都是放在控制器的函数中。 这种做法对于小型的应用和例子来说可以工作的很好;但在真实项目中,这种做法很快就会使代码变的无法维护。控制器将会变成一个垃圾场,我们要做的所有东西都会倒在这里。 模块提供了一种方法,可以用来组织应用中一块功能区域的依赖关系;同时还提供了一种机制,可以自动解析依赖关系(又叫做依赖注入)。 liujj@golden-tech
【例】获取商品列表 -- 没有模块 代码复用性差 功能边界划分困难 单元测试困难 function ItemsViewController($scope) { // 向服务器发起请求 ...... // 解析响应并放入Items对象 var items = [ {Title: 'Paint pots', Description: 'Pots full of paint', Price: 3.95}, {Title: 'Polka dots', Description: 'Dots with polka', Price: 2.95}, {Title: 'Pebbles', Description: 'Just little rocks', Price: 6.95} ]; // 把Items数组设置到$scope上,这样视图才能够现实它 $scope.items = items; }); 代码复用性差 功能边界划分困难 单元测试困难 liujj@golden-tech
【例】获取商品列表 -- 利用模块 控制器代码变得很简单。 var shoppingModule = angular.module('ShoppingModule', []); // 组织依赖关系 shoppingModule.factory('ItemsService', function() { var itemsService = {}; itemsService.query = function () { return [ {Title: 'Paint pots', Description: 'Pots full of paint', Price: 3.95}, {Title: 'Polka dots', Description: 'Dots with polka', Price: 2.95}, {Title: 'Pebbles', Description: 'Just little rocks', Price: 6.95} ]; }; return itemsService; }); // 自动解析依赖关系(依赖注入) function CartController ($scope, ItemsService) { $scope.items = ItemsService.query(); } 控制器代码变得很简单。 liujj@golden-tech
创建服务 provider(name, Objcet or constructor()) 一个可配置的服务,创建的逻辑比较复杂。 如果传递一个Objcet作为参数,那么这个Object必须带有一个名为$get的函数,这个函数需要返回服务的名字;否则,Angular会认为传递的是一个构造函数,调用构造函数会返回服务实例对象。 factory(name, $getFunction()) 一个不可配置的服务,创建逻辑比较复杂。 需要指定一个函数,当调用这个函数的时候,会返回服务的实例。 可以把它看成provider(name, {$get: $getFunction()}) service(name, constructor()) 一个不可配置的服务,创建逻辑比较简单。 与上面provider函数的constructor参数类似,Angular调用它可以创建服务实例。 liujj@golden-tech
模块依赖 使用Module接口来定义模块之间的依赖关系。 如果打算使用第三方包中所提供的服务或者指令,它们一般会带有自己的模块。由于你的应用会依赖这些模块,所以需要在应用模块中定义依赖关系才能引用它们。 例:引用(假象)模块SnazzyUIWidgets和SuperDataSync var appMod = angular.module(‘app’, [‘SnazzyUIWidgets’, ‘SuperDataSync’]); liujj@golden-tech
使用过滤器格式化数据 可以用过滤器来声明应该如何变换数据格式,然后再显示给用户 过滤器的语法 {{ expression | filterName : parameter1 : ...parameterN }} 例: {{ 12.9 | currency }} ⇒ $12.90 Angular内置的其他过滤器还有date、number、uppercase等 在绑定过程中,可以用管道符号把过滤器连接起来 例: {{ 12.9 | number:0 | currency }} ⇒ $13.00 liujj@golden-tech
自定义过滤器 // 首字母大写过滤器 <p>{{pageHeading | titleCase}}</p> var homeModule = angular.module('HomeModule', []); homeModule.filter('titleCase', function(){ var titleCaseFilter = function(input) { var words = input.split(' '); for (var i = 0; i < words.length; i++) { words[i] = words[i].charAt(0).toUpperCase() + words[i].slice(1); }; return words.join(' '); return titleCaseFilter; }); function HomeController ($scope) { $scope.pageHeading = 'behold the majesty of your page title'; } liujj@golden-tech
使用路由和$location 切换视图 虽然从技术上来说AJAX应用确实是单页面应用,但是很多时候,出于各种原因,我们需要为用户展示或者隐藏一些子页面视图。我们可以利用Angular的$route服务来管理这种场景。 利用路由服务来定义这样一种东西:对于浏览器所指向的URL,Angular将会加载并显示一个模板,并实例化一个控制器来为模板提供内容。 可以通过调用$routeProvider服务上的函数来创建路由,把需要创建的路由当成一个配置块传给这些参数即可。 liujj@golden-tech
var aMailServices = angular var aMailServices = angular.module('AMail', []); function emailRouteConfig ($routeProvider) { $routeProvider.when('/', { controller: ListController, templateUrl: 'list.html' }).when('/view/:id', { controller: DetailController, templateUrl: 'detail.html' }).otherwise({ redirectTo: '/' }); } aMailServices.config(emailRouteConfig); messages = [ ... ]; function ListController ($scope) { $scope.messages = messages; } function DetailController ($scope, $routeParams) { $scope.message = messages[$routeParams.id]; } 在URL、模板和控制器之间建立映射关系 '/':ListController:'list.html' 在id前面加了一个冒号,从而制定了一个参数化的URL组件 配置路由,以便Amail服务能够找到它 通过$routeParams.id取得传递的参数
与服务器交互 真正的应用需要和真实的服务器进行交互 Angular提供了一个叫做$http的服务。 提供了一个可扩展的方法列表,使得与服务器的交互更加容易 支持HTTP,JSON和CORS方式 包含了安全性支持,避免JSON格式的脆弱性和XSRF(跨站请求伪造) [ { "title": 'Paint pots', "quantity": 8, "price": 3.95. }, "title": 'Polka dots', "quantity": 17, "price": 12.95. "title": 'Pebbles', "quantity": 5, "price": 6.95. } ] function ShoppingController ($scope, $http) { $http.get('/products').success(function(data, status, headers, config) { $scope.items = data; }); } liujj@golden-tech
function ShoppingController ($scope, $http) { $http.get('/products').success(function(data, status, headers, config) { $scope.items = data; }); } JSON返回值例: [ { "title": 'Paint pots', "quantity": 8, "price": 3.95. }, "title": 'Polka dots', "quantity": 17, "price": 12.95. "title": 'Pebbles', "quantity": 5, "price": 6.95. } ] liujj@golden-tech
使用指令修改DOM 指令扩展了HTML语法,同时它也是使用自定义的元素和属性把行为和DOM转换关联到一起的方式 当内置指令无法满足需求时,可以自定义指令 与服务一样,可以通过模块对象的API来定义指令,只要调用模块实例的directive()函数即可,其中directiveFunction是一个工厂函数,用来定义指令的特性 var appModule = angular.module(‘appModule’, [...]); appModule.directive(‘directiveName’, directiveFunction); liujj@golden-tech
【例】autofocus <button ngbk-focus>I'm very focused!</button> var appModule = angular.module('app', []); appModule.directive('ngbkFocus', function(){ return { link: function (scope, element, attrs, controller) { element[0].focus(); } }; }); liujj@golden-tech
校验用户输入 Angular允许为表单中的输入元素定义一个合法的状态,并且只有当所有元素都是合法状态时才允许提交表单。 required type='email' type='number' 可以通过$valid属性获取表单的校验状态 <form name='addUserForm'> <div>First name: <input ng-model='user.first' required></div> <div>Last name: <input ng-model='user.last' required></div> <div>Email: <input type='email' ng-model='user.email' required></div> <div>Age: <input type='number' ng-model='user.age' ng-maxlength='3' ng-minlength='1'></div> <div><button>Submit</button></div> </form> liujj@golden-tech