1 |
|
ng-app
告诉AngularJS处理整个HTML页并引导应用本示例演示AngularJS的双向数据绑定(bi-directional data binding):
1 |
|
<input ng-model="yourname" />
绑定到一个叫yourname的模型变量。AngularJS会在DOMContentLoaded事件触发时执行,并通过ng-app
指令寻找你的应用根作用域。如果ng-app指令找到了,那么AngularJS将会:
ng-app
指令的标签为根节点来编译其中的DOM。这使得你可以只指定DOM中的一部分作为你的AngularJS应用。1 |
|
如果你需要主动控制一下初始化的过程,你可以使用手动执行引导程序的方法。比如当你使用“脚本加载器(script loader)”,或者需要在AngularJS编译页面之前做一些操作,你就会用到它了。
下面的例子演示了手动初始化AngularJS的方法。它的效果等同于使用ng-app指令 。
1 |
|
下面是一些你的代码必须遵守的顺序:
api/angular.bootstrap
将模板编译成可执行的、数据双向绑定的应用程序。AngularJS应用引导过程有3个重要点:
一旦AngularJS应用引导完毕,它将继续侦听浏览器的HTML触发事件,如鼠标点击事件、按键事件、HTTP传入响应等改变DOM模型的事件。这类事件一旦发生,AngularJS将会自动检测变化,并作出相应的处理及更新。
在AngularJS中,一个视图是模型通过 模板 渲染之后的映射。这意味着,不论模型什么时候发生变化,AngularJS会实时更新结合点,随之更新视图。
比如,视图组件被AngularJS用下面这个模板构建出来:
1 | <html ng-app> |
<li>
标签里面的 ng-repeat="phone in phones"
语句是一个AngularJS迭代器。这个迭代器告诉AngularJS用第一个<li>
标签作为模板为列表中的每一部手机创建一个<li>
元素。phone.name
和phone.snippet
周围的花括号标识着数据绑定。和常量计算不同的是,这里的表达式实际上是我们应用的一个数据模型引用,这些我们在PhoneListCtrl
控制器里面都设置好了。
控制器里面初始化了数据模型,如示例的PhoneListCtrl
控制器:
1 | function PhoneListCtrl($scope) { |
尽管控制器看起来并没有起到什么控制的作用,但是它在这里起到了至关重要的作用。通过给定我们数据模型的语境,控制器允许我们建立模型和视图之间的数据绑定。我们是这样把表现层,数据和逻辑部件联系在一起的:
PhoneListCtrl
——控制器方法的名字(在JS文件controllers.js中)和<body>
标签里面的ngController指令的值相匹配。$scope
) 相关联。当应用启动之后,会有一个根作用域被创建出来,而控制器的作用域是根作用域的一个典型后继。这个控制器的作用域对所有<body ng-controller="PhoneListCtrl">
标记内部的数据绑定有效。AngularJS的作用域理论非常重要:一个作用域可以视作模板、模型和控制器协同工作的粘接器。AngularJS使用作用域,同时还有模板中的信息,数据模型和控制器。这些可以帮助模型和视图分离,但是他们两者确实是同步的!任何对于模型的更改都会即时反映在视图上;任何在视图上的更改都会被立刻体现在模型中。
本节利用过滤器实现一个全文检索。
1 | <div class="container-fluid"> |
在这段代码中,用户在输入框中输入的数据名字称作query
,会立刻作为列表迭代器 (phone in phones | filter:query)
其过滤器的输入。当数据模型引起迭代器输入变化的时候,迭代器可以高效得更新DOM将数据模型最新的状态反映出来。
filter
过滤器:filter
函数使用query
的值来创建一个只包含匹配query
记录的新数组。ngRepeat会根据filter
过滤器生成的手机记录数据数组来自动更新视图。整个过程对于开发者来说都是透明的。
本节演示利用双向绑定实现动态排序:添加一个新的模型属性,把它和迭代器集成起来,然后让数据绑定完成剩下的事情。
1 | Search: <input ng-model="query"> |
orderProp
的<select>
标签,这样我们的用户就可以选择我们提供的两种排序方法。filter
过滤器后面添加一个orderBy
过滤器用其来处理进入迭代器的数据。orderBy
过滤器以一个数组作为输入,复制一份副本,然后把副本重排序再输出到迭代器。AngularJS在select
元素和orderProp
模型之间创建了一个双向绑定。而后,orderProp
会被用作orderBy
过滤器的输入。
1 | function PhoneListCtrl($scope) { |
age
属性,以便根据age
属性对手机进行排序。orderProp
的默认值为age
。如果我们不设置默认值,这个模型会在我们的用户在下拉菜单选择一个顺序之前一直处于未初始化状态。本节演示使用AngularJS一个内置服务$http
来获取网络数据集。我们将使用AngularJS的依赖注入(dependency injection (DI))功能来为PhoneListCtrl控制器提供这个AngularJS服务。
在控制器中使用AngularJS服务$http
向Web服务器发起一个HTTP请求,以此从app/phones/phones.json
文件中获取数据。$http
仅仅是AngularJS众多内建服务中之一,这些服务可以处理一些Web应用的通用操作。AngularJS能将这些服务注入到任何你需要它们的地方。
服务是通过AngularJS的依赖注入DI子系统来管理的。依赖注入服务可以使你的Web应用良好构建(比如分离表现层、数据和控制三者的部件)并且松耦合(一个部件自己不需要解决部件之间的依赖问题,它们都被DI子系统所处理)。
1 | function PhoneListCtrl($scope, $http) { |
$http
向Web服务器发起一个HTTP GET请求,索取phone/phones.json(注意,url是相对于我们的index.html文件的)。服务器用json文件中的数据作为响应。(这个响应或许是实时从后端服务器动态产生的。但是对于浏览器来说,它们看起来都是一样的。为了简单起见,我们在教程里面简单地使用了一个json文件。)
$http
服务用success返回对象应答。当异步响应到达时,用这个对象应答函数来处理服务器响应的数据,并且把数据赋值给作用域的phones数据模型。注意到AngularJS会自动检测到这个json应答,并且已经为我们解析出来了!
为了使用AngularJS的服务,你只需要在控制器的构造函数里面作为参数声明出所需服务的名字,就像这样:
1 | function PhoneListCtrl($scope, $http) {...} |
当控制器构造的时候,AngularJS的依赖注入器会将这些服务注入到你的控制器中。当然,依赖注入器也会处理所需服务可能存在的任何传递性依赖(一个服务通常会依赖于其他的服务)。
注意到参数名字非常重要,因为注入器会用他们去寻找相应的依赖。
$
前缀命名习惯
作为一个命名习惯,AngularJS内建服务,作用域方法,以及一些其他的AngularJS API都在名字前面使用一个$
前缀。不要使用$
前缀来命名你自己的服务和模型,否则可能会产生名字冲突。
由于AngularJS是通过控制器构造函数的参数名字来推断依赖服务名称的。所以如果你要压缩PhoneListCtrl控制器的JS代码,它所有的参数也同时会被压缩,这时候依赖注入系统就不能正确的识别出服务了。
为了克服压缩引起的问题,只要在控制器函数里面给$inject
属性赋值一个依赖服务标识符的数组,就像被注释掉那段最后一行那样:
1 | PhoneListCtrl.$inject = ['$scope', '$http']; |
另一种方法也可以用来指定依赖列表并且避免压缩问题——使用Javascript数组方式构造控制器:把要注入的服务放到一个字符串数组(代表依赖的名字)里,数组最后一个元素是控制器的方法函数:
1 | var PhoneListCtrl = ['$scope', '$http', function($scope, $http) { /* constructor body */ }]; |
上面提到的两种方法都能和AngularJS可注入的任何函数完美协作,要选哪一种方式完全取决于你们项目的编程风格,建议使用数组方式。
本节演示如何编写模板添加图片缩略图和链接。
1 | ... |
处理图片比较简单,只需要使用ngSrc
指令代替<img>
的src
属性标签就可以了。使用ngSrc
指令防止浏览器产生一个指向非法地址的请求。
在本节之前,应用只给我们的用户提供了一个简单的界面(一张所有手机的列表),并且所有的模板代码位于index.html文件中。下一步是增加一个能够显示我们列表中每一部手机详细信息的页面。
为了增加详细信息视图,我们可以拓展index.html来同时包含两个视图的模板代码,但是这样会很快给我们带来巨大的麻烦。相反,我们要把index.html模板转变成“布局模板”。这是我们应用所有视图的通用模板。其他的“局部布局模板”随后根据当前的“路由”被充填入,从而形成一个完整视图展示给用户。
AngularJS中应用的路由通过$routeProvider
来声明,它是$route
服务的提供者。这项服务使得控制器、视图模板与当前浏览器的URL可以轻易集成。应用这个特性我们就可以实现深链接,它允许我们使用浏览器的历史(回退或者前进导航)和书签。
正如从前面你学到的,依赖注入是AngularJS的核心特性,所以你必须要知道一点这家伙是怎么工作的。
当应用引导时,AngularJS会创建一个注入器,我们应用后面所有依赖注入的服务都会需要它。这个注入器自己并不知道$http
和$route
是干什么的,实际上除非它在模块定义的时候被配置过,否则它根本都不知道这些服务的存在。注入器唯一的职责是载入指定的服务模块,在这些模块中注册所有定义的服务提供者,并且当需要时给一个指定的函数注入依赖(服务)。这些依赖通过它们的提供者“懒惰式”(需要时才加载)实例化。
提供者是提供(创建)服务实例并且对外提供API接口的对象,它可以被用来控制一个服务的创建和运行时行为。对于$route
服务来说,$routeProvider
对外提供了API接口,通过API接口允许你为你的应用定义路由规则。
AngularJS模块解决了从应用中删除全局状态和提供方法来配置注入器这两个问题。和AMD或者require.js这两个模块(非AngularJS的两个库)不同的是,AngularJS模块并没有试图去解决脚本加载顺序以及懒惰式脚本加载这样的问题。这些目标和AngularJS要解决的问题毫无关联,所以这些模块完全可以共存来实现各自的目标。
1 | angular.module('phonecat', []). |
为了给我们的应用配置路由,我们需要给应用创建一个模块。我们管这个模块叫做phonecat,并且通过使用configAPI,我们请求把$routeProvider
注入到我们的配置函数并且使用$routeProvider.whenAPI
来定义我们的路由规则。
我们的路由规则定义如下:
/phones
时,手机列表视图会被显示出来。为了构造这个视图,AngularJS会使用phone-list.html模板和PhoneListCtrl控制器。/phone/:phoneId
时,手机详细信息视图被显示出来。这里:phoneId
是URL的变量部分。为了构造手机详细视图,AngularJS会使用phone-detail.html模板和PhoneDetailCtrl控制器。我们重用之前创造过的PhoneListCtrl控制器,同时我们为手机详细视图添加一个新的PhoneDetailCtrl控制器,把它存放在app/js/controllers.js文件里。
$route.otherwise({redirectTo: '/phones'})
语句使得当浏览器地址不能匹配我们任何一个路由规则时,触发重定向到/phones。
注意到在第二条路由声明中:phoneId
参数的使用。$route
服务使用路由声明/phones/:phoneId
作为一个匹配当前URL的模板。所有以:
符号声明的变量(此处变量为phones)都会被提取,然后存放在$routeParams
对象中。
为了让我们的应用引导我们新创建的模块,我们同时需要在ngApp
指令的值上指明模块的名字:
1 | <!doctype html> |
1 | ... |
$route
服务通常和ngView
指令一起使用。ngView
指令的角色是为当前路由把对应的视图模板载入到布局模板中。
1 | <html lang="en" ng-app="phonecat"> |
注意,我们把index.html模板里面大部分代码移除,我们只放置了一个<div>
容器,这个<div>
具有ng-view
属性。我们删除掉的代码现在被放置在phone-list.html模板中:
1 | <div class="container-fluid"> |
同时我们为手机详细信息视图添加一个占位模板。
1 | TBD: detail view for {{phoneId}} |
注意到我们的布局模板中没再添加PhoneListCtrl
或PhoneDetailCtrl
控制器属性!
本节演示使用一个定制的过滤器来把文本串图形化:√作为“true”;以及×作为“false”。来让我们看看过滤器代码长什么样子。
为了创建一个新的过滤器,先创建一个phonecatFilters模块,并且将定制的过滤器注册给这个模块。
1 | angular.module('phonecatFilters', []).filter('checkmark', function() { |
我们的过滤器命名为checkmark。它的输入要么是true,要么是false,并且我们返回两个表示true或false的unicode字符(\u2713
和\u2718
)。
现在我们的过滤器准备好了,我们需要将我们的phonecatFilters模块作为一个依赖注册到我们的主模块phonecat上。
1 | ... |
由于我们的模板代码写在app/js/filter.js文件中,所以我们需要在布局模板中引入这个文件。
1 | ... |
在AngularJS模板中使用过滤器的语法是:
1 | {{ expression | filter }} |
我们把过滤器应用到手机详细信息模板中:
1 | ... |
本节演示如何通过事件处理器实现点击手机缩略图可以将细节图修改成该缩略图的大图。
1 | ... |
在PhoneDetailCtrl控制器中,我们创建了mainImageUrl模型属性,并且把它的默认值设为第一个手机图片的URL。
1 | <img ng-src="{{mainImageUrl}}" class="phone"> |
我们把大图片的ngSrc
指令绑定到mainImageUrl属性上。
同时我们注册一个ngClick
处理器到缩略图上。当一个用户点击缩略图的任意一个时,这个处理器会使用setImage事件处理函数来把mainImageUrl属性设置成选定缩略图的URL。
本节演示定义一个代表RESTful客户端的定制服务。有了这个客户端我们可以用一种更简单的方式来发送XHR请求,而不用去关心更底层的$http
服务(API、HTTP方法和URL)。
定制的服务被定义在app/js/services,所以我们需要在布局模板中引入这个文件。另外,我们也要加载angularjs-resource.js这个文件,它包含了ngResource模块以及其中的$resource
服务,我们一会就会用到它们:
1 | ... |
1 | angular.module('phonecatServices', ['ngResource']). |
我们使用模块API通过一个工厂方法注册了一个定制服务。我们传入服务的名字Phone和工厂函数。工厂函数和控制器构造函数差不多,它们都通过函数参数声明依赖服务。Phone服务声明了它依赖于$resource
服务。
$resource
服务使得用短短的几行代码就可以创建一个RESTful客户端。我们的应用使用这个客户端来代替底层的$http
服务。
1 | ... |
通过重构掉底层的$http
服务,把它放在一个新的服务Phone中,我们可以大大简化子控制器(PhoneListCtrl和PhoneDetailCtrl)。AngularJS的$resource
相比于$http
更加适合于与RESTful数据源交互。而且现在我们更容易理解控制器这些代码在干什么了。
1 | ... |
注意到,在PhoneListCtrl里我们把:
1 | $http.get('phones/phones.json').success(function(data) { |
换成了:
1 | $scope.phones = Phone.query(); |
我们通过这条简单的语句来查询所有的手机。
另一个非常需要注意的是,在上面的代码里面,当调用Phone服务的方法是我们并没有传递任何回调函数。尽管这看起来结果是同步返回的,其实根本就不是。被同步返回的是一个“future”——一个对象,当XHR相应返回的时候会填充进数据。鉴于AngularJS的数据绑定,我们可以使用future并且把它绑定到我们的模板上。然后,当数据到达时,我们的视图会自动更新。
有的时候,单单依赖future对象和数据绑定不足以满足我们的需求,所以在这些情况下,我们需要添加一个回调函数来处理服务器的响应。PhoneDetailCtrl控制器通过在一个回调函数中设置mainImageUrl就是一个解释。