` 列表是通过 `ng-repeat` 创建的数据绑定,所以当项目消失时列表会自动收缩。记住,每当用户在 UI 界面上点击一个 Remove 按钮时,`remove()` 函数就会被调用。
266 |
267 | ##小结
268 |
269 | 我们已经看到了 Angular 最基本的用法以及一些非常简单的例子。本书后面的部分将专注于介绍这个框架所提供的更多功能。
270 |
--------------------------------------------------------------------------------
/Chapter4.markdown:
--------------------------------------------------------------------------------
1 | #分析一个AngularJS应用程序
2 |
3 | 在第2章中, 我们已经讨论了一些AngularJS常用的功能, 然后在第3章讨论了该如何结构化开发应用程序. 现在, 我们不再继续深单个的技术点, 第4章将着眼于一个小的, 实际的应用程序进行讲解. 我们将从一个实际有效的应用程序中感受一下我们之前已经讨论过的(示例)所有的部分.
4 |
5 | 我们将每次介绍一部分, 然后讨论其中有趣和关联的部分, 而不是讨论完整应用程序的前端和核心, 最后在本章的后面我们会慢慢简历这个完整的应用程序.
6 |
7 | ## 目录
8 |
9 | - [应用程序](#应用程序)
10 | - [模型, 控制器和模板之间的关系](#模型-控制器和模板之间的关系)
11 | - [模型](#模型)
12 | - [控制器, 指令和服务](#控制器-指令和服务)
13 | - [服务](#服务)
14 | - [指令](#指令)
15 | - [控制器](#控制器)
16 | - [模板](#模板)
17 | - [测试](#测试)
18 | - [单元测试](#单元测试)
19 | - [脚本测试](#脚本测试)
20 |
21 | ##应用程序
22 |
23 | Guthub是一个简单的食谱管理应用, 我们设计它用于存储我们超级美味的食谱, 同时展示AngularJS应用程序的各个不同的部分. 这个应用程序包含以下内容:
24 |
25 | + 一个两栏的布局
26 | + 在左侧有一个导航栏
27 | + 允许你创建新的食谱
28 | + 允许你浏览现有的食谱列表
29 |
30 | 主视图在左侧, 其变化依赖于URL, 或者食谱列表, 或者单个食谱的详情, 或者可添加新食谱的编辑功能, 或者编辑现有的食谱. 我们可以在图4-1中看到这个应用的一个截图:
31 |
32 | 
33 |
34 | Figure 4-1. Guthub: A simple recipe management application
35 |
36 | 这个完整的应用程序可以在我们的Github中的`chapter4/guthub`中得到.
37 |
38 | ##模型, 控制器和模板之间的关系
39 |
40 | 在我们深入应用程序之前, 让我们来花一两段文字来讨论以下如何将标题中的者三部分在应用程序中组织在一起工作, 同时来思考一下其中的每一部分.
41 |
42 | `model`(模型)是真理. 只需要重复这句话几次. 整个应用程序显示什么, 如何显示在视图中, 保存什么, 等等一切都会受模型的影响. 因此你要额外花一些时间来思考你的模型, 对象的属性将是什么, 以及你打算如何从服务器获取并保存它. 视图将通过数据绑定的方式自动更新, 所以我们的焦点应该集中在模型上.
43 |
44 | `controller`保存业务逻辑: 如何获取模型, 执行什么样的操作, 视图需要从模型中获取什么样的信息, 以及如何将模型转换为你所想要的. 验证职责, 使用调用服务器, 引导你的视图使用正确的数据, 大多数情况下所有的这些事情都属于控制器来处理.
45 |
46 | 最后, `template`代表你的模型将如何显示, 以及用户将如何与你的应用程序交互. 它主要约束以下几点:
47 |
48 | + 显示模型
49 | + 定义用户可以与你的应用程序交互的方式(点击, 文本输入等等)
50 | + 应用程序的样式, 并确定何时以及如何显示一些元素(显示或隐藏, hover等等)
51 | + 过滤和格式化数据(包括输入和输出)
52 |
53 | 要意识到在Angular中的模型-视图-控制器涉及模式中模板并不是必要的部分. 相关, 视图是模板获取执行被编译后的版本. 它是一个模板和模型的组合.
54 |
55 | 任何类型的业务逻辑和行为都不应该进入模板中; 这些信息应该被限制在控制器中. 保持模板的简单可以适当的分离关注点, 并且可以确保你只使用单元测试的情况下就能够测试大多数的代码. 而模板必须使用场景测试的方式来测试.
56 |
57 | 但是, 你可能会问, 在哪里操作DOM呢? DOM操作并不会真正进入到控制器和模板中. 它会存在于Angular的指令中(有时候也可以通过服务来处理, 这样可以避免重复的DOM操作代码). 我们会在我们的GutHub的示例文件中涵盖一个这样的例子.
58 |
59 | 废话少说, 让我们来深入探讨一下它们.
60 |
61 | ##模型
62 |
63 | 对于应用程序我们要保持模型非常简单. 这一有一个菜谱. 在整个完整的应用程序中, 它们是一个唯一的模型. 它是构建一切的基础.
64 |
65 | 每个菜谱都有下面的属性:
66 |
67 | + 一个用于保存到服务器的ID
68 | + 一个名称
69 | + 一个简短的描述
70 | + 一个烹饪说明
71 | + 是否是一个特色的菜谱
72 | + 一个成份数组, 每个成分的数量, 单位和名称
73 |
74 | 就是这样. 非常简单. 应用程序的中一切都基于这个简单的模型. 下面是一个让你食用的示例菜谱(如图4-1一样):
75 | ```js
76 | {
77 | 'id': '1',
78 | 'title': 'Cookies',
79 | 'description': 'Delicious. crisp on the outside, chewy' +
80 | ' on the outside, oozing with chocolatey goodness' +
81 | ' cookies. The best kind',
82 | 'ingredients': [
83 | {
84 | 'amount': '1',
85 | 'amountUnits': 'packet',
86 | 'ingredientName': 'Chips Ahoy'
87 | }
88 | ],
89 | 'instructions': '1. Go buy a packet of Chips Ahoy\n'+
90 | '2. Heat it up in an oven\n' +
91 | '3. Enjoy warm cookies\n' +
92 | '4. Learn how to bake cookies from somewhere else'
93 | }
94 | ```
95 | 下面我们将会看到如何基于这个简单的模型构建更复杂的UI特性.
96 |
97 | ##控制器, 指令和服务
98 |
99 | 现在我们终于可以得到这个让我们牙齿都咬到肉里面去的美食应用程序了. 首先, 我们来看看代码中的指令和服务, 以及讨论以下它们都是做什么的, 然后我们我们会看看这个应用程序需要的多个控制器.
100 |
101 | ###服务
102 | ```js
103 | //this file is app/scripts/services/services.js
104 |
105 | var services = angular.module('guthub.services', ['ngResource']);
106 |
107 | services.factory('Recipe', ['$resource', function(){
108 | return $resource('/recipes/:id', {id: '@id'});
109 | }]);
110 |
111 | services.factory('MultiRecipeLoader', ['Recipe', '$q', function(Recipe, q){
112 | return function(){
113 | var delay = $q.defer();
114 | Recipe.query(function(recipes){
115 | delay.resolve(recipes);
116 | }, function(){
117 | delay.reject('Unable to fetch recipes');
118 | });
119 | return delay.promise;
120 | };
121 | }]);
122 |
123 | services.factory('RecipeLoader', ['Recipe', '$route', '$q', function(Recipe, $route, $q){
124 | return function(){
125 | var delay = $q.defer();
126 | Recipe.get({id: $route.current.params.recipeId}, function(recipe){
127 | delay.resolve(recipe);
128 | }, function(){
129 | delay.reject('Unable to fetch recipe' + $route.current.params.recipeId);
130 | });
131 | return delay.promise;
132 | };
133 | }]);
134 | ```
135 | 首先让我们来看看我们的服务. 在33页的"使用模块组织依赖"小节中已经涉及到了服务相关的知识. 这里, 我们将会更深一点挖掘服务相关的信息.
136 |
137 | 在这个文件中, 我们实例化了三个AngularJS服务.
138 |
139 | 有一个菜谱服务, 它返回我们所调用的Angular Resource. 这些是RESTful资源, 它指向一个RESTful服务器. Angular Resource封装了低层的`$http`服务, 因此你可以在你的代码中只处理对象.
140 |
141 | 注意单独的那行代码 - `return $resource` - (当然, 依赖于`guthub.services`模型), 现在我们可以将`recipe`作为参数传递给任意的控制器中, 它将会注入到控制器中. 此外, 每个菜谱对象都内置的有以下几个方法:
142 |
143 | + Recipe.get()
144 | + Recipe.save()
145 | + Recipe.query()
146 | + Recipe.remove()
147 | + Recipe.delete()
148 |
149 | > 如果你使用了`Recipe.delete`方法, 并且希望你的应用程序工作在IE中, 你应该像这样调用它: `Recipe[delete]()`. 这是因为在IE中`delete`是一个关键字.
150 |
151 | 对于上面的方法, 所有的查询众多都在一个单独的菜谱中进行; 默认情况下`query()`返回一个菜谱数组.
152 |
153 | `return $resource`这行代码用于声明资源 - 也给了我们一些好东西:
154 |
155 | 1. 注意: URL中的id是指定的RESTFul资源. 它基本上是说, 当你进行任何查询时(`Recipe.get()`), 如果你给它传递一个id字段, 那么这个字段的值将被添加早URL的尾部.
156 |
157 | 也就是说, 调用`Recipe.get{id: 15})将会请求/recipe/15.
158 |
159 | 2. 那第二个对象是什么呢? {id: @id}吗? 是的, 正如他们所说的, 一行代码可能需要一千行解释, 那么让我们举一个简单的例子.
160 |
161 | 比方说我们有一个recipe对象, 其中存储了必要的信息, 并且包含一个id.
162 |
163 | 然后, 我们只需要像下面这样做就可以保存它:
164 | ```js
165 | //Assuming existingRecipeObj has all the necessary fields,
166 | //including id(say 13)
167 | var recipe = new Recipe(existingRecipeObj);
168 | recipe.$save();
169 | ```
170 | 这将会触发一个POST请求到`/recipe/13`.
171 |
172 | `@id`用于告诉它, 这里的id字段取自它的对象中同时用于作为id参数. 这是一个附加的便利操作, 可以节省几行代码.
173 |
174 | 在`apps/scripts/services/services.js`中有两个其他的服务. 它们两个都是加载器(Loaders); 一个用于加载单独的食谱(RecipeLoader), 另一个用于加载所有的食谱(MultiRecipeLoader). 这在我们连接到我们的路由时使用. 在核心上, 它们两个表现得非常相似. 这两个服务如下:
175 |
176 | 1. 创建一个`$q`延迟(deferred)对象(它们是AngularJS的promises, 用于链接异步函数).
177 | 2. 创建一个服务器调用.
178 | 3. 在服务器返回值时resolve延迟对象.
179 | 4. 通过使用AngularJS的路由机制返回promise.
180 |
181 | > **AngularJS中的Promises**
182 | >
183 | > 一个promise就是一个在未来某个时刻处理返回对象或者填充它的接口(基本上都是异步行为). 从核心上讲, 一个promise就是一个带有`then()`函数(方法)的对象.
184 | >
185 | >让我们使用一个例子来展示它的优势, 假设我们需要获取一个用户的当前配置:
186 |
187 | ```js
188 | var currentProfile = null;
189 | var username = 'something';
190 |
191 | fetchServerConfig(function(){
192 | fetchUserProfiles(serverConfig.USER_PROFILES, username,
193 | function(profiles){
194 | currentProfile = profiles.currentProfile;
195 | });
196 | });
197 | ```
198 | > 对于这种做法这里有一些问题:
199 | >
200 | > 1. 对于最后产生的代码, 缩进是一个噩梦, 特别是如果你要链接多个调用时.
201 | >
202 | > 2. 在回调和函数之间错误报告的功能有丢失的倾向, 除非你在每一步中处理它们.
203 | >
204 | > 3. 对于你想使用`currentProfile`做什么, 你必须在内层回调中封装其逻辑, 无论是直接的方式还是使用一个单独分离的函数.
205 | >
206 | > Promises解决了这些问题. 在我们进入它是如何解决这些问题之前, 先让我们来看看一个使用promise对同一问题的实现.
207 |
208 | ```js
209 | var currentProfile = fetchServerConfig().then(function(serverConfig){
210 | return fetchUserProfiles(serverConfig.USER_PROFILES, username);
211 | }).then(function{
212 | return profiles.currentProfile;
213 | }, function(error){
214 | // Handle errors in either fetchServerConfig or
215 | // fetchUserProfile here
216 | });
217 | ```
218 | > 注意其优势:
219 | >
220 | > 1. 你可以链接函数调用, 因此你不会产生缩进带来的噩梦.
221 | >
222 | > 2. 你可以放心前一个函数调用会在下一个函数调用之前完成.
223 | >
224 | > 3. 每一个`then()`调用都要两个参数(这两个参数都是函数). 第一个参数是成功的操作的回调函数, 第二个参数是错误处理的函数.
225 | > 4. 在链接中发生错误的情况下, 错误信息会通过错误处理器传播到应用程序的其他部分. 因此, 任何回调函数的错误都可以在尾部被处理.
226 | >
227 | > 你会问, 那什么是`resolve`和`reject`呢? 是的, `deferred`在AngularJS中是一种创建promises的方式. 调用`resolve`来满足promise(调用成功时的处理函数), 同时调用`reject`来处理promise在调用错误处理器时的事情.
228 |
229 | 当我们链接到路由时, 我们会再次回到这里.
230 |
231 | ###指令
232 |
233 | 我们现在可以转移到即将用在我们应用程序的指令上来. 在这个应用程序中将有两个指令:
234 |
235 | `butterbar`
236 |
237 | 这个指令将在路由发生改变并且页面仍然还在加载信息时处理显示和隐藏任务. 它将连接路由变化机制, 基于页面的状态来自动控制显示或者隐藏是否使用哪个标签.
238 |
239 | `focus`
240 |
241 | 这个`focus`指令用于确保指定的文本域(或者元素)拥有焦点.
242 |
243 | 让我们来看一下代码:
244 | ```js
245 | // This file is app/scripts/directives/directives.js
246 |
247 | var directives = angular.module('guthub.directives', []);
248 |
249 | directives.directive('butterbar', ['$rootScope', function($rootScope){
250 | return {
251 | link: function(scope, element attrs){
252 | element.addClass('hide');
253 |
254 | $rootScope.$on('$routeChangeStart', function(){
255 | element.removeClass('hide');
256 | });
257 |
258 | $rootScope.$on('$routeChangeSuccess', function(){
259 | element.addClass('hide');
260 | });
261 | }
262 | };
263 | }]);
264 |
265 | directives.dirctive('focus',function(){
266 | return {
267 | link: function(scope, element, attrs){
268 | element[0].focus();
269 | }
270 | };
271 | });
272 | ```
273 | 上面所述的指令返回一个对象带有一个单一的属性, link. 我们将在第六章深入讨论你可以如何创建你自己的指令, 但是现在, 你应该知道下面的所有事情:
274 |
275 | 1. 指令通过两个步骤处理. 在第一步中(编译阶段), 所有的指令都被附加到一个被查找到的DOM元素上, 然后处理它. 任何DOM操作否发生在编译阶段(步骤中). 在这个阶段结束时, 生成一个连接函数.
276 |
277 | 2. 在第二步中, 连接阶段(我们之前使用的阶段), 生成前面的DOM模板并连接到作用域. 同样的, 任何观察者或者监听器都要根据需要添加, 在作用域和元素之前返回一个活动(双向)绑定. 因此, 任何关联到作用域的事情都发生在连接阶段.
278 |
279 | 那么在我们指令中发生了什么呢? 让我们去看一看, 好吗?
280 |
281 | `butterbar`指令可以像下面这样使用:
282 |
283 |
My loading text...
284 |
285 | 它基于前面隐藏的元素, 然后添加两个监听器到根作用域中. 当每次一个路由开始改变时, 它就显示该元素(通过改变它的class[className]), 每次路由成功改变并完成时, 它再一次隐藏`butterbar`.
286 |
287 | 另一个有趣的事情是注意我们是如何注入`$rootScope`到指令中的. 所有的指令都直接挂接到AngularJS的依赖注入系统, 因此你可以注入你的服务和其他任何需要的东西到其中.
288 |
289 | 最后需要注意的是处理元素的API. 使用jQuery的人会很高兴, 因为他直到使用的是一个类似jQuery的语法(addClass, removeClass). AngularJS实现了一个调用jQuery的自己, 因此, 对于任何AngularJS项目来说, jQuery都是一个可选的依赖项. 如果你最终在你的项目中使用完整的jQuery库, 你应该直到它使用的是它自己内置的jQlite实现.
290 |
291 | 第二个指令(focus)简单得多. 它只是在当前元素上调用`focus()`方法. 你可以用过在任何input元素上添加`focus`属性来调用它, 就像这样:
292 |
293 |
294 |
295 | 当页面加载时, 元素将立即获得焦点.
296 |
297 | ###控制器
298 |
299 | 随着指令和服务的覆盖, 我们终于可以进入控制器部分了, 我们有五个控制器. 所有的这些控制器都在一个单独的文件中(`app/scripts/controllers/controllers.js`), 但是我们会一个个来了解它们. 让我们来看第一个控制器, 这是一个列表控制器, 负责显示系统中所有的食谱列表.
300 | ```js
301 | app.controller('ListCtrl', ['scope', 'recipes', function($scope, recipes){
302 | $scope.recipes = recipes;
303 | }]);
304 | ```
305 | 注意列表控制器中最重要的一个事情: 在这个控制器中, 它并没有连接到服务器和获取是食谱. 相反, 它只是使用已经取得的食谱列表. 你可能不知道它是如何工作的. 你可能会使用路由一节来回答, 因为它有一个我们之前看到`MultiRecipeLoader`. 你只需要在脑海里记住它.
306 |
307 | 在我们提到的列表控制器下, 其他的控制器都与之非常相似, 但我们仍然会逐步指出它们有趣的地方:
308 | ```js
309 | app.controller('ViewCtrl', ['$scope', '$location', 'recipe', function($scope, $location, recipe){
310 | $scope.recipe = recipe;
311 |
312 | $scope.edit = function(){
313 | $location.path('/edit/' + recipe.id);
314 | };
315 | }]);
316 | ```
317 | 这个视图控制器中有趣的部分是其编辑函数公开在作用域中. 而不是显示和隐藏字段或者其他类似的东西, 这个控制器依赖于AngularJS来处理繁重的任务(你应该这么做)! 这个编辑函数简单的改变URL并跳转到编辑食谱的部分, 你可以看见, AngularJS并没有处理剩下的工作. AngularJS识别已经改变的URL并加载响应的视图(这是与我们编辑模式中相同的食谱部分). 来看一看!
318 |
319 | 接下来, 让我们来看看编辑控制器:
320 | ```js
321 | app.controller('EditCtrl', ['$scope', '$location', 'recipe', function($scope, $location, recipe){
322 | $scope.recipe = recipe;
323 |
324 | $scope.save = function(){
325 | $scope.recipe.$save(function(recipe){
326 | $location.path('/view/' + recipe.id);
327 | });
328 | };
329 |
330 | $scope.remove = function(){
331 | delete $scope.recipe;
332 | $location.path('/');
333 | };
334 | }]);
335 | ```
336 | 那么在这个暴露在作用域中的编辑控制器中新的`save`和`remove`方法有什么.
337 |
338 | 那么你希望作用域内的`save`函数做什么. 它保存当前食谱, 并且一旦保存好, 它就在屏幕中将用户重定向到相同的食谱. 回调函数是非常有用的, 一旦你完成任务的情况下执行或者处理一些行为.
339 |
340 | 有两种方式可以在这里保存食谱. 一种是如代码所示, 通过执行$scope.recipe.$save()方法. 这只是可能, 因为`recipe`是一个通过开头部分的RecipeLoader返回的资源对象.
341 |
342 | 另外, 你可以像这样来保存食谱:
343 | ```js
344 | Recipe.save(recipe);
345 | ```
346 | `remove`函数也是很简单的, 在这里它会从作用域中移除食谱, 同时将用户重定向到主菜单页. 请注意, 它并没有真正的从我们的服务器上删除它, 尽管它很再做出额外的调用.
347 |
348 | 接下来, 我们来看看New控制器:
349 | ```js
350 | app.controller('NewCtrl', ['$scope', '$location', 'Recipe', function($scope, $location, Recipe){
351 | $scope.recipe = new Recipe({
352 | ingredents: [{}]
353 | });
354 |
355 | $scope.save = function(){
356 | $scope.recipe.$save(function(recipe){
357 | $location.path('/view/' + recipe.id);
358 | });
359 | };
360 | }]);
361 | ```
362 | New控制器几乎与Edit控制器完全一样. 实际上, 你可以结合两个控制器作为一个单一的控制器来做一个练习. 唯一的主要不同是New控制器会在第一步创建一个新的食谱(这也是一个资源, 因此它也有一个`save`函数).其他的一切都保持不变.
363 |
364 | 最后, 我们还有一个Ingredients控制器. 这是一个特殊的控制器, 在我们深入了解它为什么或者如何特殊之前, 先来看一看它:
365 | ```js
366 | app.controller('Ingredients', ['$scope', function($scope){
367 | $scope.addIngredients = function(){
368 | var ingredients = $scope.recipe.ingredients;
369 | ingredients[ingredients.length] = {};
370 | };
371 |
372 | $scope.removeIngredient = function(index) {
373 | $scope.recipe.ingredients.splice(index, 1);
374 | };
375 | }]);
376 | ```
377 | 到目前为止, 我们看到的所有其他控制器斗鱼UI视图上的相关部分联系着. 但是这个Ingredients控制器是特殊的. 它是一个子控制器, 用于在编辑页面封装特定的恭喜而不需要在外层(父级)来处理. 有趣的是要注意, 由于它是一个字控制器, 继承自作用域中的父控制器(在这里就是Edit/New控制器). 因此, 它可以访问来自父控制器的`$scope.recipe`.
378 |
379 | 这个控制器本身并没有什么有趣或者独特的地方. 它只是添加一个新的成份到现有的食谱成份数组中, 或者从食谱的成份列表中删除一个特定的成份.
380 |
381 | 那么现在, 我们就来完成最后的控制器. 唯一的JavaScript代码块展示了如何设置路由:
382 | ```js
383 | // This file is app/scripts/controllers/controllers.js
384 |
385 | var app = angular.module('guthub', ['guthub.directives', 'guthub.services']);
386 |
387 | app.config(['$routeProvider', function($routeProvider){
388 | $routeProvider.
389 | when('/', {
390 | controller: 'ListCtrl',
391 | resolve: {
392 | recipes: function(MultiRecipeLoader) {
393 | return MultiRecipeLoader();
394 | }
395 | },
396 | templateUrl: '/views/list.html'
397 | }).when('/edit/:recipeId', {
398 | controller: 'EditCtrl',
399 | resolve: {
400 | recipe: function(RecipeLoader){
401 | return RecipeLoader();
402 | }
403 | },
404 | templateUrl: '/views/recipeForm.html'
405 | }).when('/view/:recipeId', {
406 | controller: 'ViewCtrl',
407 | resolve: {
408 | recipe: function(RecipeLoader){
409 | return RecipeLoader();
410 | }
411 | },
412 | templateUrl: '/views/viewRecipe.html'
413 | }).when('/new', {
414 | controller: 'NewCtrl',
415 | templateUrl: '/views/recipeForm.html'
416 | }).otherwise({redirectTo: '/'});
417 | }]);
418 | ```
419 | 正如我们所承诺的, 我们终于到了解析函数使用的地方. 前面的代码设置Guthub AngularJS模块, 路由以及参与应用程序的模板.
420 |
421 | 它挂接到我们已经创建的指令和服务上, 然后指定不同的路由指向应用程序的不同地方.
422 |
423 | 对于每个路由, 我们指定了URL, 它备份控制器, 加载模板, 以及最后(可选的)提供了一个`resolve`对象.
424 |
425 | 这个`resolve`对象会告诉AngularJS, 每个resolve键需要在确定路由正确时才能显示给用户. 对我们来说, 我们想要加载所有的食谱或者个别的配方, 并确保在显示页面之前服务器要响应我们. 因此, 我们要告诉路由提供者我们的食谱, 然后再告诉他如何来取得它.
426 |
427 | 这个环节中我们在第一节中定义了两个服务, 分别时`MultiRecipeLoader`和`RecipeLoader`. 如果`resolve`函数返回一个AngularJS promise, 然后AngularJS会智能在获得它之前等待promise解决问题. 这意味着它将会等待到服务器响应.
428 |
429 | 然后将返回结果传递到构造函数中作为参数(与来自对象字段的参数的名称一起作为参数).
430 |
431 | 最后, `otherwise`函数表示当没有路由匹配时重定向到默认URL.
432 |
433 | > 你可能会注意到Edit和New控制器两个路由通向同一个模板URL, `views/recipeForm.html`. 这里发生了什么呢? 我们复用了编辑模板. 依赖于相关的控制器, 将不同的元素显示在编辑食谱模板中.
434 |
435 | 完成这些工作之后, 现在我们可以聚焦到模板部分, 来看看控制器如何挂接到它们之上, 以及如何管理现实给最终用户的内容.
436 |
437 | ##模板
438 |
439 | 让我们首先来看看最外层的主模板, 这里就是`index.html`. 这是我们单页应用程序的基础, 同时所有其他的视图也会装在到这个模板的上下文中:
440 | ```html
441 |
442 |
443 |
444 |
Guthub - Create and Share
445 |
446 |
447 |
448 |
449 |
450 |
451 |
452 |
453 |
454 |
457 |
Loading...
458 |
459 |
471 |
472 |
473 | ```
474 | 注意前面的模板中有5个有趣的元素, 其中大部分你在第2章中都已经见过了. 让我们逐个来看看它们:
475 |
476 | `ng-app`
477 |
478 | 我们设置了`ng-app`模块为Guthub. 这与我们在`angular.module`函数中提供的模块名称相同. 这便是AngularJS如何知道两个挂接在一起的原因.
479 |
480 | `script`标签
481 |
482 | 这表示应用程序在哪里加载AngularJS. 这必须在所有使用AngularJS的JS文件被加载之前完成. 理想的情况下, 它应该在body的底部完成(\之前).
483 |
484 | `Butterbar`
485 |
486 | 我们第一次使用自定义指令. 在我们定义我们的`butterbar`指令之前, 我们希望将它用于一个元素, 以便在路由改变时显示它, 在成功的时候隐藏它(loading...处理). 需要突出显示这个元素的文本(在这里我们使用了一个非常烦人的"Loading...").
487 |
488 | 链接的`href`值
489 |
490 | `href`用于链接到我们单页应用程序的各个页面. 追它们如何使用#字符来确保页面不会重载的, 并且相对于当前页面. AngularJS会监控URL(只要页面没有重载), 然后在需要的时候起到神奇的作用(或者通常, 将这个非常烦人的路由管理定义为我们路由的一部分).
491 |
492 | `ng-view`
493 |
494 | 这是最后一个神奇的杰作. 在我们的控制器一节, 我们定义了路由. 作为定义的一部分, 每个路由表示一个URL, 控制器关联路由和一个模板. 当AngularJS发现一个路由改变时, 它就会加载关联的模板, 并将控制器添加给它, 同时替换`ng-view`为该模板的内容.
495 |
496 | 有一件引人注目的事情是这里缺少`ng-controller`标签. 大部分应用程序某种程度上都需要一个与外部模板关联的MainController. 其最常见的位置是在body标签上. 在这种情况下, 我们并没有使用它, 因为完整的外部模板没有AngularJS内容需要引用到一个作用域.
497 |
498 | 现在我们来看看与每个控制器关联的单独的模板, 就从"食谱列表"模板开始:
499 | ```html
500 |
501 |
Recipe List
502 |
507 | ```
508 | 是的, 它是一个非常无聊(普通)的模板. 这里只有两个有趣的点. 第一个是非常标准的`ng-repeat`标签用法. 他会获得作用域内的所有食谱并重复检出它们.
509 |
510 | 第二个是`ng-href`标签的用法而不是`href`属性. 这是一个在AngularJS加载期间纯粹无效的空白链接. `ng-href`会确保任何时候都不会给用户呈现一个畸形的链接. 总是会使用它在任何时候使你的URLs都是动态的而不是静态的.
511 |
512 | 当然, 你可能感到奇怪: 控制器在哪里? 这里没有`ng-controller`定义, 也确实没有Main Controller定义. 这是路由映射发挥的作用. 如果你还记得(或者往前翻几页), `/`路由会重定向到列表模板并且带有与之关联的ListController. 因此, 当引用任何变量或者类似的东西时, 它都在List Controller作用域内部.
513 |
514 | 现在我们来看一些有更多实质内容的东西: 视图形式.
515 | ```html
516 |
517 |
{{recipe.title}}
518 |
519 |
{{recipe.decription}}
520 |
521 |
Ingredients
522 |
No Ingredients
523 |
524 | -
525 | {{ingredient.amount}}
526 | {{ingredient.amountUnits
527 | {{ingredient.ingredientName}}
528 |
529 |
530 |
531 |
Instructions
532 |
{{recipe.instructions}}
533 |
534 |
539 | ```
540 | 这是另一个不错的, 很小的包含模板. 我们将提醒你注意三件事, 虽然不会按照它们所出现的顺序.
541 |
542 | 第一个就是非常标准的`ng-repeat`. 食谱(recipes)再次出现在View Controller作用域中, 这是用过在页面现实给用户之前通过`resolve`函数加载的. 这确保用户查看它时也面不是一个破碎的, 未加载的状态.
543 |
544 | 接下来一个有趣的用法是使用`ng-show`和`ng-class`(这里应该是`ng-hide`)来设置模板的样式. `ng-show`标签被添加到\
标签上, 这是用来显示一个星号标记的icon. 现在, 这个星号标记只在食谱是一个特色食谱的时候才显示(例如通过`recipe.featured`布尔值来标记). 理想的情况下, 为了确保适当的间距, 你需要使用一个空白的空格图标, 并给这个空格图标绑定`ng-hide`指令, 然后同归同样的AngularJS表达式`ng-show`来显示. 这是一个常见的用法, 显示一个东西并在给定的条件下来隐藏.
545 |
546 | `ng-class`用于添加一个类(CSS类)给\标签(在这种情况下就是"特色")当食谱是一个特色食谱时. 它添加了一些特殊的高亮来使标题更加引人注目.
547 |
548 | 最后一个需要注意的时表单上的`ng-submit`指令. 这个指令规定在表单被提交的情况下调用`scope`中的`edit()`函数. 当任何没有关联明确函数的按钮被点击时机会提交表单(这种情况下便是Edit按钮). 同样, AngularJS足够智能的在作用域中(从模块,路由,控制器中)在正确的时间里引用和调用正确的方法.
549 |
550 | > **上面这段解释与原书代码有一些差别, 读者自行理解. 原书作者暂未给出解答.**
551 |
552 | 现在我们可以来看看我们最后的模板(可能目前为止最复杂的一个), 食谱表单模板:
553 | ```html
554 |
555 | Edit Recipe
556 |
598 | ```
599 | 不要惊慌. 它看起来像很多代码, 并且它时一个很长的代码, 但是如果你认真研究以下它, 你会发现它并不是非常复杂. 事实上, 其中很多都是很简单的, 比如重复的显示可编辑输入字段用于编辑食谱的模板:
600 |
601 | + `focus`指令被添加到第一个输入字段上(`title`输入字段). 这确保当用户导航到这个页面时, 标题字段会自动聚焦, 并且用户可以立即开始输入标题信息.
602 |
603 | + `ng-submit`指令与前面的例子非常相似, 因此我们不会深入讨论它, 它只是保存是食谱的状态和编辑过程的结束信号. 它会挂接到Edit Controller中的`save()`函数.
604 |
605 | + `ng-model`指令用于将不同的文本输入框和文本域绑定到模型中.
606 |
607 | 在这个页面更有趣的一方面, 并且我们建议你花一点之间来了解它的便是配料列表部分的`ng-controller`标签. 让我们花一分钟的事件来了解以下这里发生了什么.
608 |
609 | 我们看到了一个显示配料成份的列表, 并且容器标签关联了一个`ng-controller`. 这意味着这个`\`标签是Ingredients Controller的作用域. 但是这个模板实际的控制器是什么呢, 是Edit Controller? 事实证明, Ingredients Controller是作为Edit Controller的子控制器创建的, 从而继承了Edit Controller的作用域. 这就是为什么它可以从Edit Controller访问食谱对象(recipe object)的原因.
610 |
611 | 此外, 它还增加了一个`addIngredient()`方法, 这是通过处理高亮的`ng-click`使用的, 那么就只能在`\`标签作用域内访问. 那么为什么你需要这么做呢? 因为这是分离你担忧的最好的方式. 为什么Edit Controller需要一个`addIngredients()`方法, 问99%的模板都不会关心它. 因为如此精确你的子控制器和嵌套控制器是很不错的, 它可以包含任务并循序你分离你的业务逻辑到更多的可维护模块中.
612 |
613 | + 另外一个控制器便是我们在这里想要深入讨论的表单验证控制. 它很容易在AngularJS中设置一个特定的表单字段为"必填项". 只需要简单的添加required标签到输入框上(与前面的代码中的情况一样). 但是现在你要怎么对它.
614 |
615 | 为此, 我们先跳到保存按钮部分. 注意它上面有一个`ng-disabled`指令, 这换言之就是`recipeForm.$invalid`. 这个`recipeForm`是我们已经声明的表单名称. AngularJS增加了一些特殊的变量(`$valid`和`$invalid`只是其中两个)允许你控制表单的元素. AngularJS会查找到所有的必填元素并更新所对应的特殊变量. 因此如果我们的Recipe Title为空, `recipeForm.$invalid`就会被这只为true(`$valid`就为false), 同时我们的保存(Save)按钮就会立刻被禁用.
616 |
617 | 我们还可以给一个文本输入框设置最大和最小长度(输入长度), 以及一个用于验证一个输入字段的正则表达式模式. 另外, 这里还有只在满足特定条件时用于显示特定错误消息的高级用法. 让我们使用一个很小的分支例子来看看:
618 | ```html
619 |
623 | ```
624 | 在前面的这个例子中, 我们添加了一要求: 用户名至少是三个字符(通过使用`ng-minlength`指令). 现在, 表单范围内会关心每个命名输入框的填充形式--在这个例子中我们只有一个`userName`--其中每个输入框都会有一个`$error`对象(这里具体的还包括什么样的错误或者没有错误: `required`, `minlength`, `maclength`或者模式)和一个`$valid`标签来表示输入框本身是否有效.
625 |
626 | 我们可以利用这个来选择性的将错误信息显示给用户, 这根据不用的输入错误类型来显示, 正如我们上面的实例所示.
627 |
628 | 跳回到我们原来的模板中--Recipe表单模板--在这里的ingredients repeater里面还有另外一个很好的`ng-show`高亮的用法. 这个Add Ingredient按钮只会在最后的一个配料的旁边显示. 着可以通过在一个repeater元素范围内调用一个`ng-show`并使用特殊的`$last`变量来完成.
629 |
630 | 最后我们还有最后的一个`ng-click`, 这是附加的第二个按钮, 用于删除该食谱. 注意这个按钮只会在食谱尚未保存的时候显示. 虽然通常它会编写一个更有意义的`ng-hide="recipe.id", 有时候它会使用更有语义的`ng-show="!recipe.id". 也就是说, 如果食谱没有一个id的时候显示, 而不是在食谱有一个id的时候隐藏.
631 |
632 | ##测试
633 |
634 | 随着控制器部分, 我们已经推迟向你显示测试部分了, 但你知道它会即将到来, 不是吗? 在这一节, 我们将会涵盖你已经编写部分的代码测试, 以及涉及你要如何编写它们.
635 |
636 | ###单元测试
637 |
638 | 第一个, 也是非常重要的一种测试是单元测试. 对于控制器(指令和服务)的测试你已经开发和编写的正确的结构, 并且你可能会想到它们会做什么.
639 |
640 | 在我们深入到各个单元测试之前, 让我们围绕所有我们的控制器单元测试来看看测试装置:
641 | ```js
642 | describle('Controllers', function() {
643 | var $scope, ctrl;
644 | //you need to include your module in a test
645 | beforeEach(module('guthub'));
646 | beforeEach(function() {
647 | this.addMatchers({
648 | toEqualData: function(expected) {
649 | return angular.equals(this.actual, expected);
650 | }
651 | });
652 | });
653 |
654 | describle('ListCtrl', function() {....});
655 | // Other controller describles here as well
656 | });
657 | ```
658 | 这个测试装置(我们仍然使用Jasmine的行为方式来编写这些测试)做了几件事情:
659 |
660 | 1. 创建一个全局(至少对于这个测试规范是这个目的)可访问的作用域和控制器, 所以我们不用担心每个控制器会创建一个新的变量.
661 |
662 | 2. 初始化我们应用程序所用的模块(在这里是Guthub).
663 |
664 | 3. 添加一个我们称之为`equalData`的特殊的匹配器. 这基本上允许我们在资源对象(就像食谱)通过`$resource`服务和调用RESTful来执行断言(测试判断).
665 |
666 | > 记得在任何我们处理在`ngRsource`上返回对象的断言时添加一个称为`equalData`特殊匹配器. 这是因为`ngRsource`返回对象还有额外的方法在它们失败时默认希望调用equal方法.
667 |
668 | 这个装置到此为止, 让我们来看看List Controller的单元测试:
669 | ```js
670 | describle('ListCtrl', function(){
671 | var mockBackend, recipe;
672 | // _$httpBackend_ is the same as $httpBackend. Only written this way to diiferentiate between injected variables and local variables
673 | breforeEach(inject(function($rootScope, $controller, _$httpBackend_, Recipe) {
674 | recipe = Recipe;
675 | mockBackend = _$httpBackend_;
676 | $scope = $rootScope.$new();
677 | ctrl = $controller('ListCtrl', {
678 | $scope: $scope,
679 | recipes: [1, 2, 3]
680 | });
681 | }));
682 |
683 | it('should have list of recipes', function() {
684 | expect($scope.recipes).toEqual([1, 2, 3]);
685 | });
686 | });
687 | ```
688 | 记住这个List Controller只是我们最简单的控制器之一. 这个控制器的构造器只是接受一个食谱列表并将它保存到作用域中. 你可以编写一个测试给它, 但它似乎有一点不合理(我们还是这么做了, 因为这个测试很不错).
689 |
690 | 相反, 更有趣的是MulyiRecipeLoader服务方面. 它负责从服务器上获取食谱列表并将它作为一个参数传递(当通过`$route`服务正确的连接时).
691 | ```js
692 | describe('MultiRecipeLoader', function() {
693 | var mockBackend, recipe, loader;
694 | // _$httpBackend_ is the same as $httpBackend. Only written this way to differentiate between injected variables and local variables.
695 |
696 | beforeEach(inject(function(_$httpBackend_, Recipe, MultiRecipeLoader) {
697 | recipe = Recipe;
698 | mockBackend = _$httpBackend_;
699 | loader = MultiRecipeLoader;
700 | }));
701 |
702 | it('should load list of recipes', function() {
703 | mockBackend.expectGET('/recipes').respond([{id: 1}, {id: 2}]);
704 |
705 | var recipes;
706 |
707 | var promise = loader(); promise.then(function(rec) {
708 | recipes = rec;
709 | });
710 |
711 | expect(recipes).toBeUndefined( ) ;
712 |
713 | mockBackend. f lush() ;
714 |
715 | expect(recipes).toEqualData([{id: 1}, {id: 2}]); });
716 | });
717 | // Other controller describes here as well
718 | ```
719 | 在我们的测试中, 我们通过挂接到一个模拟的`HttpBackend`来测试MultiRecipeLoader. 这来自于测试运行时所包含的`angular-mocks.js`文件. 只需将它注入到你的`beforeEach`方法中就足以让你设置预期目的. 接下来, 我们进行了一个更有意义的测试, 我们期望设置一个服务器的GET请求来获取recipes, 浙江返回一个简单的数组对象. 然后使用我们新的自定义的匹配器来确保正确的返回数据. 注意在模拟backend中的`flush()`调用, 这将告诉模拟Backend从服务器返回响应. 你可以使用这个机制来测试控制流程和查看你的应用程序在服务器返回一个响应之前和之后是如何处理的.
720 |
721 | 我们将跳过View Controller, 因为它除了在作用域中添加一个`edit()`方法之外基于与List Controller一模一样. 这是非常简单的测试, 你可以在你的测试中注入`$location`并检查它的值.
722 |
723 | 现在让我们跳到Edit Controller, 其中有两个有趣的点我们进行单元测试. 一个是类似我们之前看到过的`resolve`函数, 并且可以以同样的方式测试. 相反, 我们现在想看看我们可以如和测试`save()`和`remove()`方法. 让我们来看看对于它们的测试(假设我们的测试工具来自于前面的例子):
724 | ```js
725 | describle('EditController', function() {
726 | var mockBackend, location;
727 | beforeEach(inject($rootScope, $controller, _$httpBackend_, $location, Recipe){
728 | mockBackend = _$httpBackend_;
729 | location = $location;
730 | $scope = $rootScope.$new();
731 |
732 | ctrl = $controller('EditCtrl', {
733 | $scope: $scope,
734 | $location: $location,
735 | recipe: new Recipe({id: 1, title: 'Recipe'});
736 | });
737 | }));
738 |
739 | it('should save the recipe', function(){
740 | mockBackend.expectPOST('/recipes/1', {id: 1, title: 'Recipe'}).respond({id: 2});
741 |
742 | // Set it to something else to ensure it is changed during the test
743 | location.path('test');
744 |
745 | $scope.save();
746 | expect(location.path()).toEqual('/test');
747 |
748 | mockBackend.flush();
749 |
750 | expect(location.path()).toEqual('/view/2');
751 | });
752 |
753 | it('should remove the recipe', function(){
754 | expect($scope.recipe).toBeTruthy();
755 | location.path('test');
756 |
757 | $scope.remove();
758 |
759 | expect($scope.recipe).toBeUndefined();
760 | expect(location.path()).toEqual('/');
761 | });
762 | });
763 | ```
764 | 在第一个测试用, 我们测试了`save()`函数. 特别是, 我们确保在我们的对象保存时首先创建一个到服务器的POST请求, 然后, 一旦服务器响应, 地址就改变到新的持久对象的视图食谱页面.
765 |
766 | 第二个测试更简单. 我们进行了简单的检测以确保在作用域中调用`remove()`方法的时候移除当前食谱, 然后重定向到用户主页. 这可以很容易通过注入`$location`服务到我们的测试中并使用它.
767 |
768 | 其余的针对控制器的单元测试遵循非常相似的模式, 因此在这里我们跳过它们. 在他们的底层中, 这些单元测试依赖于一些事情:
769 |
770 | + 确保控制器(或者更可能是作用域)在结束初始化时达到正确的状态
771 |
772 | + 确认经行正确的服务器调用, 以及通过作用域在服务器调用期间和完成后去的正确的状态(通过在单元测试中使用我们的模拟后端服务)
773 |
774 | + 利用AngularJS的依赖注入框架着手处理元素以及控制器对象用于确保控制器会设置正确的状态.
775 |
776 | ###脚本测试
777 |
778 | 一旦我们对单元测试很满意, 我们可能禁不住的往后靠一下, 抽根雪茄, 收工. 但是AngularJS开发者不会这么做, 直到他们完成了他们的脚本测试(场景测试). 虽然单元测试确保我们的每一块JS代码都按照预期工作, 我们也要确保模板加载, 并正确的挂接到控制器上, 以及在模板重点击做正确的事情.
779 |
780 | 这正是AngularJS带给你的脚本测试(场景测试), 它允许你做以下事情:
781 |
782 | + 加载你的应用程序
783 | + 浏览一个特定的页面
784 | + 随意的点击周围和输入文本
785 | + 确保发生正确的事情
786 |
787 | 所以, 脚本测试如何在我们的"食谱列表"页面工作? 首先, 在我们开始实际的测试之前, 我们需要做一些基础工作.
788 |
789 | 对于该脚本测试工作, 我们需要一个工作的Web服务器以准备从Guthub应用上接受请求, 同时将允许我们从它上面存储和获取一个食谱列表. 随意的更改代码以使用内存中的食谱列表(移除`$resource`食谱并只是将它转换为一个JSON对象), 或者复用和修改我们前面章节向你展示的Web服务器, 或者使用Yeoman!
790 |
791 | 一旦我们有了一个服务器并运行起来, 同时服务于我们的应用程序, 然后我们就可以编写和运行下面的测试:
792 | ```js
793 | describle('Guthub App', function(){
794 | it('should show a list of recipes', function(){
795 | browser().navigateTo('/index.html');
796 | //Our Default Guthub recipes list has two recipes
797 | expect(repeater('.recipes li').count()).toEqual(2);
798 | });
799 | });
800 | ```
801 |
--------------------------------------------------------------------------------
/Chapter5.markdown:
--------------------------------------------------------------------------------
1 | #与服务器通信
2 |
3 | 目前,我们已经接触过下面要谈的主题的主要内容,这些内容包括你的Angular应用如何规划设计、不同的AngularJS部件如何装配在一起并正常工作以及AngularJS中的模板代码运行机制的一小部分内容。把它们结合在一起,你就可以搭建一些简洁优雅的Web应用,但他们的运作主要还是限制在客户端.在前面第二章,我们接触了一点用`$http`服务做与服务器端通信的内容,但是在这一章,我们将会深入探讨如何在现实世界的真实应用中使用它(`$http`).
4 |
5 | 在这一章,我们将讨论一下AngularJS如何帮你与服务器端通信,这其中包括在最低抽象等级的层面或者用它提供的优雅的封装器。而且我们将会深入探讨AngularJS如何用内建缓存机制来帮你加速你的应用.如果你想用`SocketIO`开发一个实时的Angular应用,那么第八章有一个例子,演示了如何把·SocketIO·封装成一个指令然后如何使用这个指令,在这一章,我们就不涉及这方面内容了.
6 |
7 | ## 目录
8 |
9 | - [通过$http进行通行](#通过http进行通行)
10 | - [进一步配置你的请求](#进一步配置你的请求)
11 | - [设定HTTP头信息(Headers)](#设定http头信息headers)
12 | - [缓存响应数据](#缓存响应数据)
13 | - [对请求(Request)和响应(Response)的数据所做的转换](#对请求request和响应response的数据所做的转换)
14 | - [单元测试](#单元测试)
15 | - [使用RESTful资源](#使用restful资源)
16 | - [resource资源的声明](#resource资源的声明)
17 | - [定制方法](#定制方法)
18 | - [不要使用回调函数机制!(除非你真的需要它们)](#不要使用回调函数机制除非你真的需要它们)
19 | - [简化的服务器端操作](#简化的服务器端操作)
20 | - [对ngResource做单元测试](#对ngresource做单元测试)
21 | - [$q和预期值(Promise)](#q和预期值promise)
22 | - [响应拦截处理](#响应拦截处理)
23 | - [安全方面的考虑](#安全方面的考虑)
24 | - [JSON的安全脆弱性](#json的安全脆弱性)
25 | - [跨站请求伪造(XSRF)](#跨站请求伪造xsrf)
26 |
27 | ##通过$http进行通行
28 |
29 | 从Ajax应用(使用XMLHttpRequests)发动一个请求到服务器的传统方式包括:得到一个XMLHttpRequest对象的引用、发起请求、读取响应、检验错误代码然后最后处理服务器响应。它就是下面这样:
30 |
31 | var xmlhttp = new XMLHttpRequest();
32 | xmlhttp.onreadystatechange = function() {
33 | if (xmlhttp.readystate == 4 && xmlhttp.status == 200) {
34 | var response = xmlhttp.responseText;
35 | } else if (xmlhttp.status == 400) { // or really anything in the 4 series
36 | // Handle error gracefully
37 | }
38 | };
39 | // Setup connection
40 | xmlhttp.open(“GET”, “http://myserver/api”, true);
41 | // Make the request
42 | xmlhttp.send();
43 |
44 | 对于这样一个简单、常用且经常重复的任务,上面这个代码量比较大.如果你想重复性地做这件事,你最终可能会做一个封装或者使用现成的库.
45 |
46 | AngularJS XHR(XMLHttpRequest) API遵循Promise接口.因为XHRs是异步方法调用,服务器响应将会在未来一个不定的时间返回(当然希望是越快越好).Promise接口保证了这样的响应将会如何处理,它允许Promise接口的消费者以一种可预计的方式使用这些响应.
47 |
48 | 假设我们想从我们的服务器取回用户的信息.如果接口在/api/user地址可用,并且接受id作为url参数,那么我们的XHR请求就可以像下面这样使用Angular的核心$http服务:
49 |
50 | $http.get('api/user', {params: {id: '5'}
51 | }).success(function(data, status, headers, config) {
52 | // Do something successful.
53 | }).error(function(data, status, headers, config) {
54 | // Handle the error
55 | });
56 |
57 | 如果你来自jQuery世界,你可能会注意到:AngularJS和jQuery处理异步需求的方式很相似.
58 |
59 | 我们上面例子中使用的$htttp.get方法仅仅是AngularJS核心服务$http提供的众多简便方法之一.类似的,如果你想使用AngularJS向相同URL带一些POST请求数据发起一个POST请求,你可以像下面这样做:
60 |
61 | var postData = {text: 'long blob of text'};
62 | // The next line gets appended to the URL as params
63 | // so it would become a post request to /api/user?id=5
64 | var config = {params: {id: '5'}};
65 | $http.post('api/user', postData, config
66 | ).success(function(data, status, headers, config) {
67 | // Do something successful
68 | }).error(function(data, status, headers, config) {
69 | // Handle the error
70 | });
71 |
72 | AngularJS为大多数常用请求类型都提供了类似的简便方法,他们包括:
73 |
74 | + GET
75 | + HEAD
76 | + POST
77 | + DELETE
78 | + PUT
79 | + JSONP
80 |
81 | ###进一步配置你的请求
82 |
83 | 有时,工具箱提供的标准请求配置还不够,它可能是因为你想做下面这些事情:
84 |
85 | + 你可能想为请求添加权限验证的头信息
86 | + 改变请求数据的缓存方式
87 | + 在请求被发送或者响应返回时,对数据以一些方式做一定的转换处理
88 |
89 | 在上面这样的情况之下,你可以进一步配置请求,通过可选的传递进请求的配置对象.在之前的例子中,我们使用配置对象来标明可选的URL参数,即便我们哪儿演示的GET和POST方法是简便方法。内部的原生方法可能看上面像相面这样:
90 |
91 | $http(config)
92 |
93 | 下面演示的是一个调用这个方法的伪代码模板:
94 |
95 | $http({
96 | method: string,
97 | url: string,
98 | params: object,
99 | data: string or object,
100 | headers: object,
101 | transformRequest: function transform(data, headersGetter) or an array of functions,
102 | transformResponse: function transform(data, headersGetter) or an array of functions,
103 | cache: boolean or Cache object,
104 | timeout: number,
105 | withCredentials: boolean
106 | });
107 |
108 | GET、POST和其它的简便方法已经设置了请求的method类型,所以不需要再设置这个,config配置对象是传给·$http.get·、·$http.post·方法的最后一个参数,所以当你使用任何简便方法的时候,你任何能用这个config配置对象.
109 |
110 | 你也可以通过传入含有下面这些键的属性集config对象来改变已有的request对象
111 |
112 | + method : 一个表示http请求类型的字符串,比如GET,或者POST
113 | + url : 一个URL字符串代表要请求资源的绝对或相对URL
114 | + params : 一个对象(准确的说是键值映射)包含字符串到字符串内容,它代表了将会转换为URL参数的键值对,比如下面这样:
115 | [{key1: 'value1', key2: 'value2'}]
116 | 它将会被转换为:
117 | ?key1=value&key2=value2
118 | 这串字符将会加在URL后面,如果在value的位置你用一个对象取代字符串或数字,那这个对象将会转换为JSON字符串.
119 | + data :一个字符串或一个对象,它将会被作为请求消息数据被发送.
120 | + timeout : 这是请求被认定为过期之前所要等待的毫秒数.
121 |
122 | 还有部分另外的选项可以被配置,在下面的章节中,我们将会深度探索这些选项.
123 |
124 | ###设定HTTP头信息(Headers)
125 |
126 | AngularJS有一个默认的头信息,这个头信息将会对所有的发送请求使用,它包含以下信息:
127 | 1.Accept: application/json, text/plain, /
128 | 2.X-Requested-With:XMLHttpRequest
129 |
130 | 如果你想设置任何特定的头信息,这儿有两种方法来做这件事:
131 |
132 | 第一种方法,如果你相对所有的发送请求都使用这些特定头信息,那你需要把特定有信息设置为Angular默认头信息的一部分.可以在`$httpProvider.defaults.headers`配置对象里面设置这个,这个步骤通常会在你的app设置config部分来做.所以如果你想移除"Requested-With"头信息且对所有的GET请求启用"DO NOT TRACK"设置,你可以简单地通过以下代码来做:
133 |
134 | angular.module('MyApp',[]).
135 | config(function($httpProvider) {
136 | // Remove the default AngularJS X-Request-With header
137 | delete $httpProvider.default.headers.common['X-Requested-With'];
138 | // Set DO NOT TRACK for all Get requests
139 | $httpProvider.default.headers.get['DNT'] = '1';
140 | });
141 |
142 | 如果你只想对某个特定的请求设置头信息,而不是设置默认头信息.那么你可以通过给$http服务传递包含指定头信息的config对象来做.相同的定制头信息可以作为第二个参数传递给GET请求,第一个参数是URL字符串:
143 |
144 | $http.get('api/user', {
145 | // Set the Authorization header. In an actual app, you would get the auth
146 | // token from a service
147 | headers: {'Authorization': 'Basic Qzsda231231'},
148 | params: {id: 5}
149 | }).success(function() { // Handle success });
150 |
151 | 如何在应用中处理权限验证头信息的成熟示例将会在第八章的Cheetsheets示例部分给出.
152 |
153 | ###缓存响应数据
154 |
155 | AngularJS为HTTP GET请求提供了一个开箱即用的简单缓存系统.缺省情况下,它对所有的请求都是禁用的,但是如果你想对你的请求启用缓存系统,你可以使用以下代码:
156 |
157 | $http.get('http://server/myapi', {
158 | cache: true
159 | }).success(function() { // Handle success });
160 |
161 | 这段代码启用了缓存系统,然后AngularJS将会缓存来自Server的响应数据.但对相同的URL的请求第二次发出时,AngularJS将会从缓存里面取出前一次的响应数据作为响应返回.这个缓存系统也很智能,即使你同时对相同URL发出多个请求,只有一个请求会发向Server,这个请求的响应数据将会反馈给所有(同时发起的)请求。
162 |
163 | 然而这种做法从可用性的角度看可能是有所冲突的,当一个用户首先看到旧的结果,然后新的结果突然冒出来,比如一个用户可能即将单击一个数据项,而实际上这个数据项后台已经发生了变化.
164 |
165 | 注意所有响应(即使是从缓存里取出的)本质上仍旧是异步响应.换句话说,期望你的利用缓存响应时的异步代码运行仍旧和他向后台服务器发出请求时的代码运行机制是一样的.
166 |
167 | ###对请求(Request)和响应(Response)的数据所做的转换
168 |
169 | AngularJS对所有`$http`服务发起的请求和响应做一些基本的转换,它们包括:
170 |
171 | + 请求(Request)转换:
172 | 如果请求的Cofig配置对象的data属性包含一个对象,将会把这个对象序列化为JSON格式.
173 | + 响应(Response)转换:
174 | 如果探测到一个XSRF头,把它剥离掉.如果响应数据被探测为JSON格式,用JSON解析器把它反序列化为JSON对象.
175 |
176 | 如果你需要部分系统默认提供的转换,或者想使用你自己的转换,你可以把你的转换函数作为Config配置对象的一部分传递进去(后面有细述).这些转换函数得到HTTP请求和HTTP响应的数据主体以及它们的头信息.然后把序列化的修改后版本返回出来.在Config对象里面配置这些函数需要使用·transformRequest·键和·transformResponse·键,这些都可以通过使用`$httpProvider·服务在模块的config函数里面配置它.
177 |
178 | 我们什么时候使用这些哪?让我假设我们有一个服务器,它更习惯于jQuery运行的方式.它可能希望我们的POST数据以`key1=val1&key2=val2`字符串的形式传递,而不是以`{key1:val1,key2:val2}`这样的JSON格式.这个时候,我们可能相对每个请求做这样的转换,或者单个地增加transformRequest转换函数,为了达成这个示例这样的目标,我们将要设置一个通用transformRequet转换函数,以便对所有的发出请求,这个函数都可以把JSON格式转换为键值对字符串,下面代码演示了如何做这个工作:
179 |
180 | var module = angular.module('myApp');
181 | module.config(function ($httpProvider) {
182 | $httpProvider.defaults.transformRequest = function(data) {
183 | // We are using jQuery’s param method to convert our
184 | // JSON data into the string form
185 | return $.param(data);
186 | };
187 | });
188 |
189 | ##单元测试
190 |
191 | 目前为止,我们已经了解如何使用`$http`服务以及如何以可能的方式做你需要的配置.但是如何写一些单元测试来保证这些够真实有效的运行哪?
192 |
193 | 正如我们曾经三番五次的提到的那样,AngularJS一直以测试为先的原则而设计.所以Angualr有一个模拟服务器后端,在单元测试中,它可以帮你就可以测试你发出的请求是否正确,甚至可以精确控制模拟响应如何得到处理,什么时候得到处理.
194 |
195 | 让我们探索一下下面这样的单元测试场景:一个控制向服务器发起请求,从服务器得到数据,把它赋给作用域内的模型,然后以具体的模板格式显示出来.
196 |
197 | 我们的`NameListCtrl`控制器是一个非常简单的控制器.它的存在只有一个目的:访问`names`API接口,然后把得到数据存储在作用域scope模型内.
198 |
199 | function NamesListCtrl($scope, $http) {
200 | $http.get('http://server/names', {params: {filter: ‘none’}}).
201 | success(function(data) {
202 | $scope.names = data;
203 | });
204 | }
205 |
206 | 怎样对这个控制器做单元测试?在我们的单元测试中,我们必须保证下面这些条件:
207 |
208 | + `NamesListCtrl`能够找到所有的依赖项(并且正确的得到注入的这些依赖)》
209 | + 当控制器加载时尽可能快地立刻发情请求从服务器得到names数据.
210 | + 控制器能够正确地把响应数据存储到作用域scope的`names`变量属性中.
211 |
212 | 在我们的单元测试中构造一个控制器时,我们给它注入一个scope作用域和一个伪造的HTTP服务,在构建测试控制器的方式和生产中构建控制器的方式其实是一样的.这是推荐方法,尽管它看上去上有点复杂。让我看一下具体代码:
213 |
214 | describe('NamesListCtrl', function(){
215 | var scope, ctrl, mockBackend;
216 |
217 | // AngularJS is responsible for injecting these in tests
218 | beforeEach(inject(function(_$httpBackend_, $rootScope, $controller) {
219 | // This is a fake backend, so that you can control the requests
220 | // and responses from the server
221 | mockBackend = _$httpBackend_;
222 |
223 | // We set an expectation before creating our controller,
224 | // because this call will get triggered when the controller is created
225 | mockBackend.expectGET('http://server/names?filter=none').
226 | respond(['Brad', 'Shyam']);
227 | scope = $rootScope.$new();
228 |
229 | // Create a controller the same way AngularJS would in production
230 | ctrl = $controller(PhoneListCtrl, {$scope: scope});
231 | }));
232 |
233 | it('should fetch names from server on load', function() {
234 | // Initially, the request has not returned a response
235 | expect(scope.names).toBeUndefined();
236 |
237 | // Tell the fake backend to return responses to all current requests
238 | // that are in flight.
239 | mockBackend.flush();
240 | // Now names should be set on the scope
241 | expect(scope.names).toEqual(['Brad', 'Shyam’]);
242 | });
243 | });
244 |
245 | ##使用RESTful资源
246 |
247 | ·$http·服务提供一个比较底层的实现来帮你发起XHR请求,但是同时也给提供了很强的可控性和弹性.在大多数情况下,我们处理的是对象集或者是封装有一定属性和方法的对象模型,比如带有个人资料的自然人对象或者信用卡对象.
248 |
249 | 在上面这样的情况下,如果我们自己构建一个JS对象来表示这种较复杂对象模型,那做法就有点不够nice.如果我们仅仅想编辑某个对象的属性、保存或者更新一个对象,那我们如何让这些状态在服务器端持久化.
250 |
251 | `$resource`正好给你提供这种能力.AngularJS resources可以帮助我们以描述的方式来定义对象模型,可以定义一下这些特征:
252 |
253 | + resource的服务器端URL
254 | + 这种请求常用参数的类型
255 | + (你可以免费自动得到get、save、query、remove和delete方法),除了那些方法,你可以定义其它的方法,这些方法封装了对象模型的特定功能和业务逻辑(比如信用卡模型的charge()付费方法)
256 | + 响应的期望类型(数组或者一个独立对象)
257 | + 头信息
258 |
259 | ------------------------------------------------------
260 | 什么时候你可以用Angular Resources组件?
261 |
262 | 只有你的服务器端设施是以RESTful方式提供服务的时候,你才应该用Angular resources组件.比如信用卡那个案例,我们将用它作为本章这一部分的例子,他将包括以下内容:
263 |
264 | 1. 给地址`/user/123/card`发送一个GET请求,将会得到用户123的信用卡列表.
265 | 2. 给地址`/user/123/card/15`发送一个GET请求,将会得到用户123本人的ID为15的信用卡信息
266 | 3. 给地址`/user/123/card`发送一个在POST请求数据部分包含信用卡信息的POST请求,将会为用户123新创建一个信用卡
267 | 4. 给地址`/user/123/card/15`发送一个在POST请求数据部分包含信用卡信息的POST请求,将会更新用户123的ID为5的信用卡的信息.
268 | 5. 给地址`/user/123/card/15`一个方法为DELETE类型的请求,将会删除掉用户123的ID为5的信用卡的数据.
269 |
270 | -------------------------------------------------------
271 |
272 | 除了按照你的要求给你提供一个查询服务器端信息的对象,`$resource`还可以让你使用返回的数据对象就像他们是持久化数据模型一样,可以修改他们,还可以把你的修改持久化存储下来.
273 |
274 | `ngResource`是一个单独的、可选的模块.要想使用它,你看需要做以下事情:
275 |
276 | + 在你的HTML文件里面引用angular-resource.js的实际地址
277 | + 在你的模块依赖里面声明对它的依赖(例如,angular.module('myModule',['ngResource'])).
278 | + 在需要的地方,注入$resource这个依赖项.
279 |
280 | 在我们看怎样用ngResource方法创建一个resource资源之前,我们先看一下怎样用基本的$http服务做类似的事情.比如我们的信用卡resource,我们想能够读取、查询、保存信用卡信息,另外还要能为信用卡还款.
281 |
282 | 这儿是上述需求一个可能的实现:
283 |
284 | myAppModule.factory('CreditCard', ['$http', function($http) {
285 | var baseUrl = '/user/123/card';
286 | return {
287 | get: function(cardId) {
288 | return $http.get(baseUrl + '/' + cardId);
289 | },
290 | save: function(card) {
291 | var url = card.id ? baseUrl + '/' + card.id : baseUrl;
292 | return $http.post(url, card);
293 | },
294 | query: function() {
295 | return $http.get(baseUrl);
296 | },
297 | charge: function(card) {
298 | return $http.post(baseUrl + '/' + card.id, card, {params: {charge: true}});
299 | }
300 | };
301 | }]);
302 |
303 | 取代以上方式,你也可以轻松创建一个在你的应用中始终如一的Angular资源服务,就像下面代码这样:
304 |
305 | myAppModule.factory('CreditCard', ['$resource', function($resource) {
306 | return $resource('/user/:userId/card/:cardId',
307 | {userId: 123, cardId: '@id'},
308 | {charge: {method:'POST', params:{charge:true}, isArray:false});
309 | }]);
310 |
311 | 做到现在,你就可以任何时候从Angular注入器里面请求一个CreditCard依赖,你就会得到一个Angular资源,默认情况下,这个资源会提供一些初始的可用方法.表格5-1列出了这些初始方法以及他们的运行行为,这样你就可以知道在服务器怎样配置来配合这些方法了.
312 |
313 | 表格5-1 一个信用卡reource
314 | Function Method URL Expected Return
315 | CreditCard.get({id: 11}) GET /user/123/card/11 Single JSON
316 | CreditCard.save({}, ccard) POST /user/123/card with post data “ccard” Single JSON
317 | CreditCard.save({id: 11}, ccard) POST /user/123/card/11 with post data “ccard” Single JSON
318 | CreditCard.query() GET /user/123/card JSON Array
319 | CreditCard.remove({id: 11}) DELETE /user/123/card/11 Single JSON
320 | CreditCard.delete({id: 11}) DELETE /user/123/card/11 Single JSON
321 |
322 | 让我们看一个信用卡resource使用的代码样例,这样可以让你理解起来觉得更浅显易懂.
323 |
324 | // Let us assume that the CreditCard service is injected here
325 | // We can retrieve a collection from the server which makes the request
326 | // GET: /user/123/card
327 | var cards = CreditCard.query();
328 | // We can get a single card, and work with it from the callback as well
329 | CreditCard.get({cardId: 456}, function(card) {
330 | // each item is an instance of CreditCard
331 | expect(card instanceof CreditCard).toEqual(true);
332 | card.name = "J. Smith";
333 | // non-GET methods are mapped onto the instances
334 | card.$save();
335 | // our custom method is mapped as well.
336 | card.$charge({amount:9.99});
337 | // Makes a POST: /user/123/card/456?amount=9.99&charge=true
338 | // with data {id:456, number:'1234', name:'J. Smith'}
339 | });
340 |
341 | 前面这个样例代码里面发生了很多事情,所以我们将会一个一个地认真讲解其中的重要部分:
342 |
343 | ###resource资源的声明
344 |
345 | 声明你自己的`$resource`非常简单,只要调用注入的$resource函数,并给他传入正确的参数即可。(你现在应该已经知道如何注入依赖,对吧?)
346 |
347 | $resource函数只有一个必须参数,就是提供后台资源数据的URL地址,另外还有两个可选参数:默认request参数信息和其它的想在资源上要配置的动作.
348 |
349 | 请注意:第一个URL地址参数中的的变量数据是参数化可配置的(:冒号是参数变量的语法符号,比如`:userId`以为这个参数将会被实际的userId参数变量取代(译者注:写过参数化SQL语句的人应该很熟悉),而`:cardId`将会被cardId参数变量的值所取代),如果没有给函数传递这些参数变量,那那个位置将会被空字符取代.
350 |
351 | 函数的第二个参数则负责提供所有请求的默认参数变量信息.在这个案例中,我们给userId参数传递一个常量:123,cardId参数则更有意思,我们给cardId参数传递了一个"@id"字符串.这意味着如果我们使用一个从服务器返回的对象而且我们可以调用这个对象的任何方法(比如$save),那么这个对象的id属性将会被取出来赋给cardId字段.
352 |
353 | 函数的第三个参数是一些我们想要暴露的其它方法,这些方法是对定制资源做操作的方法.在下面的章节,我们将会深度讨论这个话题
354 |
355 | ###定制方法
356 |
357 | $resource函数的第三个参数是可选的,主要用来传递要在resource资源上暴露的其它自定义方法。
358 |
359 | 在这个案例中,我们自定义了一个方法charge.这个自定义方法可以通过传递一个对象而被配置上.这个对象里有个键值,表明了此方法的暴露名称.这个配置需要顶一个request请求的方法类型(GET,POST等等),以及该请求中需要的参数也要被传递(比如charge=true),并且声明返回对象是数组还是单个普通对象。这一切到搞定之后,你就可以在有这个业务实际需要求的时候,自由地调用`CreditCard.charge()`方法.
360 |
361 | ###不要使用回调函数机制!(除非你真的需要它们)
362 |
363 | 第三个需要注意的事情是资源调用的返回类型.让我们再次关注一下`CreditCard.query()`这个函数.你将会看到不是在回调函数中给cards赋值,而是直接把它赋给card变量.在异步服务器请求的情况下唉,这样的代码是如何运作的哪?
364 |
365 | 你担心代码是否正常工作是对的,但是代码实际上是可以正常工作的.这里实际发生的是AngularJS赋值了一个引用(是普通对象的还是数组的取决于你期望的返回类型),这个引用将会在未来服务器请求响应返回时被填充.在这期间,这个引用是个空应用.
366 |
367 | 因为在AngularJS应用中的大多数通用过程都是从服务器端取数据,把它赋给一个变量,然后在模版上显示它,而上面这样的简化机制非常优雅.在你的控制器代码中,你所有需要去做的就是发出服务器端请求,把返回值赋给正确的作用域(scope)变量.然后剩下的合适渲染这些数据就由模板系统去操心了.
368 |
369 | 如果你想对返回值做一些业务逻辑处理,拿着汇总方法就不能奏效了.在这种情况下,你就得依赖回调函数机制了,比如在Credit.get()调用中使用的那种机制.
370 |
371 | ###简化的服务器端操作
372 |
373 | 无论你使用资源简化函数机制还是回调函数,关于返回对象都有几点问题需要我们注意.
374 |
375 | 返回的对象不是一个普通JS对象,实际上,他是“resource”类型的对象.这就意味着对象里除了包含服务器返回的数据以外,还有一些附加的行为函数(在这个案例中如$save()和$charge函数).这样我们就可以很方便的执行服务器端操作,比如取数据、修改数据并把修改在服务器端持久化保存下来(其实也就是一般CURD应用里面的通用操作).
376 |
377 | ###对ngResource做单元测试
378 |
379 | ngResource依赖项是一个封装,它以Angular核心服务`$http`为基础.因此,你可能已经知道如何对它做单元测试了.它和我们看到的对`$http`做单元测试的样例比起来基本没什么真正的变化.你只需要知道最终的服务器端请求应该由resource发起,告诉模拟`$http`服务关于请求的信息.其他的基本都一样.下面我们来看看如何本节测试前面的代码:
380 |
381 | describe('Credit Card Resource', function(){
382 | var scope, ctrl, mockBackend;
383 | beforeEach(inject(function(_$httpBackend_, $rootScope, $controller) {
384 | mockBackend = _$httpBackend_;
385 | scope = $rootScope.$new();
386 | // Assume that CreditCard resource is used by the controller
387 | ctrl = $controller(CreditCardCtrl, {$scope: scope});
388 | }));
389 |
390 | it('should fetched list of credit cards', function() {
391 | // Set expectation for CreditCard.query() call
392 | mockBackend.expectGET('/user/123/card').
393 | respond([{id: '234', number: '11112222'}]);
394 |
395 | ctrl.fetchAllCards();
396 |
397 | // Initially, the request has not returned a response
398 | expect(scope.cards).toBeUndefined();
399 |
400 | // Tell the fake backend to return responses to all current requests
401 | // that are in flight.
402 | mockBackend.flush();
403 |
404 | // Now cards should be set on the scope
405 | expect(scope.cards).toEqualData([{id: '234', number: '11112222'}]);
406 | });
407 | });
408 |
409 | 这个测试看上去和简单的`$http`单元测试非常相似,除了一些细微区别.注意在我们的expect语句里面,取代了简单的"equals"方法,哦我们用的是特殊的"toEqualData"方法.这种eapect语句会智能地省略ngResource添加到对象上的附加方法.
410 |
411 | ##`$q`和预期值(Promise)
412 |
413 | 目前为止,我们已经看到了AngulrJS是如何实现它的异步延迟API机制.
414 |
415 | 预期值建议(Promise proposal)是AngularJS构建异步延迟API的底层基础.作为底层机制,预期值建议(Promise proposal)为异步请求做了下面这些事:
416 |
417 | + 异步请求返回的是一个预期(promise)而不是一个具体数据值.
418 | + 预期值有一个`then`函数,这个函数有两个参数,一个参数函数响应"resolved“或者"sucess"事件,另外一个参数函数响应"rejected”或者"failure"事件.这些函数以一个结果参数调用,或者以一个拒绝原因参数调用.
419 | + 确保当结果返回的时候,两个参数函数中有一个将会被调用
420 |
421 | 大多数的延迟机制和Q(详见$q API文档)是以上面这种方法实现的,AngularJS为什么这样实现具体是因为以下原因:
422 |
423 | + $q对于整个AngularJS是可见的,因此它被集成到作用域数据模型里面。这样返回数据就能快速传递,UI上的闪烁更新也就更少.
424 | + AngularJS模板也能识别$q预期值,因为预期值可以被当作结果值一样对待,而不是把它仅仅当作结果的预期.这种预期值会在响应返回时被通知提醒.
425 | + 更小的覆盖范围:AngularJS仅仅实现那些基本的、对于公共异步任务的需求来说最重要的延迟函数机制.
426 |
427 | 你也许会问这样的问题:为什么我们会做如此疯狂激进的实现机制?让我们先看一个在在异步函数使用方面的标准问题:
428 |
429 | fetchUser(function(user) {
430 | fetchUserPermissions(user, function(permissions) {
431 | fetchUserListData(user, permissions, function(list) {
432 | // Do something with the list of data that you want to display
433 | });
434 | });
435 | });
436 | 上面这个代码就是人们使用JavaScirpt时经常抱怨的令人恐惧的深层嵌套缩进椎体的噩梦.返回值异步本质与实际问题的同步需求之间产生矛盾:导致多级函数包含关系,在这种情况下要想准确跟踪里面某句代码的执行上下文环境就很难.
437 |
438 | 另外,这种情况对错误处理也有很大影响.错误处理的最好方法是什么?在每次都做错误处理?那代码结构就会非常乱.
439 |
440 | 为了解决上面这些问题,预期值建议(Promise proposal)机制提供了一个then函数的概念,这个函数会在响应成功返回的时候调用相关的函数去执行,另一方面,当产生错误的时候也会干相同的事,这样整个代码就有嵌套结构变为链式结构.所以之前那个例子用预期值API机制(至少在AngularJS中已经被实现的)改造一下,代码结构会平整许多:
441 |
442 | var deferred = $q.defer();
443 | var fetchUser = function() {
444 | // After async calls, call deferred.resolve with the response value
445 | deferred.resolve(user);
446 |
447 | // In case of error, call
448 | deferred.reject(‘Reason for failure’);
449 | }
450 | // Similarly, fetchUserPermissions and fetchUserListData are handled
451 |
452 | deferred.promise.then(fetchUser)
453 | .then(fetchUserPermissions)
454 | .then(fetchUserListData)
455 | .then(function(list) {
456 | // Do something with the list of data
457 | }, function(errorReason) {
458 | // Handle error in any of the steps here in a single stop
459 | });
460 |
461 | 那个完全的横椎体代码一下子被优雅地平整了,而且提供了链式的作用域,以及一个单点的错误处理.你在你自己的应用中处理异步请求回调时也可以用相同的代码,只要调用Angular的$q服务.这种机制可以帮我做一些很酷的事情:比如响应拦截.
462 |
463 | ##响应拦截处理
464 |
465 | 我们的讲解已经覆盖了怎样调用服务器端服务、怎样处理响应、怎样把响应优雅地抽象化封装、怎样处理异步回调.但是在真实世界的Web应用中,你最终还不得不对每个服务器端请求调用做一些通用的处理操作,比如错误处理、权限认证、以及其它考虑到安全问题的处理操作,比如对响应数据做裁剪处理(译注:有的Ajax响应为了安全需要,会添加一定约定好的噪声数据).
466 |
467 | 有着现在已经对$q API的深入理解,我们目前就可以利用响应拦截器机制来做上面所有提出过的功能.响应拦截器(正如其名)可以在响应数据被应用使用之前拦截他它,并且对它做数据转换处理,比如错误处理以及其它任何处理,包括厨房洗碗槽.(估计是指数据清洗)
468 |
469 | 让我们看一个代码例子,这个例子中的代码拦截响应,并对响应数据做了轻微的数据转换.
470 |
471 | // register the interceptor as a service
472 | myModule.factory('myInterceptor', function($q, notifyService, errorLog) {
473 | return function(promise) {
474 | return promise.then(function(response) {
475 | // Do nothing
476 | return response;
477 | }, function(response) {
478 | // My notify service updates the UI with the error message
479 | notifyService(response);
480 | // Also log it in the console for debug purposes
481 | errorLog(response);
482 | return $q.reject(response);
483 | });
484 | }
485 | });
486 |
487 | // Ensure that the interceptor we created is part of the interceptor chain
488 | $httpProvider.responseInterceptors.push('myInterceptor');
489 |
490 | ##安全方面的考虑
491 |
492 | 目前我们开发Web应用的时候,安全是一个非常重要的关注点,在我们的考虑维度直中,它必须作为首位被考虑.AngularJS给我们提供了一些帮助,同时也带来了两个安全攻击的角度,下面这一节我们将会讲解这些内容.
493 |
494 | ###JSON的安全脆弱性
495 |
496 | 当我们对服务器发送一个请求JSON数组数据的GET请求时(特别是当这些数据是敏感数据且需要登录验证或读取授权时),就会有一个不易察觉的JSON安全漏洞被暴露出来.
497 |
498 | 当我们使用一个恶意站点时,站点可能会用\
304 | ```
305 | 这里的id属性很重要, 因为这是Angular用来存储模板的URL键. 稍候你将会使用这个id在指令的`templateUrl`中指定要插入的模板.
306 |
307 | 这个版本能够很好的载入而不需要服务器, 因为没有必要的`XMLHttpRequest`来获取内容.
308 |
309 | 最后, 你可以越过`$http`或者以其他机制来加载你自己的模板, 然后将它们直接设置在Angular中称为`$templateCache`的对象上. 我们希望在指令运行之前缓存中的这个模板可用, 因此我们将通过module上的run函数来调用它.
310 | ```js
311 | var appModule = angular.module('app', []);
312 |
313 | appModule.run(function($templateCache){
314 | $templateCache.put('helloTemplateCached.html', 'Hi there
');
315 | });
316 |
317 | appModule.directive('hello', function(){
318 | return {
319 | restrict: 'E',
320 | templateUrl: 'helloTemplateCached.html',
321 | replace: true;
322 | };
323 | });
324 | ```
325 | 你可能希望在产品中这么做, 仅仅作为一个减少所需的GET请求数量的技术. 你可以运行一个脚本将所有的模板合并到一个单独的文件中, 并在一个新的模块中加载它, 然后你就可以从你的主应用程序模块中引用它.
326 |
327 | ####Transclusion
328 |
329 | 除了替换或者追加内容, 你还可以通过`transclude`属性将原来的内容移到新模板中. 当设置为true时, 指令将删除原来的内容, 但是在你的模板中通过一个名为`ng-transclude`的指令重新插入来使它可用.
330 |
331 | 我们可以使用transclusion来改变我们的示例:
332 | ```js
333 | appModule.directive('hello', function() {
334 | return {
335 | template: 'Hi there
',
336 | transclude: true
337 | };
338 | });
339 | ```
340 | 像这样来应用它:
341 | ```html
342 | Bob
343 | ```
344 | 你会看到: "Hi there Bob."
345 |
346 | ###编译和链接功能
347 |
348 | 虽然插入模板是有用的, 任何指令真正有趣的工作发生在它的`compile`和它的`link`函数中.
349 |
350 | `compile`和`link`函数被指定为Angular用来创建应用程序实际视图的后两个阶段. 让我们从更高层次来看看Angular的初始化过程, 按一定的顺序:
351 |
352 | **Script loads**
353 |
354 | >Angular加载和查找`ng-app`指令来判定应用程序界限.
355 |
356 | **Compile phase(阶段)**
357 |
358 | >在这个阶段, Angular会遍历DOM节点以确定所有注册在模板中的指令. 对于每一个指令, >然后基于指令的规则(`template`,`replace`,`transclude`等等)转换DOM, 并且如果它存在就调用`compile`函数. >它的返回结果是一个编译过的`template`函数, 这将从所有的指令中调用`link`函数来收集.
359 |
360 | **Link phase(阶段)**
361 |
362 | >创建动态的视图, 然后Angular会对每个指令运行一个`link`函数. `link`函数通常在DOM或者模型上创建监听器. >这些监听器用于视图和模型在所有的时间里都保持同步.
363 |
364 | 因此我们必须在编译阶段处理模板的转换, 同时在链接阶段处理在视图中修改数据. 按照这个思路, 指令中的`compile`和`link`函数之间主要的区别是`compile`函数处理模板自身的转换, 而`link`函数处理在模型和视图之间创造一个动态的连接. 作用域挂接到编译过的`link`函数正是在这个第二阶段, 并且通过数据绑定将指令变成活动的.
365 |
366 | 出于性能的考虑, 这两个阶段才分开的. `compile`函数仅在编译阶段执行一次, 而`link`函数会被执行多次, 对每个指令实例. 例如, 让我们来说说你上面使用的`ng-repeat`指令. 你并不想用`compile`, 这会导致在每次`ng-repeat`重复时都产生一个DOM遍历的操作. 相反, 你会希望一次编译, 然后链接.
367 |
368 | 虽然你毫无疑问的应该学习编译和链接之间的不同, 以及每个功能, 你需要编写的大部分的指令都不需要转换模板; 你还会编写大部分的链接函数.
369 |
370 | 让我们再看看每个语法来比较一下, 我们有:
371 | ```js
372 | compile: function compile(tElement, tAttrs, transclude) {
373 | return {
374 | pre: function preLink(scope, iElement, iAttrs, controller) {...},
375 | post: function postLink(scope, iElement, iAttrs, controller) {...}
376 | }
377 | }
378 | ```
379 | 以及链接:
380 | ```js
381 | link: function postLink(scope, iElement, iAttrs) {...}
382 | ```
383 | 注意这里有一点不同的是`link`函数获得了一个作用域的访问, 而`compile`没有. 这是因为在编译阶段期间, 作用域并不存在. 然而你有能力从`compile`函数返回`link`函数. 这些`link`函数能够访问到作用域.
384 |
385 | 还要注意的是`compile`和`link`都会获得一个到它们对应的DOM元素和这些元素属性[attributes]列表的引用. 这里的一点区别是`compile`函数是从模板中获得模板元素和属性, 并且会获取到`t`前缀. 而`link`函数使用模板创建的视图实例中获得它们的, 它们会获取到`i`前缀.
386 |
387 | 这种区别只存在于当指令位于其他指令中制造模板副本的时候. `ng-repeat`就是一个很好的例子.
388 | ```html
389 |
390 |
391 |
392 | ```
393 | 这里, `compile`函数将只被调用一次, 而`link`函数在每次复制`my-widget`时都会被调用一次--等价于元素在things中的数量. 因此, 如果`my-widget`需要到所有`my-widget`副本(实例)中修改一些公共的东西, 为了提升效率, 正确的做法是在`compile`函数中处理.
394 |
395 | 你可能还会注意到`compile`函数好哦的了一个`transclude`属性函数. 这里, 你还有机会以编写一个函数以编程的方式transcludes内容, 对于简单的的基于模板不足以transclusion的情况.
396 |
397 | 最后, `compile`可以返回一个`preLink`和`postLink`函数, 而`link`仅仅指向一个`postLink`函数. `preLink`, 正如它的名字所暗示的, 它运行在编译阶段之后, 但是会在指令链接到子元素之前. 同样的, `postLink`会运行在所有的子元素指令被链接之后. 这意味着如果你需要改变DOM结构, 你将在`posyLink`中处理. 在`preLink`中处理将会混淆流程并导致一个错误.
398 |
399 | ###作用域
400 |
401 | 你会经常希望从指令中访问作用域来监控模型的值并在它们改变时更新UI, 同时在外部时间造成模型改变时通知Angular. 者时最常见的, 当你从jQuery, Closure或者其他库中包裹一些非Angular组件或者实现简单的DOM事件时. 然后将Angular表达式作为属性传递到你的指令中来执行.
402 |
403 | 这也是你期望使用一个作用域的原因之一, 你可以获得三种类型的作用域选项:
404 |
405 | 1. 从指令的DOM元素中获得**现有的作用域**.
406 | 2. 创建一个**新作用域**, 它继承自你闭合的控制器作用域. 这里, 你见过能够访问树上层作用域中的所有值. 这个作用域将会请求这种作用域与你DOM元素中其他任意指令共享它并被用于与它们通信.
407 | 3. 从它的父层**隔离出来的作用域**不带有模型属性. 当你在创建可重用的组件而需要从父作用域中隔离指令操作时, 你将会希望使用这个选项.
408 |
409 | 你可以使用下面的语法来创建这些作用域类型的配置:
410 | ```html
411 |
412 |
413 |
414 | | Scope Type |
415 | Syntax |
416 |
417 |
418 |
419 |
420 | | existing scope |
421 | scope: false(如果不指定将使用这个默认值)
422 | |
423 |
424 | | new scope |
425 | scope: true |
426 |
427 |
428 | | isolate scope |
429 | scope: { /* attribute names and binding style */ } |
430 |
431 |
432 |
433 | ```
434 | 当你创建一个隔离的作用域时, 默认情况下你不需要访问父作用域中模型中的任何东西. 然而, 你也可以指定你想要的特定属性传递到你的指令中. 你可以认为是吧这些属性名作为参数传递给函数的.
435 |
436 | 注意, 虽然隔离的作用域不就成模型属性, 但它们仍然是其副作用域的成员. 就像所有其他作用域一样, 它们都有一个`$parent`属性引用到它们的父级.
437 |
438 | 你可以通过传递一个指令属性名的映射的方式从父作用域传递特定的属性到隔离的作用域中. 这里有三种合适的方式从父作用域中传递数据. 我们称这些传递数据不同的方式为"绑定策略". 你也可以可选的指定一个局部别名给属性名称.
439 |
440 | 以下是没有别名的语法:
441 | ```js
442 | scope: {
443 | attributeName1: 'BINDING_STRATEGY',
444 | attributeName2: 'BINDING_STRATEGY',...
445 | }
446 | ```
447 | 以下是使用别名的方式:
448 | ```js
449 | scope: {
450 | attributeAlias: 'BINDING_STRATEGY' + 'templateAttributeName',...
451 | }
452 | ```
453 | 绑定策略被定义为表6-4中的符号:
454 |
455 | 表6-4 绑定策略
456 | ```html
457 |
458 |
459 |
460 | | Symbol |
461 | Meaning
462 | |
463 |
464 |
465 |
466 | | @ |
467 | 将属性作为字符串传递. 你也可以通过在属性值中使用插值符号{{}}来从闭合的作用域中绑定数据值. |
468 |
469 |
470 | | = |
471 | 使用你的指令的副作用域中的一个属性绑定数据到属性中. |
472 |
473 |
474 | | & |
475 | 从父作用域中传递到一个函数中, 以后调用. |
476 |
477 |
478 |
479 | ```
480 | 这些都是相当抽象的概念, 因此让我们来看一个具体的例子上的变化来进行说明. 比方说我们希望创建一个`expander`指令在标题栏被点击时显示额外的内容.
481 |
482 | 收缩时它看起来如图6-2所示.
483 |
484 | 
485 |
486 | 图6-2 Expander in closed state
487 |
488 | 展开时它看起来如图6-3所示.
489 |
490 | 
491 |
492 | 图6-3 Expander in open state
493 |
494 | 我们会编写如下代码:
495 | ```html
496 |
497 |
498 | {{text}}
499 |
500 |
501 | ```
502 | 标题(Cliked me to expand)和文本(Hi there folks...)的值来自于闭合的作用域中. 我们可以像下面这样来设置一个控制器:
503 | ```js
504 | function SomeController($scope) {
505 | $scope.title = 'Clicked me to expand';
506 | $scope.text = 'Hi there folks, I am the content that was hidden but is now shown.';
507 | }
508 | ```
509 | 然后我们可以来编写指令:
510 | ```js
511 | angular.module('expanderModule', [])
512 | .directive('expander', function(){
513 | return {
514 | restrict: 'EA',
515 | replace: true,
516 | transclude: true,
517 | scope: { title: '=expanderTitle'},
518 | template: '' +
519 | '
{{title}}
' +
520 | '
' +
521 | '
',
522 | link: function(scope, element, attris){
523 | scope.showMe = false;
524 | scope.toggle = function toggle(){
525 | scope.showMe = !scope.showMe;
526 | }
527 | }
528 | }
529 | });
530 | ```
531 | 然后编写下面的样式:
532 | ```css
533 | .expander {
534 | border: 1px solid black;
535 | width: 250px;
536 | }
537 | .expander > .title {
538 | background-color: black;
539 | color: white;
540 | padding: .1em .3em;
541 | cursor: pointer;
542 | }
543 | .expander > .body {
544 | padding: .1em .3em;
545 | }
546 | ```
547 | 接下来让我们来看看指令中的每个选项是做什么的, 在表6-5中.
548 |
549 | 表6-5 Functions of elements
550 | ```html
551 |
552 |
553 |
554 | | FunctionName |
555 | Description |
556 |
557 |
558 |
559 |
560 | | restrict: EA |
561 | 一个元素或者属性都可以调用这个指令. 也就是说, \...\与\ ...\ 是等价 |
562 |
563 |
564 | | replace:true |
565 | 使用我们提供的模板替换原始元素 |
566 |
567 |
568 | | transclude:true |
569 | 将原始元素的内容移动到我们所提供的模板的另外一个位置. |
570 |
571 |
572 | | scope: {title: =expanderTitle} |
573 | 创建一个称为`title`的局部作用域, 将父作用域的属性数据绑定到声明的`expanderTitle`属性中. 这里, 我们重命名title为更方便的expanderTitle. 我们可以编写`scope: { expanderTitle: '='}`, 那么在模板中我们就要使用`expanderTitle`了. 但是在其他指令也有一个`title`属性的情况下, 在API中消除title的歧义和只是重命名它用于在局部使用是有意义的. 请注意, 这里自定义指令也使用了相同的驼峰式命名方式作为指令名. |
574 |
575 |
576 | | template: \<'div'\>+ |
577 | 声明这个指令要插入的模板. 注意我们使用了`ng-click`和`ng-show`来显示和隐藏自身并使用`ng-transclude`声明了原始内容会去哪里. 还要注意的是transcluded的内容能够访问父作用域, 而不是指令闭合中的作用域. |
578 |
579 |
580 | | link... |
581 | 设置`showMe`模型来检测expander的展开/关闭状态, 同时定义在用于点击`title`这个div的时候调用定义的`toggle()`函数. |
582 |
583 |
584 |
585 | ```
586 | 如果我们像使用更多有意义的东西来在模板中定义`expander title`而不是在模型中, 我们还可以使用传递通过在作用域声明中使用`@`符号传递一个字符串风格的属性, 就像下面这样:
587 | ```js
588 | scope: { title: '@expanderTitle'},
589 | ```
590 | 在模板中我们就可以实现相同的效果:
591 | ```html
592 |
593 | {{text}}
594 |
595 | ```
596 | 注意, 对于@策略我们仍然可以通过使用插入法将title数据绑定到我们的控制器作用域中:
597 | ```html
598 |
599 | {{text}}
600 |
601 | ```
602 | ###操作DOM元素
603 |
604 | 传递给指令的`link`和`compile`函数的`iElement`和`tElement`是包裹原生DOM元素的引用. 如果你已经加载了jQuery库, 你也可以使用你已经习惯使用的jQuery元素.
605 |
606 | 如果你没有使用jQuery, 你也可以使用Angular内置的被称为jqLite的包装器. 它提供了一个jQuery的子集便于我们在Angular中创建任何东西. 对于多数应用程序, 你都可以单独使用这些API做任何你需要做的事情.
607 |
608 | 如果你需要直接访问原生的DOM元素你可以通过使用`element[0]`访问对象的第一个元素来获得它.
609 |
610 | 你可以在Angular文档的`angular.element()`查看它所支持的API的完整列表--你可以用这个函数创建你自己的jqLite包装的DOM元素. 它包含像`addClass()`, `bind()`, `find()`, `toggleClass()`等等其他方法. 其次, 其中大多数有用的核心方法都来自于jQuery, 但是它的代码亮更少.
611 |
612 | 对于其他的jQuery API, 元素在Angular中都有指定的函数. 这些都是存在的, 无论你是否使用完整的jQuery库.
613 |
614 | Table 6-6. Angular specific functions on an element
615 | ```html
616 |
617 |
618 |
619 | | Function |
620 | Description |
621 |
622 |
623 |
624 |
625 | | controller(name) |
626 | 当你需要直接与一个控制器通信时, 这个函数会返回附加到元素上的控制器. 如果没有现有的元素, 它会遍历DOM元素并查找最近的父控制器. name参数是可选的, 它是用于指定相同元素上其他指令名称的. 如果提供这个参数, 它会从相应的指令中返回控制器. 这个名字应该与所有指令一样使用一个驼峰式的格式. 也就是说, 使用`ngModle`来替换`ng-model`的形式. |
627 |
628 |
629 | | injector() |
630 | 获取当前元素或者它的父元素的注入器. 它还允许你访问在这些元素上定义的所依赖的模块. |
631 |
632 |
633 | | scope() |
634 | 返回当前元素或者它最近的父元素的作用域. |
635 |
636 |
637 | | inheritedData() |
638 | 正如jQuery的`data()`函数, `inheritedData()`会在一个封闭的方式中设置和获取数据. 此外还能够从当前元素获取数据, 它也会遍历DOM元素并查找值. |
639 |
640 |
641 |
642 | ```
643 | 这里有一个例子, 让我们重新定义之前的expander例子而不使用`ng-show`和`ng-click`. 它看起来像下面这样:
644 | ```js
645 | angular.module('expanderModule', [])
646 | .directive('expander', function(){
647 | return {
648 | restrict: 'EA',
649 | replace: true,
650 | transclude: true,
651 | scope: { title: '=expanderTitle' },
652 | template: '' +
653 | '
{{title}}
' +
654 | '
' +
655 | '
',
656 |
657 | link: function(scope, element, attrs) {
658 | var titleElement = angular.element(element.children().eq(0));
659 | var bodyElement = angular.element(element.children().eq(1));
660 |
661 | titleElement.bind('click', toggle);
662 |
663 | function toggle() {
664 | bodyElement.toggleClass('closed');
665 | }
666 | }
667 | }
668 | });
669 | ```
670 | 这里我们从模板中移除了`ng-click`和`ng-show`. 相反的时, 当用户单击expander的title时执行所定义的行为, 我们将从title元素创建一个jqLite元素, 然后它绑定一个click事件并将`toggle()`函数作为它的回调函数. 在`toggle()`函数中, 我们在expander的body元素上调用`toggleClass()`来添加或者移除一个被称为`closed`的class(HTML类名), 这里我们给元素设置了一个值为`display: none`的类, 像下面这样:
671 | ```css
672 | .closed {
673 | display: none;
674 | }
675 | ```
676 |
677 | ###控制器
678 |
679 | 当你有相互嵌套的指令需要相互通信时, 你可以通过控制器做到这一点. 比如一个\