AngularJs第二章

来源:互联网 发布:数据库开发工程师招聘 编辑:程序博客网 时间:2024/06/12 01:00

英文原版免积分下载地址:http://download.csdn.net/detail/yangnianbing110/7695577

第二章:AngularJs应用的骨架
不同于之前的典型的库,你可以单独挑选使用对自己有用的函数,angular被设计为相互协作的套件,本章中我们会介绍Angular中所有基础的模块,因此你可以理解他们是如何相互协作的,后面的章节会对这些模块做更为详细的介绍。

调用Angular
启动Angular需要两步:
1.加载angular.js库
2.通过ng-app指定告诉Angular哪部分DOM为其作用范围

加载Script
加载脚本同加载其他库的脚本一样,可以通过google的cdn加载:
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.0.4/angular.min.js"></script>
强烈建议加载Google的cdn,google的服务相当快,而且这些加载的脚本的缓存可以跨应用使用。当然也可以从本地进行加载。

通过ng-app定义angular边界
ng-app告诉框架其作用边界,如果整个页面都为angular作用范围,那可ng-app加到html标签上:
<html ng-app>
</html>
如果只想管理部分的DOM
<html>
<div ng-app>
</div>
</html>

模型视图控制器
第一章中,我们提及到angular支持mvc风格的开发,因此开发angular应用有非常大的灵活性,你可以如下做:
1.模型包含应用当前的状态
2.视图展示模型数据
3.控制器关机模型和视图之间的交互
可以通过对象属性创建模型,甚至是包含数据的基本类型,模型变量没有任何特殊之处,如果想展示给用户一些文本,可以如下定义一个string:
var someText = "you have started you journey";
编写html模板创建视图,将其和模型中的数据联系起来。
<p>{{someText}}</p>
我们称这种双括号为语法插值,会将表达式替换为一个计算后的值
控制器中定义模型数据并和$scope绑定。
function TextController($scope){
    $scope.someText = someText;
}
将其组合起来:
<html ng-app>
<body ng-controller="TextController">
<p>{{someText}}</p>
<script
src="https://ajax.googleapis.com/ajax/libs/angularjs/1.0.1/angular.min.js">
</script>
<script>
function TextController($scope) {
$scope.someText = 'You have started your journey.';
}
</script>
</body>
</html>
浏览器中运行会看到:You have started your journey.
这种基本类型只适合在简单的情况下使用,通常你需要定义模型对象来包含数据。
var messages = {};
messages.someText = 'You have started your journey.';
function TextController($scope) {
    $scope.messages = messages;
}
在模板中:<p>{{messages.someText}}</p>
稍后讨论$scope对象会看到,像这样创建模型对象会避免一些原型继承带来的问题
我们讨论的好的实践会帮助你省去很多精力。在先前的例子中,我们在全局范围内创建了TextController,我们应该如下在某个模块范围内定义:
<html ng-app='myApp'>
<body ng-controller='TextController'>
<p>{{someText.message}}</p>
<script
src="https://ajax.googleapis.com/ajax/libs/angularjs/1.0.1/angular.min.js">
</script>
<script>
var myAppModule = angular.module('myApp', []);
myAppModule.controller('TextController',
function($scope) {
var someText = {};
someText.message = 'You have started your journey.';
$scope.someText = someText;
});
</script>
</body>
</html>
该例子中我们告诉ng-app模块的名称为myApp,我们告诉Angular对象创建名为myApp的模块,并通过我们的控制器函数来响应模块的控制器功能。
你现在可能想了解模块是怎么一回事,现在你只需要记住不要再全局命名空间内定义变量是一个好的习惯,我们通过模块来实现。

模板和数据绑定
Angular应用中的模板就是CNG服务器加载的普通的html文件,在模板中通过HTML标签和指令定义页面UI。
当加载到浏览器中的时候,Angular将模板和数据结合起来,如第一章中看到的购物车列表:
<div ng-repeat="item in items">
<span>{{item.title}}</span>
</div>
数据是从哪里来的?在购物车示例中,我们定义了一个数组,这种方式在创建UI或者仅仅想了解其工作原理的时候可以,然而对于大多数应用,我们都会从服务器获取持久化数据,运行在浏览器中的应用连接服务器请求所需要的数据,Angular框架负责用数据区渲染模型。
基本的工作流程如下:
1.用户请求应用的首页
2。用户浏览器创建一个连接服务器的http连接,加载index.html页面模板
3.angular等到页面完全加载之后查找ng-app标签定义其工作边界。
4.angualr遍历模板查找指令以及数据绑定,注册DOM操作的监听器,同时从服务器获取初始数据,最后应用启动成功,模板被转换成为DOM
5.连接服务器加载用户所需要的其他的数据。
第一步到第三步是每个angular应用的标准流程,第四部和第五部是可选的,这些步骤可以同步执行,也可以异步执行,为了性能考虑,通常数据和模板一起加载避免多次请求。
通过Angular构造应用,应用模板和其数据分离,因此这些模板可以被缓存,第一次加载之后只需要加载数据,同缓存javascript,图片,css以及其他的资源一样,把模板也进行缓存可以更好的提升应用的性能。

展示文本
通过ng-bind指令可以在UI中的任意位置显示以及更新文本,下面两个形式是等价的:
<p>{{greeting}}</p>或者<p ng-bind="greeting"></p>
两个形式的显示形式是一样的,都会生成<p>Hi there</p>
那么合适选用哪种方式呢?用双括号的方式的语法更加简单。

表单输入
在Angular中使用表单元素非常简单,如前面的例子,我们可以使用ng-model属性将页面元素和模型属性绑定起来,这对于文本输入,单选框,多选框这些元素都有用,我们可以如下创建一个复选框:
<form ng-controller="SomeController">
<input type="checkbox" ng-model="youCheckedIt">
</form>
1.当用户选中的时候,SomeController的$scope对象中的属性youCheckedIt会被赋值为true,去选择的时候会赋值为false
2.youCheckedI被设置为TRUE的时候复选框会被选中,设置为false,复选框会被去选择。
如果用户操作表单元素的时候我们需要执行一些特定操作的时候,可以使用ng-change属性去响应用户的操作,这里我们创建一个简单的计算器帮助新用户理解他们需要多少钱:
<form ng-controller="StartUpController">
   Starting:<input ng-change="computeNeeded()" ng-model="funding.startingEstimate" />
   Recommendation:{{funding.needed}}
  </form>
这个简单的例子中,我们只是输入用户估计值得10倍,我们初始值设置为0:
function StartUpController($scope){
   $scope.funding = {startingEstimate:0};
   
   $scope.computeNeeded = function(){
    $scope.needed = $scope.startingEstimate * 10;
   };
  }
上面的代码有一个潜在的问题,我们只是在用户输入的时候重新进行计算,如果这个值只能用户通过输入框更改那么没有任何问题,但是如果其他的输入框也绑定到该模型上,或者如果从服务器读取新的数据进行更新?
为了在任何情况下字段的更新都重新进行计算,我们使用$scope对象的函数$watch(),稍后会详细讨论。$watch接受两个参数,一个表达式和一个回调函数,任何情况下表达式的值被改变了都会调用回调函数。
这里,我们监听表达式funding.startingEstimate,改变的时候调用函数computeNeeded():
function StartUpController($scope){
   $scope.funding = {startingEstimate:0};
   computeNeeded = function(){
    $scope.funding.needed = $scope.funding.startingEstimate * 10;
   };
   $scope.$watch("funding.startingEstimate", computeNeeded);
  }
需要注意的是watch函数中的表达式是用引号标记的,是一个字符串,这个表达式可以访问$scope对象中的属性或者进行简单的操作。也可以监听一个函数的返回值。
对于表单,我们可以使用ng-submit指令去响应表单提交的操作。
<form ng-submit="requestFunding()" ng-controller="StartUpController">
   Starting:<input ng-model="startingEstimate" />
   Recommendation:{{needed}}
   <button>Fund my startup!</button>
  </form>
 function StartUpController($scope){
   $scope.startingEstimate = 0;
   
   computeNeeded = function(){
    $scope.needed = $scope.startingEstimate * 10;
   };
   
   $scope.requestFunding = function(){
    window.alert("Sorry, please get more customers first");
   };
  }
ng-submit指定会阻止浏览器post提交表单事件。
类似于浏览器的本地事件,Angular提供了类似的事件处理指令。
对于onclick函数使用ng-click,对于ondblick时间使用ng-dbclick等等。
我们将尝试通过reset按钮将输入值置为0来演示:
<form ng-submit="requestFunding()" ng-controller="StartUpController">
Starting: <input ng-change="computeNeeded()" ng-model="startingEstimate">
Recommendation: {{needed}}
<button>Fund my startup!</button>
<button ng-click="reset()">Reset</button>
</form>
function StartUpController($scope) {
$scope.computeNeeded = function() {
$scope.needed = $scope.startingEstimate * 10;
};
$scope.requestFunding = function() {
window.alert("Sorry, please get more customers first.");
};
$scope.reset = function() {
    $scope.startingEstimate = 0;
};
}

关于非侵入式的javascript的几句话:
在你javascript开发的生涯中,可能有人告诉你编写非侵入式的javascipt,使用click,mousedown以及这些类似的行内时间处理是不好的,这种说法是正确的。
非侵入式的javascript有很多种解释:
1.不是所有德尔浏览器都支持javascript,让所有的用户可以看到网页的内容而不需要执行任何代码。
2.人们使用浏览器的方式不一样,盲人使用屏幕阅读器,一些手机用户的手机不能处理这些javascript代码。
3.不同的平台对应于javascript的支持不一样。
4.这些事件处理函数引用的都是全局的函数,当你需要使用其他的类库的时候可能让你头痛。
5.这些事件处理包含结构和行为,让你的代码难于维护和理解。
大部分情况下,这么写javascript确实更加美好,不好的是代码更加复杂难以阅读,通常需要在这些元素上面标记id,通过id获取这些元素,申明元素的事件处理函数,可以创建一种结构只是去创建这些关联,但是大部分的应用都将这些关联散落在程序的每个地方。

在Angular的开发过程中我们重新思考了这个问题。
技术的世界变化飞快,第一点已经不是问题,如果你的浏览器不支持javascript,那么他可能是90年的产物。对于第二点,屏幕阅读也能够支持javascript了,正确的使用RIA语义便签可以方便的创建丰富的界面。手机客户端的浏览器现在也可以运行javascript。
我们现在的问题是:是否可以获得内联javascript代码的可读性与简洁性?
如前所述:对于大部分的内联事件处理函数,angular有对应的格式:ng-eventhandler="exxpression",eventhandler包括click,mousedown,change等等,如果想获取某个点击元素的时间,只需要如下使用ng-click指令:<div ng-click="doSomething()"></div>
你脑袋里面可能会说:不,这种写法不好。但是你可以放心这些指令和之前的事情处理器是不一样的:
1.在每个浏览器里面的表现都是一致的。
2.没有在全局对象上操作,只能访问控制器内定义的函数。
最后一点可能听起来有点神秘,让我们看一个示例,在一个典型的应用内,我们会创建一个导航条,点击不同的选项加载不同的内容:
<div class="navbar" ng-controller="NavController">
   <li class="menu-item" ng-click="doSomething()">Something</li>
  </div>
  <div class="contentArea" ng-controller="ContentAreaController">
   <div ng-click="doSomething()">...</div>
  </div>
 <li>和<div>上都有相同名称的事件监听函数,当用户点击其中的一个时,会发现他们调用的是不同Controller范围内的函数。
function NavController($scope){
   $scope.doSomething = function(){
    alert("A");
   };
  }
 function ContentAreaController($scope){ 
   $scope.doSomething = function(){
    alert("B");
   };
  }
  现在看看第五点:结构和行为混合在一起,但是你不能指出这会带来任何负面的影响,这和我们所说的另外一个问题:视图和逻辑想混合类似。
我们做一个测试看看我们的系统是否有这种耦合:我们能不能创建一个单元测试测试应用的逻辑但是并不需要展示其DOM?
在Angular中我们可以编写包含逻辑的控制器,但是并不用引用DOM。这里引起耦合的问题不是事件处理函数,而是我们以前编写代码的方式。注意到目前我们所写的控制器从来都没有引用DOM,可以轻易的创建于DOM无关的控制器,所有关于元素的定位以及事件的触发都是由Angular完成。

列表,表单以及其他的迭代元素
可能最有用的的Angular指令是ng-repeat,为集合中的每一个元素拷贝一份DOM元素,在任何你想创建列表的地方都可以使用ng-repeat
这里我们为老师编写一个学生的花名册,我们应该服务器获取学生的数据,这里为了简单我们在本地数据模型里定义:
var students = [{name:"Mary Contrary", id:1},
     {name:"Jack Sprat", id:2},
     {name:"Jill Hill", id:3}];
 
function StudentController($scope){
 $scope.students = students;
}
<ul ng-controller="StudentController">
   <li ng-repeat="student in students">
    <a href="/student/view/{{student.id}}">{{student.name}}</a>
   </li>
  </ul>
ng-repeat会拷贝包含其所在的元素以及其子元素在内的所有元素。
同时我们改变包含学生信息的数组会同步改变列表。
var students = [{name:'Mary Contrary', id:'1'},
    {name:'Jack Sprat', id:'2'},
    {name:'Jill Hill', id:'3'}];
function StudentListController($scope) {
$scope.students = students;
$scope.insertTom = function () {
$scope.students.splice(1, 0, {name:'Tom Thumb', id:'4'});
        };
}
<ul ng-controller=''>
<li ng-repeat='student in students'>
<a href='/student/view/{{student.id}}'>{{student.name}}</a>
</li>
</ul>
<button ng-click="insertTom()">Insert</button>
同时可以通过$index访问当前迭代元素的索引,以及一些值为boolean的变量标识该元素是否为第一个,最后一个或者中间的位置,$first, $middle,$last

显示和隐藏
对于菜单以及一环境敏感的工具,显示和隐藏元素是一个很重要的功能,同Angular中其他的东西一样,我们通过改变模型来改变UI,通过指令把这种改变反映到视图中去。
ng-show和ng-hide可以帮助我们实现显示和影藏,这两个指定根据传递给他们的表达式去决定元素是否隐藏。指令的工作原理是将元素的样式设置为display:block来显示元素,设置为display:none来隐藏元素。我们假想创建一个x-射线的控制面板。
<div ng-controller=""DeathrayMenuController>
   <button ng-click="toggleMenu()">Toggle Menu</button>
   <ul ng-show="menuState.show">
    <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.show = false;
 $scope.toggleMenu = function(){
  $scope.menuState.show = !$scope.menuState.show;
 } ;
}


CSS类和样式
现在很容易铜鼓{{}}来动态设置类以及样式。
.menu-disabled-true {
color: gray;
}

<div ng-controller='DeathrayMenuController'>
<ul>
<li class='menu-disabled-{{isDisabled}}' ng-click='stun()'>Stun</li>
</ul>
<div/>

function DeathrayMenuController($scope) {
$scope.isDisabled = false;
$scope.stun = function() {
$scope.isDisabled = 'true'; 
};
}
stun菜单上面的类会被设置为menu-disabled-加上$scope.isDisabled的值组成的类名。初始值是false,因此初始的类名是menu-disabled-false。没有这个css类,因此没有任何效果,当$scope.isDisabled被设置为true的时候,css类名为menu-disabled-true,文本颜色会变为灰色。
行内样式也可以通过这种方式动态设置。
这种方式有一个缺点,其样式类名是拼接起来的,对于小的项目可能很好理解,但是很快就会变得不可维护。
Angular提供了ng-classs和ng-style指令处理这个问题,他们都接受一个表达式作为参数,表达式的值可能是下面的某一种:
1.一个 表示类名的空格分隔符
2.类名数组
3.一个包含类名以及boolean值得map
假设你想在应用的头部展示错误或者警告,可以如下使用ng-class指令:
<style type="text/css">
   .error{
    background-color: red;
   }
   
   .warning{
    background-color: yellow;
   }
  </style>

 <div ng-controller="HeaderController">
   <div ng-class="{error:isError, warning:isWarning}">{{}}</div>
   <button ng-click="showError()">Simulate Error</button>
   <button ng-clicl="showWarning()">Simulate Warning</button>
  </div>

function HeaderController($scope){
 $scope.isError = false;
 $scope.isWarning = false;
 
 $scope.showError = function(){
  $scope.messageText = "this is an error!";
  $scope.isError = true;
  $scope.isWarning = false;
 };
 $scope.showWarning = function(){
  $scope.messageText = "this is an warn!";
  $scope.isError = false;
  $scope.isWarning = true;
 };
}

我们可以用这种方式高亮一个选中的行,我们创建一个餐馆的菜单,高亮用户选中的行:
设置高亮的css样式:
.selected{
    background-color: lightgreen;
   }
模板中我们设置ng-class为{selected:$index == selectedRow},当选中的行等于ng-repeat中的$index时表达式结果为true,通过ng-click获取用户选中的行:
<table ng-controller='RestaurantTableController'>
<tr ng-repeat='restaurant in directory' ng-click='selectRestaurant($index)'
ng-class='{selected: $index==selectedRow}'>
<td>{{restaurant.name}}</td>
<td>{{restaurant.cuisine}}</td>
</tr>
</table>

myAppModule.controller("RestaurantTableController",
function($scope){
 $scope.directory = [{name:'The Handsome Heifer', cuisine:'BBQ'},
   {name:'Greens Green Greens', cuisine:'Salads'},
   {name:'House of Fine Fish', cuisine:'Seafood'}];
 $scope.selectRestaurant = function(row) {
  $scope.selectedRow = row;
 };
});

思考src以及href属性
当在img标签和a标签中使用数据绑定的时候,在src或者href中显示使用{{}}不起作用。因为浏览器加载图片和加载内容是同步进行的,angular没有机会去解释数据的绑定,如下显示的使用数据绑定标签:
<img src="/images/cats/{{favoriteCat}}">
而应该使用ng-src标签或者ng-href标签:
<img ng-src="/images/cats/{{favoriteCat}}">
<a ng-href="/shop/category={{numberOfBalloons}}">some text</a>

表达式:
在模板中使用表达式,在模板,应用程序的逻辑以及数据之间创建联系,同时防止逻辑代码混入模板中。
目前为止,我们通常都是使用基础数据类型作为表达式传递给Angular指令的,但是表达式不仅仅是这些,表达式可以做数学运算(+,- /, *, %),也可以做比较运算(==, !=, >, <, >=, <=),逻辑运算(&&, ||, !),位运算(^, &, |),也可以调用控制器对象通过$scope对象暴露出来的函数。
下面的表达式都是有效的表达式:
<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,虽然有效,但是需要避免。保存视图和逻辑的分离有助于测试。
他们等价于Angular中一个定制的解析器,其中不包含循环接口,逻辑控制结构,如果需要这种操作,应该通过指令去实现。

分离UI和控制器
控制器有个三职责:
1.设置应用程序的初始状态。
2.通过$scope对象暴露数据以及函数给UI模板
3.监视UI的改变采取相应的措施
关于前两点我们已经看到了很多例子,我们讨论一下第三点,控制在概念的定义上:提供了和用户操作视图互动的代码。
为了保持控制器简洁和可管理,我们建议对于每个视图区域创建一个控制器,如果有一个菜单创建一个菜单控制器,如果有一个导航,创建一个导航控制器等等。
你可能大概知道了视图的作用,但是还不明确,控制器负责响应某一部分特定的DOM区域,有两种方式划定这个作用范围:通过ng-controller属性,或者通过路由绑定一个动态加载的模板块。
稍后我们讨论路由和视图。
如果精细的区划分UI区域,可以通过创建嵌套控制器,通过一个继承树来共享模型数据和函数,来保持代码的简洁和可维护性。
<div ng-controller="ParentController">
<div ng-controller="ChildController">...</div>
</div>
我们称之为嵌套控制器,实际上是其scope对象的嵌套。传递到子控制器的$scope继承自父控制器的$scope对象,意味着子控制可以通过$scope对象访问父对象中的所有属性。

通过Scope发布模型数据
传入控制器的$scope对象是对视图暴露模型数据的一种机制,可能应用程序中有很多的数据,但是只有通过$scope对象发布的数据才被认为是模型数据,可以将$scope当做是模型改变的监听器。
我们看到的很多例子中明确的构造scope,例如$scope.count = 5。也可以通过模板间接的去改变模型。
1.通过表达式。表达式在控制器范围内执行,在表达式内设置属性也就是设置$scope对象:
<button ng-click='count=3'>Set count to three</button>
其效果同下面一样:
<div ng-controller='CountController'>
    <button ng-click='setCount()'>Set count to three</button>
</div>
function CountController($scope) {
$scope.setCount = function() {
$scope.count=3;
        }
}
2.在表单输入框中使用ng-model。

通过$watch监视模型改变
可能最常使用的与scope有关的函数是$watch,监听模型的变化,可以用来监听某个对象或者计算结果。该函数的签名如下:
$watch(watchFn, watchAction, deepWatch)
watchFn
    该参数是一个表示表达式的字符串,或者函数的返回值,表达式会被多次执行,因此需要确定是否会有副作用,也就是多次调用不会改变其状态,

watchAction
    当监听的表达式发生变化的时候会被调用,该函数接受三个参数,新的值,旧的值,以及对于scope的引用,函数的签名function(newValue, oldValue, scope).

deepWatch
    如果设置为true,angular会检查监听对象的每一个属性,如果需要监听数组中的每个元素或者对象的所有属性值,需要用到这个参数,angular会遍历数组以及对象,这在数组非常大的时候是一个耗时的操作。

$watch函数返回一个函数,可以注销事件监听,从而不在监听指定对象的改变。
如果需要先监听一个事件,之后需要注销掉事件监听:
var dereg = $scope.$watch('someModel.someProperty', callbackOnChange());
dereg();
让我们回顾购物车的例子,现在如果客户购物车内的商品超过$100,那么给$10的折扣:
<div ng-controller="CartController">
<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>
</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.discount;
};
function calculateDiscount(newValue, oldValue, scope) {
$scope.bill.discount = newValue > 100 ? 10 : 0;
}
$scope.$watch($scope.totalCart, calculateDiscount);
}
注意CartController的底部,我们监听totalCart()值的变化,计算商品的和,当这个值发生变化的时候就调用calculateDiscount().

关于watch()的性能
之前的例子结果是真确的,但是存在性能的问题,虽然不明显,但是调试模式下在totalCart()中打个断点,发现页面的渲染过程中,函数被调用了六次,在大型的程序中,这可能出现问题。

为什么会被调用四次,其中的三次我们可以很容易看出来:
1.{{totalCart() | currency}}
2.subtotal()
3.$watch()
之后需angular会再次执行这几个步骤,因此总共四次,这样做是为了确保模型的改变被正确的传播到视图上,以及模型的数据是否被改变。
angular保留一份监视值得拷贝和现在的数据进行比较确实是否发生改变,事实上angular最多会进行十次的比较来确保模型变化的传播,如果十次迭代之后仍然发生变化,angular会抛出一个错误,你监听的值可能处于循环之中,此时你需要尽心修改。
可能你现在担心这个问题,但是阅读完本书之后,那么就不会有问题了。angular在javascript中实现了数据绑定,这种实现依赖于本地方法的实现Object.observe(),这使得anglar的数据绑定速度可以媲美本地方法。
下一章中可以看到,Angular有一个优秀的Chromet调试扩展-Batarang,会自动高亮花费较高的数据绑定。

通过模块优化依赖
在任何大型的应用中,区分出功能代码块都不是一个容易的工作。我们了解了控制器如何模块化代码并对视图暴露数据,但是应用中其他的代码呢?最容易想到的就是控制器中的其他函数了。
对于小型的应用,这种办法可I型,但是很快就会变得不可维护,控制器会包含任何的事情,会变得不可维护。
那么就分模块吧,提供了对应用的函数进行分组,同时解决了依赖注入的问题,通常我们称这些依赖为service,为我们的额应用提供各种服务。
例如:我们的购物网站控制器需要从服务器获取一个商品列表,通过XHR或者WebSocket从服务器获取数据。
如果不适用模块带么可能是这样的:
function ItemsViewController($scope) {
// make request to server
// parse response into Item objects 
// set Items array on $scope so the view can display it 
}
这种写法当然可以正常工作,但是存在潜在的问题。
1.如果其他的控制器也需要从服务器获取商品,我们需要重复这段代码,会给维护带来负担,如果需要修改就需要修改几个地方。
2.其他如服务器鉴权,很难区分是否属于这个控制器的责任
3.对于单元测试,我们必须运行一台真实的服务器或者装XHR插件去返回数据。
通过模块,我们很容易从依赖注入得到这些,可以这么写:
function ShoppingController($scope, Items) {
    $scope.items = Items.query();
}
这非常的棒,但是Items从哪里来?我们可以将其定义为一个服务。
服务是单例对象,执行应用的一些逻辑,Angular内置有$location用来和浏览器地址,$route用来通过URL转换视图,$http用来和服务器通信等内置服务。
你可以并且应该这么做,抽象出共同点业务做成服务,服务可以在任何需要他们的控制器之间共享,因为如此通过他们在控制器之间进行通信是一种很好的机制,Angular内置的服务以$开头,但是你可以随意命名,最好不适用$开头命名避免命名冲突。
通过模块对象的API定义服务,创建服务有三种方式:
provider(name, object or constructor())
factory(name, $getFunction())
service(name, constructor())
我们稍后讨论provider(),先来看看关于factory的例子:
var shoppingModule = angular.module('ShoppingModule', []); 
shoppingModule.factory('Items', function() { 
var items = {};
items.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 items;
});
当Angular创建ShoppingController控制器的时候会传入$scope以及我们刚刚定义的Items服务,通过名字来匹配进行注入。

需要多少模块?
服务自身有依赖,模块API让依赖拥有依赖。大部分应用中,创建一个模块包含所有依赖就行了,如果从第三方库的服务或者指令,他们会有自己的模块,你的应用依赖于他们,你需要在你的应用中申明对他们的依赖。
var appMod = angular.module('app', ['SnazzyUIWidgets', 'SuperDataSync'];

通过过滤器格式化数据
过滤器可以转换展示给用户的数据,使用过滤器的规则:
{{ expression | filterName : parameter1 : ...parameterN }}
表达式是Angular中合法的表达式,filterName是你想使用的过滤器的名字,过滤器的参数以逗号分割,参数可以是任何合法的Angular表达式。
Angular内置了一些过滤器,例如我们之前看过的:
{{12.9 | currency}}会输出$12.9
将这种转换放到视图是因为这种变化只是针对人来说的,对于处理数字的逻辑没有关系。
其他的过滤器有date,number,uppercase等等。
过滤器也可以串接附加的管道标识,
{{12.9 | currency | number:0 }}输出$13.0
可以自定义过滤器:
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;
});
<body ng-app='HomeModule' ng-controller="HomeController">
    <h1>{{pageHeading | titleCase}}</h1>
</body>
function HomeController($scope) {
    $scope.pageHeading = 'behold the majesty of your page title';
}


通过Route和$location改变视图
ajax应用是单页面的应用,我们通常有多个子页面对用户显示或者隐藏。我们可以使用angular框架的$route来帮助我们,对于不同的URL加载展示不同的模板,实例化一个控制器为模板提供上下文。
通过$routeProvider服务创建路由:
var someModule = angular.module('someModule', [...module dependencies...])
someModule.config(function($routeProvider) {
$routeProvider.
when('url', {controller:aController, templateUrl:'/path/to/tempate'}).
when(...other mappings for your app...).
otherwise(...what to do if nothing else matches...);
)};
当浏览器地址改变的时候就会加载对应的试图,将模板的根元素和控制器进行绑定。
如果任何条件都不匹配则执行otherwise(),通过该功能,我们很容易创建一个超锅gmail,hotmail,我们称之为A-Mail,从简单的起,我们展示邮件的发送日期,标题,以及发送者。点击邮件的时候显示详细的信息。

在主模板中,我们干些特殊的活,不同于将所有的信息放在一个页面进行加载,我们创建一个布局模板进行加载视图。
<html ng-app="AMail">
<head>
<script src="src/angular.js"></script>
<script src="src/controllers.js"></script>
</head>
<body>
<h1>A-Mail</h1>
<div ng-view></div>
</body>
</html>
视图模板会被加载到布局模板中,我们可以将其看成html网页的某一部分,使用ng-repeat迭代消息列表通过表单展示。
<table>
<tr>
<td><strong>Sender</strong></td>
<td><strong>Subject</strong></td>
<td><strong>Date</strong></td>
</tr>
<tr ng-repeat='message in messages'>
<td>{{message.sender}}</td>
<td><a href='#/view/{{message.id}}'>{{message.subject}}</td>
<td>{{message.date}}</td>
</tr>
</table>
用户通过点击邮件主题来查看邮件的具体信息,通过将信息id绑定到url来实现,因此点击id为1的邮件会展示给用户/#/view/1,通过url进行导航。
我们创建另外一个模板展示邮件的详细信息。
<div><strong>Subject:</strong> {{message.subject}}</div>
<div><strong>Sender:</strong> {{message.sender}}</div>
<div><strong>Date:</strong> {{message.date}}</div>
<div>
<strong>To:</strong>
<span ng-repeat='recipient in message.recipients'>{{recipient}} </span>
<div>{{message.message}}</div>
<a href='#/'>Back to message list</a>
现在我们需要给这些模板绑定控制器。
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 = [
{
id: 0, sender: 'jean@somecompany.com', subject: 'Hi there, old friend',
date: 'Dec 7, 2013 12:32:00', recipients: ['greg@somecompany.com'],
message: 'Hey, we should get together for lunch sometime and catch up.'
+'There are many things we should collaborate on this year.'
}, 
{
id: 1, sender: 'maria@somecompany.com',
subject: 'Where did you leave my laptop?',
date: 'Dec 7, 2013 8:15:12', recipients: ['greg@somecompany.com'],
message: 'I thought you were going to put it in my desk drawer.'
+'But it does not seem to be there.'
},
{
id: 2, sender: 'bill@somecompany.com', subject: 'Lost python',
date: 'Dec 6, 2013 20:35:02', recipients: ['greg@somecompany.com'],
message: "Nobody panic, but my pet python is missing from her cage.'
+'She doesn't move too fast, so just call me if you see her."
} ];

function ListController($scope) {
    $scope.messages = messages;
}

function DetailController($scope, $routeParams) {
    $scope.message = messages[$routeParams.id];
}
我们通过多个视图创建出一个应用的基本结构,通过url来改变视图,意味着用户可以使用前进以及后退按钮来导航,也可以做标签来不通过应用直接访问邮件,即使他们只是html页面一部分。

与服务器通信
虽然没做示范,但是大部分应用都会同服务器打交道,也许移动易用除外,但对于其他的任何事物,不论是在云上持久化消息或者是与其他用户即时通讯,都需要同服务器打交道。
Angular提供了一个服务称之为$http,有一系列的扩展用来同服务器交互,支持http,jsonp以及cors协议,其包括安全规定用来保护json漏洞以及XSRF攻击,使你很容易转换请求和响应的数据,同时实现了简单的缓存。
现在我们从服务器获取购物车数据而不是在本地进行模仿,因此假想我们创建了一个服务用来返回商品的json格式的数据。
[
{
"id": 0,
"title": "Paint pots",
"description": "Pots full of paint",
"price": 3.95
},
{
"id": 1,
"title": "Polka dots",
"description": "Dots with that polka groove",
"price": 12.95
},
{
"id": 2,
"title": "Pebbles",
"description": "Just little rocks, really",
"price": 6.95
}
]
我们可以如此访问服务器:
function ShoppingController($scope, $http){
 $http.get("/products").success(function(data, status, headers, config){
  $scope.items = data;
 });
}

<body ng-controller="ShoppingController">
<h1>Shop!</h1>
<table>
<tr ng-repeat="item in items">
<td>{{item.title}}</td>
<td>{{item.description}}</td>
<td>{{item.price | currency}}</td>
</tr>
</table>
</div>
</body>
如同我们先前学习到的,我们应该将同服务器交互的工作委托给一个service以更好的代码复用。

使用指令改变DOM
 指定扩展了html规则,  将自定义元素的属性和元素的的行为同dom转换联系起来,通过指令可以创建可重用的UI组件,配置你的应用,做几乎任何你想在UI模板中做的事情。
你可以使用内置的指令来实现angular应用,或者你更愿意自己编写指令。
同服务一样,通过调用模块的directive()函数来创建指令。
var appModule = angular.module('appModule', [...]);
appModule.directive('directiveName', directiveFunction);
编写指令函数是一个很深的领域,本章我们会详细的讲解,但是为了激起你的兴趣,我们先来看一个小例子。
html5有一个新的属性称之为autofocus来自动聚焦在属于元素中,通过这个属性在页面加载完之后可以不需要点击直接使用键盘进行交互,这非常棒,可以不用任何代码就实现这种功能,但是对于非输入元素呢?我们可以使用指令来进行实现。
var appMoule = angular.module('app',[]);
appMoule.directive("ngbkFocus", function(){
 return{
  link:function(scope, element, attrs, controller){
   element[0].focus();
  }
 };
});

<html lang='en' ng-app='app'>
<body ng-controller="SomeController">
<button ng-click="clickUnfocused()">
Not focused
</button>
<button ngbk-focus ng-click="clickFocused()">
I'm very focused!
</button>
<div>{{message.text}}</div>
</body>
</html>

function SomeController($scope) {
$scope.message = { text: 'nothing clicked yet' };
$scope.clickUnfocused = function() {
$scope.message.text = 'unfocused button clicked';
};
$scope.clickFocused = function {
$scope.message.text = 'focus button clicked';
}
}
var appModule = angular.module('app', ['directives']);

校验用户输入
angular增强的form元素有几个不错的功能挺适合单页面应用、其中的一个功能就是校验用户的输入,如果全部合法才能进行提交。
例如:我们需要创建一个注册用户界面,需要用户姓名以及邮箱,但是年龄是可选的,在用户提交数据之前需要进行校验。
我们需要确定用户输入了姓名,以及符合要求的邮箱,如何输入了年龄则也是有效的。
我们可以通过Anglar对于form表单的扩展来实现验证:
<h1>Sign Up</h1>
<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>
需要注意的是,我们使用了required属性来校验邮箱以及用户名的输入,对于angular来说对于不支持html的老浏览器工作也良好。
我们可以添加一个控制器处理表单的提交。
<form name='addUserForm' ng-controller="AddUserController">
控制器中我们可以通过$valid获取表单验证的结果。
我们可以在按钮中添加ng-disabled属性来禁用按钮:
<button ng-disabled='!addUserForm.$valid'>Submit</button>
最后我们可以使用控制器告诉用户添加成功,代码如下:
<h1>Sign Up</h1>
<form name='addUserForm' ng-controller="AddUserController">
<div ng-show='message'>{{message}}</div>
<div>First name: <input name='firstName' 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-min='1'></div>
<div><button ng-click='addUser()'
ng-disabled='!addUserForm.$valid'>Submit</button>
</form>
function AddUserController($scope) {
$scope.message = '';
$scope.addUser = function () {
$scope.message = 'Thanks, ' + $scope.user.first + ', we added you!';
};
}

0 0