Angular应用程序启动
ng-app
指令
<body ng-app></body>
指令指定Angular应用程序的根(root)元素,用于启动Angular应用程序
双大括号表达式:
Nothing here {{'yet'+'!'}}
Angular模板应用的两个核心功能:
- 一个绑定需要被双大括号(Double-curly)
{{ }}
括起来 - 简单表达式'yet'+'!'可以用于绑定
绑定告诉Angular需要进行对表达式求值,并把结果插入放置在绑定的DOM内。
Angular表达式(Angular expression)是类似JavaScript的代码,其在当前的Angular环境数据模型空间(依上下文)中求值,这不同于全局数据空间(window DOM)。
Angular 模版
视图(view)和模板(template)
在Angular中,视图 是数据模型通过HTML 模板的投影,这意味着任何数据模型的变化,Angular都会针对绑定点刷新视图。
<html ng-app="phonecatApp">
<head>
...
<script src="bower_components/angular/angular.js"></script>
<script src="js/controllers.js"></script>
</head>
<body ng-controller="PhoneListCtrl">
<ul>
<li ng-repeat="phone in phones">
{{phone.name}}
<p>{{phone.snippet}}</p>
</li>
</ul>
</body>
</html>
这里我们用ngRepeat指令和两个Angular表达式(expression)取代了硬编码的电话列表:
-
<li>
的ng-repeat="phone in phones"
属性就是一个Angular重复指令,它告诉angular在列表中为每个电话信息重复创建<li>
元素,这里的<li>
标签就作为了模板。 - 在双大括号中的表达式
{{phone.name}}
和{{phone.snippet}}
会用实际的值替代。
这里,我们还添加了新的指令,叫做ng-controller
,它为<body>
标签指定了一个PhoneListCtrl
的控制器,对于这点
- 在双大括号中的表达式
{{phone.name}}
和{{phone.snippet}}
指定的绑定,将参考我们程序中的数据模型,这些都是在我们的PhoneListCtrl的控制器进行设置的。
注意:我们已经通过加载使用
ng-app="phonecatApp"
来指定了一个Angular数据模型,这里phonecatApp
是我们数据模型的名字,这个模型被包含在PhoneListCtrl
中。
数据模型(Model)和控制器(Controller)
这里的 数据模型(Model)(一个简单的电话信息数值)是由PhoneListCtrl控制器(Controller)实例化的。这个控制器只是简单的一个构造函数,它操作了一个$scope参数。
app/js/controllers.js:
var phonecatApp = angular.module('phonecatApp', []);
phonecatApp.controller('PhoneListCtrl', function ($scope) {
$scope.phones = [
{'name': 'Nexus S',
'snippet': 'Fast just got faster with Nexus S.'},
{'name': 'Motorola XOOM™ with Wi-Fi',
'snippet': 'The Next, Next Generation tablet.'},
{'name': 'MOTOROLA XOOM™',
'snippet': 'The Next, Next Generation tablet.'}
];
});
这里,我们定义了一个叫做PhoneListCtrl
的控制器,并且注册它到一个叫做phonecatApp的AngularJS数据模型中。注意,我们在<html>
中的ng-app
指令现在指定了一个phonecatApp
的数据模型,这将在启动Angular过程中加载。
尽管现在看来控制器并没有做什么特别的事,但它是至关重要的。通过上下文数据模型,控制器允许我们建立模型和视图之间的数据绑定。我们由此连接起二者间的描述、数据和逻辑,它们是如下工作的:
- ngController指令定位到
<body>
标签,引用指定名称的控制器,PhoneListCtrl
在JavaScript
文件controllers.js中设定。 -
PhoneListCtrl
控制器把数据链接到$scope
来注入我们的控制器函数。这个scope
是一个在应用定义时预定义(指定)的根作用范围(root scope),然后这个控制器通过<body ng-controller="PhoneListCtrl">
标签绑定。
过滤转换器
app/index.html:
<div class="container-fluid">
<div class="row">
<div class="col-md-2">
<!--工具条内容-->
Search: <input ng-model="query">
</div>
<div class="col-md-10">
<!--主体内容-->
<ul class="phones">
<li ng-repeat="phone in phones | filter:query">
{{phone.name}}
<p>{{phone.snippet}}</p>
</li>
</ul>
</div>
</div>
</div>
我们添加了一个标准的HTML<input>
标签,并对ngRepeat
指令应用了Angular的filter功能来处理<input>
输入。
就是这些改变让用户输入搜索条件后马上可以在手机列表中看到对应的变化。这些新代码展示了:
- 数据绑定:这一Angular核心特性。当页面加载后,Angular把input标签定义的输入框的值绑定到一个命名变量中(页面中命名为query的变量),这个变量还以相同的名字存在于数据模型中,二者是完全同步的。在这个代码中输入框键入的内容(绑定到query)会立即作为列表输出时的过滤条件 (通过phone in phones | filter:query 这一句)。数据模型的变化导致转换器的输入改变,转换器通过更新DOM来反映模型的当前状态。
- 使用
filter
过滤器:filter
功能使用query
来创建一个新的数组(只有那些记录中匹配了query
对应值),ngRepeat
自动根据附加了filter
变动的手机列表数据。这在开发过程中是完全透明的。
可能你注意到了因为angular的加载和初始化是需要时间的,你在浏览器标题栏中会在一瞬间看到如Google Phone Gallery:{{query}}
这样的信息,但其实这不是你希望的(这时angular的数据绑定还没有起作用,就造成了前面的原始数据显示,和后面起作用后的正确显示),对此一个更好的处理方法是使用ngBind
或者ngBindTemplate
命令,这使得在加载器无关的信息对用户来说是不可见的:
<title ng-bind-template="Google Phone Gallery: {{query}}">Google Phone Gallery</title>
双向数据绑定
app/index.html:
Search: <input ng-model="query">
Sort by:
<select ng-model="orderProp">
<option value="name">Alphabetical</option>
<option value="age">Newest</option>
</select>
<ul class="phones">
<li ng-repeat="phone in phones | filter:query | orderBy:orderProp">
<span>{{phone.name}}</span>
<p>{{phone.snippet}}</p>
</li>
</ul>
这里只列出了在index.html
模板中发生变化的部分,主要有:
- 首先,我们添加了一个
<select>
的html元素,并命名为orderProp
,这样我们就有了两个排序依据选择 - 然后我们链接了经过
filter
转换器过滤后的数据作为orderBy
转换器处理的输入。orderBy
转换器会依据规则对输入的数据(数组)排序,即输出一个排序后的新数据(数组形式)。
在这里,Angular在select
元素和orderProp
数据模型间创建了一个双向数据绑定,然后orderProp
被用于orderBy
转换器(过滤器)。
app/js/controllers.js:
var phonecatApp = angular.module('phonecatApp', []);
phonecatApp.controller('PhoneListCtrl', function ($scope) {
$scope.phones = [
{'name': 'Nexus S',
'snippet': 'Fast just got faster with Nexus S.',
'age': 1},
{'name': 'Motorola XOOM™ with Wi-Fi',
'snippet': 'The Next, Next Generation tablet.',
'age': 2},
{'name': 'MOTOROLA XOOM™',
'snippet': 'The Next, Next Generation tablet.',
'age': 3}
];
$scope.orderProp = 'age';
});
- 我们编辑了phones的数据模型,即手机数组,在这个结构对每条记录中增加了age元素项,其可用于按手机推出时间排序。
- 我们在控制器中设置了默认的值作为排序依据,即设置了orderProp的值为 age。如果不在这里设置这个值,则orderBy转换器(过滤器)是没有初始化的,直到我们在页面下拉菜单中进行了选择为止。
现在可以好好来谈谈双向数据绑定了。注意,当浏览器加载完成程序后下拉菜单的Newest项目是被选中的,这就是因为我们在控制器中设置了orderProp的排序依据是age,所以发生了数据模型向UI的绑定,现在我们在下拉菜单选择Alphabetically项,则数据模型马上会自动更新,让手机列表的排序依据要求发生变化,这时的数据绑定方向与前面恰好相反,是UI向数据模型的。
XHRs与依赖注入
在项目app/phones/phones.json文件中有一个巨大的列表,,是采用JSON格式组织的手机信息数据。文件中,它们大概是如下的形式:
[
{
"age": 13,
"id": "motorola-defy-with-motoblur",
"name": "Motorola DEFY\u2122 with MOTOBLUR\u2122",
"snippet": "Are you ready for everything life throws your way?"
...
},
...
]
这里的控制器中,我们使用了Angular的$http
服务,利用一个HTTP
请求,从服务器中获取到app/phones/phones.json
文件数据。$http
是Angular内建web程序通用服务(功能)中的一个,Angular会在程序需要时自动注入这些服务功能。
这些服务由Angular依赖注入子系统进行管理。依赖注入帮助你的程序在好的结构(例如独立的数据、控制和表现/展示)和松耦合(组件间解耦,组件之间的依赖关系不由组件自身确定,而由依赖管理子系统协调)。
app/js/controllers.js
:
var phonecatApp = angular.module('phonecatApp', []);
phonecatApp.controller('PhoneListCtrl', function ($scope, $http) {
$http.get('phones/phones.json').success(function(data) {
$scope.phones = data;
});
$scope.orderProp = 'age';
});
$http
发起一个HTTP GET请求,其内容是请求web服务器端的phones/phones.json
文件(这里的URL是相对于index.html
的相对路径)。服务器端响应这个请求,提供了json文件的内容(响应其实是一个后台服务中动态处理的反馈结果,这对于浏览器或者我们的程序来说这看起来是相同的。这个教程中为了简单起便,直接是一个json数据文件了。)
$http
服务得到了一个有success
方法的promise object
,然后我们就可以调用方法来处理异步响应和分配手机数据来构建我们控制器中作用范围中的phones数据了。注意,这里Angular自动检测了json类型响应,并分析结构化了数据。
为了使用Angular服务,你只用在控制器中需要的地方简单把调用名字作为构造函数的参数,例如:
phonecatApp.controller('PhoneListCtrl', function ($scope, $http) {...}
Angular的依赖注入管理会在控制器初始化时自动的提供声明的功能,而且依赖注入管理还自动的处理相应的层次依赖关系(通常一个服务功能还取决于其他服务功能,这些问题Angular都会自动处理)。
注意参数的名字很重要(不能随意变动),因为依赖管理会用到这些名字进行查找来解决依赖关系并进行注入。
在压缩代码时需要关注的地方
因为Angular的依赖控制采用了以名字作为构造函数的参数传入机制运行,所以如果你想压缩你的PhoneListCtrl
控制器部分JavaScript代码就需要注意一些细节,否则自动机制下所有的参数名会自动压缩而导致依赖注入功能出错。
对于这样的问题就是提供一个禁止压缩的依赖名称列表,这样列表中的名称在压缩时不会进行缩减替换,这样就能保证压缩后的代码能够正常工作了,对此有两个方法:
- 在控制器构造函数上创建一个
inject
(注入)字符串数组,数组中每个字符串都是需要注入的服务名。在我们的例子中就是这样写:
function PhoneListCtrl($scope, $http) {...}
PhoneListCtrl.$inject = ['$scope', '$http'];
phonecatApp.controller('PhoneListCtrl', PhoneListCtrl);
- 使用内联注解语句,函数不是仅仅提供功能要求,还包括一个功能名的字符串数组(内联注解数组),例如:
function PhoneListCtrl($scope, $http) {...}
phonecatApp.controller('PhoneListCtrl', ['$scope', '$http', PhoneListCtrl]);
这两种方法都可以被Angular正常识别进行注入,所以你只需要依据你项目风格选择一种即可。
当采用第二种方式,通常定义一个内联的匿名函数供注册器实施注入:
phonecatApp.controller('PhoneListCtrl', ['$scope', '$http', function($scope, $http) {...}]);
从现在开始,我们将采用内联注解方式(第二种方法)进行处理,使得代码支持压缩。让我们为PhoneListCtrl
添加一个内联注解: app/js/controllers.js
:
var phonecatApp = angular.module('phonecatApp', []);
phonecatApp.controller('PhoneListCtrl', ['$scope', '$http',
function ($scope, $http) {
$http.get('phones/phones.json').success(function(data) {
$scope.phones = data;
});
$scope.orderProp = 'age';
}]);
模板链接与图形
app/index.html
...
<ul class="phones">
<li ng-repeat="phone in phones | filter:query | orderBy:orderProp" class="thumbnail">
<a href="#/phones/{{phone.id}}" class="thumb"><img ng-src="{{phone.imageUrl}}"></a>
<a href="#/phones/{{phone.id}}">{{phone.name}}</a>
<p>{{phone.snippet}}</p>
</li>
</ul>
...
为了动态处理链接(这在后面将导向手机的详细介绍页面),我们给href属性的赋值中用到了由双大括号括起来的数据绑定。在步骤2中,我们把{{phone.name}}
绑定到一个html元素的内容中,这一步{{phone.id}}
将用来绑定到元素属性中。
我们还为每款手机添加了图片,这里用到了ngSrc
命令,它会阻止浏览器在Angular还没有初始化并能够正常展开绑定时发出诸如下面一样的无效URL请求:http://localhost:8000/app/{{phone.imageUrl}}
,而是仅仅描述当前元素需要一个src属性,这个属性(<img src="{{phone.imageUrl}}">
,会在Angular初始化好后进行绑定),使用ngSrc
指令能始终阻止浏览器发出明显是无效的http请求。
路由与多视图
依赖
这一步添加的路由功能是由Angular的ngRoute模块提供,这是一个独立于Angular核心框架的模块。
多视图、路由与布局模板
我们的程序逐渐强大,也变得更加复杂。在之前,我们只有唯一的视图来(用来显示手机列表),并且所有的模板代码都放置在index.html中。新的步骤中会添加一个视图来显示列表中每个设备详细的信息(详细说明视图)。
为了添加详细说明视图,我们扩展index.html
模板文件来包含两个视图, 但这将很快 引起混乱, 为了替代, 我们尝试把index.html
转换到我们称为布局模板(layout template
)其中有模板(布局模板)在所有视图中通用,其他则是局部模板(partial templates
),局部模板只包括当前路由route——视图当前显示需要的部分。
在Angular中,程序的路由通过$routeProvider
声明,它是$route
服务的提供者。这个服务能容易的把控制器、视图模板和浏览器当前的地址栏信息连接起来。使用这个特性,我们可以实现深层链接(deep linking),它可以让你可以利用浏览器历史(后退和前进)以及收藏标签。
模版
$route
服务通常与ngView
指令联合使用。ngView
指令规包含了视图模板如何路由到的布局视图。这使得index.html
模板显得更加完美。
注意:从AngularJS版本1.2开始ngRoute是独立的模块,需要单独加载angular-route.js文件,这可以通过bower下载到。
app/index.html
:
<!doctype html>
<html lang="en" ng-app="phonecatApp">
<head>
...
<script src="bower_components/angular/angular.js"></script>
<script src="bower_components/angular-route/angular-route.js"></script>
<script src="js/app.js"></script>
<script src="js/controllers.js"></script>
</head>
<body>
<div ng-view></div>
</body>
</html>
我们添加了2个新的<script>
标签以在我们的index
文件中加载额外的JavaScript
文件:
-
angular-route.js
:定义了Angular
的ngRoute
模块,用于提供路由功能 -
app.js
:这个文件在程序中提供一个root
模块。
注意我们从index.html
模板文件中移除了大部分代码,取而代之的是一个定义了ng-view
属性的div
标签。移除的代码被放置到phone-list.html
模板文件中:app/partials/phone-list.html
:
<div class="container-fluid">
<div class="row">
<div class="col-md-2">
<!--Sidebar content-->
Search: <input ng-model="query">
Sort by:
<select ng-model="orderProp">
<option value="name">Alphabetical</option>
<option value="age">Newest</option>
</select>
</div>
<div class="col-md-10">
<!--Body content-->
<ul class="phones">
<li ng-repeat="phone in phones | filter:query | orderBy:orderProp" class="thumbnail">
<a href="#/phones/{{phone.id}}" class="thumb"><img ng-src="{{phone.imageUrl}}"></a>
<a href="#/phones/{{phone.id}}">{{phone.name}}</a>
<p>{{phone.snippet}}</p>
</li>
</ul>
</div>
</div>
</div>
我们还增加了一个包含占位符的手机详细说明视图的模板: app/partials/phone-detail.html
:
TBD:detail view for <span>{{phoneId}}</span>
注意现在我们使用了phoneId
表达式,它是定义在PhoneDetailCtrl
控制器中的。
程序模块
为了改善程序的组织,我们使用Angular
的ngRoute
·模块,并且把控制器移动到自己的模块phonecatControllers
。
我们添加angular-route.js
到index.html
中,并添加了一个新的phonecatControllers
模块到controllers.js
中。这些不是所有我们会用到的代码,我们还要添加一些我们程序依赖的模块。下面两个模块都是phonecatApp
需要的,我们可用指令和服务提供。
app/js/app.js
:
var phonecatApp = angular.module('phonecatApp', [
'ngRoute',
'phonecatControllers'
]);
...
注意传递给angular.module
的第2个参数['ngRoute','phonecatControllers']
,这个数组列出了phonecatApp
依赖的模块。
...
phonecatApp.config(['$routeProvider',
function($routeProvider) {
$routeProvider.
when('/phones', {
templateUrl: 'partials/phone-list.html',
controller: 'PhoneListCtrl'
}).
when('/phones/:phoneId', {
templateUrl: 'partials/phone-detail.html',
controller: 'PhoneDetailCtrl'
}).
otherwise({
redirectTo: '/phones'
});
}]);
通过phonecatApp.config()
方法,我们请求$routeProvider
注入自己配置的函数,并且使用$routeProvider.when()
方法提供了自定义路由规则。
我们的程序定义了如下的路由:
- 匹配
/phones
:当URL结尾片段是/phones
时显示一个手机列表。为了构造这个输出视图,Angular会使用phone-list.html
模板和PhoneListCtrl
控制器 - 匹配
/phones/:phoneId
: 当URL结尾片段匹配/phones/:phoneId
时会显示对应手机的详细说明视图。这里:phoneId
是一个URL变量区块。为了生成这个手机详细说明视图,Angular使用phone-detail.html
模板和PhoneDetailCtrl
控制器。 - 其他的匹配(重定向到
/phones
):当浏览器地址栏信息不能匹配到有效路由(自定义的路由设置没有对应项目)时,尝试重定向到/phones
我们再次使用了上一步骤的PhoneListCtrl
控制器,然后添加了一个新的(但是是空的)PhoneDetailCtrl
控制器到app/js/controllers.js
文件中来为手机详细说明视图提供数据。
注意在第2个路由声明中的:phoneId
参数。$route
服务使用路由声明——/phones/:phoneId
——作为一个模板来匹配当前的URL。所有的这些变量定义都会在$routeParams
对象中展开。
把匹配到的URL参数存储到对象$routeParams中,对象名就是变量phoneID,例如匹配到的URL地址为phones/iphone,对象$routeParams的值就是{phoneID:'iphone'}
控制器
app/js/controllers.js
:
var phonecatControllers = angular.module('phonecatControllers', []);
phonecatControllers.controller('PhoneListCtrl', ['$scope', '$http',
function ($scope, $http) {
$http.get('phones/phones.json').success(function(data) {
$scope.phones = data;
});
$scope.orderProp = 'age';
}]);
phonecatControllers.controller('PhoneDetailCtrl', ['$scope', '$routeParams',
function($scope, $routeParams) {
$scope.phoneId = $routeParams.phoneId;
}]);
再次提醒,我们是创建了一个新的模块叫做phonecatControolers
。对于很多小的AngularJS程序,共同创造是一个模块为所有控制器(如果有几个)公用。随着程序的增强,很容易为程序添加更多的公用模块。对于大的程序,你需要为每个主要的程序功能创建特定的模块。
因为我们的例子程序是相对比较小的程序,所以我们把所有的控制器都添加到phoecatControllers
模块中。
更多模版
数据
添加phones.json
,在app/phones/
目录中也包括每个手机的json文件: app/phones/nexus-s.json
:(一个简单的例子)
{
"additionalFeatures": "Contour Display, Near Field Communications (NFC),...",
"android": {
"os": "Android 2.3",
"ui": "Android"
},
...
"images": [
"img/phones/nexus-s.0.jpg",
"img/phones/nexus-s.1.jpg",
"img/phones/nexus-s.2.jpg",
"img/phones/nexus-s.3.jpg"
],
"storage": {
"flash": "16384MB",
"ram": "512MB"
}
}
每个文件都有相同的属性数据结构,我们会把这些数据显示在手机详细说明视图中。
控制器
我们使用$http
服务请求json
文件功能来增强PhoneDetailCtrl
。它们采用手机列表控制器相同的方式工作。 app/js/controllers.js
:
var phonecatControllers = angular.module('phonecatControllers',[]);
phonecatControllers.controller('PhoneDetailCtrl', ['$scope', '$routeParams', '$http',
function($scope, $routeParams, $http) {
$http.get('phones/' + $routeParams.phoneId + '.json').success(function(data) {
$scope.phone = data;
});
}]);
为了构建HTTP的URL请求,我们从定义在$route功能中的当前路由中扩展出$routeParams.phoneId
。
模板
前面用占位符粗略定义的模板被手机详细说明各部分的列表和数据绑定替换。注意我们使用了Angular{{表达式}}
标签和ngRepeat
来从我们数据模型中提取手机数据供显示。 app/partials/phone-detail.html
:
<img ng-src="{{phone.images[0]}}" class="phone">
<h1>{{phone.name}}</h1>
<p>{{phone.description}}</p>
<ul class="phone-thumbs">
<li ng-repeat="img in phone.images">
<img ng-src="{{img}}">
</li>
</ul>
<ul class="specs">
<li>
<span>Availability and Networks</span>
<dl>
<dt>Availability</dt>
<dd ng-repeat="availability in phone.availability">{{availability}}</dd>
</dl>
</li>
...
<li>
<span>Additional Features</span>
<dd>{{phone.additionalFeatures}}</dd>
</li>
</ul>
转换器
定制转换器
为了创建一个新的转换器,你需要创建一个phonecatFilters
模块并把定制的转换器注册到这个模块中。
app/js/filters.js
:
angular.module('phonecatFilters', []).filter('checkmark', function() {
return function(input) {
return input ? '\u2713' : '\u2718';
};
});
我们要定制的转换器叫做"checkmark"。其输入(input
)是true
或者false
,返回的输出结果根据输入是 true
(\u2713 -> ✓) 或者false
(\u2718 -> ✘)。
现在我们的转换器已经准备好了,需要注册到phonecatFilters
模块中,并成为我们主模块phonecatApp
的一个依赖。
app/js/app.js
:
...
angular.module('phonecatApp', ['ngRoute','phonecatControllers','phonecatFilters']);
...
模板
因为转换器的代码存在于app/js/filters.js
文件中,所以我们需要把它添加到布局模板中。 app/index.html
:
...
<script src="js/controllers.js"></script>
<script src="js/filters.js"></script>
...
在Angular模板中采用如下的语法来应用转换器:
{{ expression | filter }}
{{转换器输入内容 | 转换器名称}}
让我们把这个转换器应用到手机详细说明模板中:app/partials/phone-detail.html
:
...
<dl>
<dt>Infrared</dt>
<dd>{{phone.connectivity.infrared | checkmark}}</dd>
<dt>GPS</dt>
<dd>{{phone.connectivity.gps | checkmark}}</dd>
</dl>
...
行为处理
控制器
app/js/controllers.js
:
...
var phonecatControllers = angular.module('phonecatControllers',[]);
phonecatControllers.controller('PhoneDetailCtrl', ['$scope', '$routeParams', '$http',
function($scope, $routeParams, $http) {
$http.get('phones/' + $routeParams.phoneId + '.json').success(function(data) {
$scope.phone = data;
$scope.mainImageUrl = data.images[0];
});
$scope.setImage = function(imageUrl) {
$scope.mainImageUrl = imageUrl;
}
}]);
在PhoneDetailCtrl
控制器中,我们创建了一个mainImageUrl
模型属性,并且设置默认值为第一个手机图片的URL。
我们也创建可一个setImage
的行为处理函数,来改变mainImageUrl
。
模板
app/partials/phone-detail.html
:
<img ng-src="{{mainImageUrl}}" class="phone">
...
<ul class="phone-thumbs">
<li ng-repeat="img in phone.images">
<img ng-src="{{img}}" ng-click="setImage(img)">
</li>
</ul>
我们把ngSrc
指令通过mainImageUrl
绑定到大图片的URL
。
我们也为缩略图注册了ngClick
处理,当用户点击任何缩略图的时候进行处理, 这个处理会使用setImage
行为处理函数依据点击的缩略图来改变mainImageUrl
的值,从而改变大图显示内容。
REST与定制服务
定义定制服务来代表一个RESTful客户端。使用这样的客户端,我们能够用更简单的方法向服务器请求数据,而不需要处理低层次的$httpAPI,HTTP方法以及URL。
依赖
RESTful功能是由Angular中的ngResource
模块提供,这个模块不属于Angular框架核心模块,所以我们必须要单独的安装和引入。
模版
我们在app/js/services.js
中定制服务,所以需要在布局模板中引入这个文件。额外,我们也需要引入angular-resource.js
文件,它提供了ngResource
模块: app/index.html
:
<script src="bower_components/angular-resource/angular-resource.js"></script>
<script src="js/services.js"></script>
服务
我们创建自己的服务提供从服务器访问手机数据: app/js/services.js
:
var phonecatServices = angular.module('phonecatServices', ['ngResource']);
phonecatServices.factory('Phone', ['$resource',
function($resource){
return $resource('phones/:phoneId.json', {}, {
query: {method:'GET', params:{phoneId:'phones'}, isArray:true}
});
}]);
我们使用模块API来注册了一个定制服务(作为工厂函数)。我们用Phone
来代表这个服务(调用工厂函数)。工厂函数类似于一个控制器的构造函数,通过函数参数可以声明依赖注入。这个Phone
服务描述了对$resource
服务功能的依赖。
这个$resource
服务功能只需要很少几行代码就简单的创建一个RESTful客户端。这个客户端可以被用于我们的程序以替代低层次的$http服务。 app/js/app.js
:
...
angular.module('phonecatApp', ['ngRoute', 'phonecatControllers','phonecatFilters', 'phonecatServices']).
...
我们也需要把phonecatServices
模块添加进phonecatApp
的模块依赖数组中。
控制器
通过使用Phone
服务代理替换低层次的$http
服务,我们可以让我们的子控制器(PhoneListCtrl
和PhoneDetailCtrl
)进一步简化。Angular的$resource
服务利用RESTful资源模型也提供了比$http
更好的资源交互(展示),让我们更容易理解控制器中的代码如何工作。
app/js/controllers.js
:
var phonecatControllers = angular.module('phonecatControllers', []);
...
phonecatControllers.controller('PhoneListCtrl', ['$scope', 'Phone', function($scope, Phone) {
$scope.phones = Phone.query();
$scope.orderProp = 'age';
}]);
phonecatControllers.controller('PhoneDetailCtrl', ['$scope', '$routeParams', 'Phone', function($scope, $routeParams, Phone) {
$scope.phone = Phone.get({phoneId: $routeParams.phoneId}, function(phone) {
$scope.mainImageUrl = phone.images[0];
});
$scope.setImage = function(imageUrl) {
$scope.mainImageUrl = imageUrl;
}
}]);
注意现在PhoneListCtrl
中我们把
$http.get('phones/phones.json').success(function(data) {
$scope.phones = data;
});
替换成
$scope.phones = Phone.query();
这是一个简单的声明。我们可以查询到所有手机。
注意在上面的代码中一个重要的事情发生了,就是我们在调用Phone
服务的方法时,没有定义任何的回调函数。虽然这像可以获得同步的返回果,但其实并不是。我们是在"未来"获得一个同步返回结果——一个由XHR响应返回数据填充的对象。因为Angular的数据绑定,我们可以使用这个"未来"结果,并绑定到我们的模板中。到时,当数据获取到后,视图会自动更新。
当然有时这样的"未来"对象以及相关数据绑定并不能满足我们所有的工作要求,所以在具体实现中,我们有时也需要添加回调来处理服务响应。例如PhoneDetailCtrl
控制器中就包括了设置mainImageUrl
这样的回调函数。