`
zzc1684
  • 浏览: 1188673 次
  • 性别: Icon_minigender_1
  • 来自: 广州
文章分类
社区版块
存档分类
最新评论

AngularJS学习笔记

阅读更多

 

http://www.360doc.com/content/13/0729/13/13328522_303333441.shtml

 

AngularJS学习笔记

  • 2013-07-19 18:33 更新
  • 邹业盛
  1. 关于AngularJS
  2. 开始的例子
  3. 依赖注入
  4. 作用域
  5. 数据绑定与模板
  6. 模板
  7. 模板中的过滤器
  8. 锚点路由
  9. 定义模板变量标识标签
  10. AJAX
  11. 工具函数
  12. 其它服务
  13. 自定义模块和服务
  14. 附加模块 ngResource
  15. AngularJS与其它框架的混用(jQuery, Dojo)

1. 关于AngularJS

AngularJS 是 Google 开源出来的一套 js 工具。下面简称其为 ng 。这里只说它是“工具”,没说它是完整的“框架”,是因为它并不是定位于去完成一套框架要做的事。更重要的,是它给我们揭示了一种新的应用组织与开发方式。

ng 最让我称奇的,是它的数据双向绑定。其实想想,我们一直在提数据与表现的分离,但是这里的“双向绑定”从某方面来说,是把数据与表现完全绑定在一起——数 据变化,表现也变化。反之,表现变化了,内在的数据也变化。有过开发经验的人能体会到这种机制对于前端应用来说,是很有必要的,能带来维护上的巨大优势。 当然,这里的绑定与提倡的分离并不是矛盾的。

ng 可以和 jQuery 集成工作,事实上,如果没有 jQuery , ng 自己也做了一个轻量级的 jQuery ,主要实现了元素操作部分的 API 。

关于 ng 的几点:

  • 对 IE 方面,它兼容 IE8 及以上的版本。
  • 与 jQuery 集成工作,它的一些对象与 jQuery 相关对象表现是一致的。
  • 使用 ng 时不要冒然去改变相关 DOM 的结构。

2. 开始的例子

我们从一个完整的例子开始认识 ng :

 1   <!DOCTYPE html>
 2   <html>
 3   <head>
 4   <meta charset="utf-8" />
 5   
 6   <title>试验</title>
 7   
 8   <script type="text/javascript" src="jquery-1.8.3.js"></script>
 9   <script type="text/javascript" src="angular.js"></script>
10   
11   </head>
12   <body>
13     <div ng-controller="BoxCtrl">
14       <div style="width: 100px; height: 100px; background-color: red;"
15            ng-click="click()"></div>
16       <p>{{ w }} x {{ h }}</p>
17       <p>W: <input type="text" ng-model="w" /></p>
18       <p>H: <input type="text" ng-model="h" /></p>
19     </div>
20   
21   
22   <script type="text/javascript" charset="utf-8">
23   
24   
25   var BoxCtrl = function($scope, $element){
26   
27     //$element 就是一个 jQuery 对象
28     var e = $element.children().eq(0);
29     $scope.w = e.width();
30     $scope.h = e.height();
31   
32     $scope.click = function(){
33       $scope.w = parseInt($scope.w) + 10;
34       $scope.h = parseInt($scope.h) + 10;
35     }
36   
37     $scope.$watch('w',
38       function(to, from){
39         e.width(to);
40       }
41     );
42   
43     $scope.$watch('h',
44       function(to, from){
45         e.height(to);
46       }
47     );
48   }
49   
50   angular.bootstrap(document.documentElement);
51   </script>
52   </body>
53   </html>

从上面的代码中,我们看到在通常的 HTML 代码当中,引入了一些标记,这些就是 ng 的模板机制,它不光完成数据渲染的工作,还实现了数据绑定的功能。

同时,在 HTML 中的本身的 DOM 层级结构,被 ng 利用起来,直接作为它的内部机制中,上下文结构的判断依据。比如例子中 pdiv 的子节点,那么 p 中的那些模板标记就是在 divCtrl 的作用范围之内。

其它的,也同样写一些 js 代码,里面重要的是作一些数据的操作,事件的绑定定义等。这样,数据的变化就会和页面中的 DOM 表现联系起来。一旦这种联系建立起来,也即完成了我们所说的“双向绑定”。然后,这里说的“事件”,除了那些“点击”等通常的 DOM 事件之外,我们还更关注“数据变化”这个事件。

最后,可以使用:

  angular.bootstrap(document.documentElement);

来把整个页面驱动起来了。(你可以看到一个可被控制大小的红色方块)

更完整的方法是定义一个 APP :

 1   <!DOCTYPE html>
 2   <html ng-app="MyApp">
 3   <head>
 4   <meta charset="utf-8" />
 5   
 6   <title>数据正向绑定</title>
 7   
 8   <script type="text/javascript" src="jquery-1.8.3.js"></script>
 9   <script type="text/javascript" src="angular.js"></script>
10   
11   </head>
12   <body>
13   
14   <div ng-controller="TestCtrl">
15     <input type="text" value="" id="a" />
16   </div>
17   
18   
19   <script type="text/javascript">
20   var TestCtrl = function(){
21     console.log('ok');
22   }
23   
24   //angular.bootstrap(document.documentElement);
25   angular.module('MyApp', [], function(){console.log('here')});
26   </script>
27   
28   </body>
29   </html>

这里说的一个 App 就是 ng 概念中的一个 Module 。对于 Controller 来说, 如果不想使用全局函数,也可以在 app 中定义:

  var app = angular.module('MyApp', [], function(){console.log('here')});
  app.controller('TestCtrl',
    function($scope){
      console.log('ok');
    }
  );

3. 依赖注入

injector , 我从 ng 的文档中得知这个概念,之后去翻看源码时了解了一下这个机制的工作原理。感觉就是虽然与自己的所想仅差那么一点点,但就是这么一点点,让我感慨想象力之神奇。

先看我们之前代码中的一处函数定义:

  var BoxCtrl = function($scope, $element){}

在这个函数定义中,注意那两个参数: $scope$element ,这是两个很有意思的东西。总的来说,它们是参数,这没什么可说的。但又不仅仅是参数——你换个名字代码就不能正常运行了。

事实上,这两个参数,除了完成“参数”的本身任务之外,还作为一种语法糖完成了“依赖声明”的任务。本来这个函数定义,完整的写法应该像 AMD 声明一样,写成:

  var BoxCtrl = ['$scope', '$element', function(s, e){}];

这样就很明显,表示有一个函数,它依赖于两个东西,然后这两个东西会依次作为参数传入。

简单起见,就写成了一个函数定义原本的样子,然后在定义参数的名字上作文章,来起到依赖声明的作用。

在处理时,通过函数对象的 toString() 方法可以知道这个函数定义代码的字符串表现形式,然后就知道它的参数是 $scope$element 。通过名字判断出这是两个外部依赖,然后就去获取资源,最后把资源作为参数,调用定义的函数。

所以,参数的名字是不能随便写的,这里也充分利用了 js 的特点来尽量做到“反省”了。

在 Python 中受限于函数名的命名规则,写出来不太好看。不过也得利于反省机制,做到这点也很容易:

  # -*- coding: utf-8 -*-
  
  def f(Ia, Ib):
      print Ia, Ib
  
  args = f.func_code.co_varnames
  SRV_MAP = {
      'Ia': '123',
      'Ib': '456',
  }
  
  srv = {}
  for a in args:
      if a in SRV_MAP:
          srv[a] = SRV_MAP[a]
  f(**srv)

4. 作用域

这里提到的“作用域”的概念,是一个在范围上与 DOM 结构一致,数据上相对于某个 $scope 对象的属性的概念。我们还是从 HTML 代码上来入手:

  <div ng-controller="BoxCtrl">
    <div style="width: 100px; height: 100px; background-color: red;"
         ng-click="click()">
    </div>
    <p>{{ w }} x {{ h }}</p>
    <p>W: <input type="text" ng-model="w" /></p>
    <p>H: <input type="text" ng-model="h" /></p>
  </div>

上面的代码中,我们给一个 div 元素指定了一个 BoxCtrl ,那么, div 元素之内,就是 BoxCtrl 这个函数运行时, $scope 这个注入资源的控制范围。在代码中我们看到的 click()wh 这些东西,它们本来的位置对应于 $scope.click$scope.w$scope.h

我们在后面的 js 代码中,也可以看到我们就是在操作这些变量。依赖于 ng 的数据绑定机制,操作变量的结果直接在页面上表现出来了。

5. 数据绑定与模板

我纠结了半天,“数据绑定”与“模板”这两个东西还真没办法分开来说。因为数据绑定需要以模板为载体,离开了模板,数据还绑个毛啊。

ng 的一大特点,就是数据双向绑定。双向绑定是一体,为了描述方便,下面分别介绍。

5.1. 数据->模板

数据到表现的绑定,主要是使用模板标记直接完成的:

  <p>{{ w }} x {{ h }}</p>

使用 {{ }} 这个标记,就可以直接引用,并绑定一个作用域内的变量。在实现上, ng 自动创建了一个 watcher 。效果就是,不管因为什么,如果作用域的变量发生了改变,我们随时可以让相应的页面表现也随之改变。我们可以看一个更纯粹的例子:

  <p id="test" ng-controller="TestCtrl">{{ a }}</p>
  
  <script type="text/javascript">
  var TestCtrl = function($scope){
    $scope.a = '123';
  }
  angular.bootstrap(document.documentElement);

上面的例子在页面载入之后,我们可以在页面上看到 123 。这时,我们可以打开一个终端控制器,输入:

  $('#test').scope().a = '12345';
  $('#test').scope().$digest();

上面的代码执行之后,就可以看到页面变化了。

对于使用 ng 进行的事件绑定,在处理函数中就不需要去关心 $digest() 的调用了。因为 ng 会自己处理。源码中,对于 ng 的事件绑定,真正的处理函数不是指定名字的函数,而是经过 $apply() 包装过的一个函数。这个 $apply() 做的一件事,就是调用根作用域 $rootScope$digest() ,这样整个世界就清净了:

  <p id="test" ng-controller="TestCtrl" ng-click="click()">{{ a }}</p>
  
  <script type="text/javascript" charset="utf-8">
  var TestCtrl = function($scope){
    $scope.a = '123';
  
    $scope.click = function(){
      $scope.a = '456';
    }
  }
  angular.bootstrap(document.documentElement);

那个 click 函数的定义,绑定时变成了类似于:

  function(){
    $scope.$apply(
      function(){
        $scope.click();
      }
    )
  }

这里的 $scope.$apply() 中做的一件事:

  $rootScope.$digest();

5.2. 模板->数据

模板到数据的绑定,主要是通过 ng-model 来完成的:

  <input type="text" id="test" ng-controller="TestCtrl" ng-model="a" />
  
  <script type="text/javascript" charset="utf-8">
  var TestCtrl = function($scope){
    $scope.a = '123';
  }

这时修改 input 中的值,然后再在控制终端中使用:

  $('#test').scope().a

查看,发现变量 a 的值已经更改了。

实际上, ng-model 是把两个方向的绑定都做了。它不光显示出变量的值,也把显示上的数值变化反映给了变量。这个在实现上就简单多了,只是绑定 change 事件,然后做一些赋值操作即可。不过 ng 里,还要区分对待不同的控件。

5.3. 数据->模板->数据->模板

现在要考虑的是一种在现实中很普遍的一个需求。比如就是我们可以输入数值,来控制一个矩形的长度。在这里,数据与表现的关系是:

  • 长度数值保存在变量中
  • 变量显示于某个 input 中
  • 变量的值即是矩形的长度
  • input 中的值变化时,变量也要变化
  • input 中的值变化时,矩形的长度也要变化

当然,要实现目的在这里可能就不止一种方案了。按照以前的做法,很自然地会想法,绑定 inputchange 事件,然后去做一些事就好了。但是,我们前面提到过 ng-model 这个东西,利用它就可以在不手工处理 change 的条件下完成数据的展现需求,在此基础之上,我们还需要做的一点,就是把变化后的数据应用到矩形的长度之上。

最开始,我们面对的应该是这样一个东西:

  <div ng-controller="TestCtrl">
    <div style="width: 100px; height: 10px; background-color: red"></div>
    <input type="text" name="width" ng-model="width" />
  </div>
  
  <script type="text/javascript" charset="utf-8">
  var TestCtrl = function($scope){
    $scope.width = 100;
  }
  angular.bootstrap(document.documentElement);
  </script>

我们从响应数据变化,但又不使用 change 事件的角度来看,可以这样处理宽度变化:

  var TestCtrl = function($scope, $element){
    $scope.width = 100;
    $scope.$watch('width',
      function(to, from){
        $element.children(':first').width(to);
      }
    );
  }

使用 $watch() 来绑定数据变化。

当然,这种样式的问题,有更直接有效的手段, ng 的数据绑定总是让人惊异:

  <div ng-controller="TestCtrl">
    <div style="width: 10px; height: 10px; background-color: red" ng-style="style">
    </div>
    <input type="text" name="width" ng-model="style.width" />
  </div>
  
  
  <script type="text/javascript" charset="utf-8">
  var TestCtrl = function($scope){
    $scope.style = {width: 100};
  }
  angular.bootstrap(document.documentElement);
  </script>

6. 模板

前面讲了数据绑定之后,现在可以单独讲讲模板了。

作为一套能称之谓“模板”的系统,除了能干一些模板的常规的事之外(好吧,即使是常规的逻辑判断现在它也做不了的),配合作用域 $scope 和 ng 的数据双向绑定机制, ng 的模板系统就变得比较神奇了。

6.1. 定义模板内容

定义模板的内容现在有三种方式:

  1. 在需要的地方直接写字符串
  2. 外部文件
  3. 使用 script 标签定义的“内部文件”

第一种不需要多说。第二种和第三种都可以和 ng-include 一起工作,来引入一段模板。

直接引入同域的外部文件作为模板的一部分:

  <div ng-include src="'tpl.html'">
  </div>
  
  <div ng-include="'tpl.html'">
  </div>

注意, src 中的字符串会作为表达式处理(可以是 $scope 中的变量),所以,直接写名字的话需要使用引号。

引入 script 定义的“内部文件”:

  <script type="text/ng-template" id="tpl">
  here, {{ 1 + 1 }}
  </script>
  
  <div ng-include src="'tpl'"></div>
  

配合变量使用:

  <script type="text/ng-template" id="tpl">
  here, {{ 1 + 1 }}
  </script>
  
  <a ng-click="v='tpl'">Load</a>
  <div ng-include src="v"></div>

6.2. 内容渲染控制

6.2.1. 重复 ng-repeat

这算是唯一的一个控制标签么……,它的使用方法类型于:

  <div ng-controller="TestCtrl">
    <ul ng-repeat="member in obj_list">
      <li>{{ member }}</li>
    </ul>
  </div>
  
  
  var TestCtrl = function($scope){
    $scope.obj_list = [1,2,3,4];
  }

除此之外,它还提供了几个变量可供使用:

  • $index 当前索引
  • $first 是否为头元素
  • $middle 是否为非头非尾元素
  • $last 是否为尾元素
  <div ng-controller="TestCtrl">
    <ul ng-repeat="member in obj_list">
      <li>{{ $index }}, {{ member.name }}</li>
    </ul>
  </div>
  
  var TestCtrl = function($scope){
    $scope.obj_list = [{name: 'A'}, {name: 'B'}, {name: 'C'}];
  }

6.2.2. 赋值 ng-init

这个指令可以在模板中直接赋值,它作用于 angular.bootstrap 之前,并且,定义的变量与 $scope 作用域无关。

  <div ng-controller="TestCtrl" ng-init="a=[1,2,3,4];">
    <ul ng-repeat="member in a">
      <li>{{ member }}</li>
    </ul>
  </div>

6.3. 节点控制

6.3.1. 样式 ng-style

可以使用一个结构直接表示当前节点的样式:

  <div ng-style="{width: 100, height: 100, backgroundColor: 'red'}">
  </div>

同样地,绑定一个变量的话,威力大了。

6.3.2. 类 ng-class

就是直接地设置当前节点的类,同样,配合数据绑定作用就大了:

  <div ng-controller="TestCtrl" ng-class="cls">
  </div>

ng-class-evenng-class-odd 是和 ng-repeat 配合使用的:

  <ul ng-init="l=[1,2,3,4]">
    <li ng-class-odd="'odd'" ng-class-even="'even'" ng-repeat="m in l">{{ m }}</li>
  </ul>

注意里面给的还是表示式,别少了引号。

6.3.3. 显示和隐藏 ng-show ng-hide ng-switch

前两个是控制 display 的指令:

  <div ng-show="true">1</div>
  <div ng-show="false">2</div>
  <div ng-hide="true">3</div>
  <div ng-hide="false">4</div>

后一个 ng-switch 是根据一个值来决定哪个节点显示,其它节点移除:

  <div ng-init="a=2">
    <ul ng-switch on="a">
      <li ng-switch-when="1">1</li>
      <li ng-switch-when="2">2</li>
      <li ng-switch-default>other</li>
    </ul>
  </div>

6.3.4. 其它属性控制

ng-src 控制 src 属性:

  <img ng-src="{{ 'h' + 'ead.png' }}" />

ng-href 控制 href 属性:

  <a ng-href="{{ '#' + '123' }}">here</a>

总的来说:

  • ng-src src属性
  • ng-href href属性
  • ng-checked 选中状态
  • ng-selected 被选择状态
  • ng-disabled 禁用状态
  • ng-multiple 多选状态
  • ng-readonly 只读状态

注意: 上面的这些只是单向绑定,即只是从数据到展示,不能反作用于数据。要双向绑定,还是要使用 ng-model

6.4. 事件绑定

事件绑定是模板指令中很好用的一部分。我们可以把相关事件的处理函数直接写在 DOM 中,这样做的最大好处就是可以从 DOM 结构上看出业务处理的形式,你知道当你点击这个节点时哪个函数被执行了。

  • ng-change
  • ng-click
  • ng-dblclick
  • ng-mousedown
  • ng-mouseenter
  • ng-mouseleave
  • ng-mousemove
  • ng-mouseover
  • ng-mouseup
  • ng-submit

对于事件对象本身,在函数调用时可以直接使用 $event 进行传递:

  <p ng-click="click($event)">点击</p>
  <p ng-click="click($event.target)">点击</p>

6.5. 表单控件

表单控件类的模板指令,最大的作用是它预定义了需要绑定的数据的格式。这样,就可以对于既定的数据进行既定的处理。

6.5.1. form

form 是核心的一个控件。 ng 对 form 这个标签作了包装。事实上, ng 自己的指令是叫 ng-form 的,区别在于, form 标签不能嵌套,而使用 ng-form 指令就可以做嵌套的表单了。

form 的行为中依赖它里面的各个输入控制的状态的,在这里,我们主要关心的是 form 自己的一些方法和属性。从 ng 的角度来说, form 标签,是一个模板指令,也创建了一个 FormController 的实例。这个实例就提供了相应的属性和方法。同时,它里面的控件也是一个 NgModelController 实例。

很重要的一点, form 的相关方法要生效,必须为 form 标签指定 nameng-controller ,并且每个控件都要绑定一个变量。 form 和控件的名字,即是 $scope 中的相关实例的引用变量名。

  <form name="test_form" ng-controller="TestCtrl">
    <input type="text" name="a" required ng-model="a"  />
    <span ng-click="see()">{{ test_form.$valid }}</span>
  </form>
  
  var TestCtrl = function($scope){
  
    $scope.see = function(){
      console.log($scope.test_form);
      console.log($scope.test_form.a);
    }
  
  }

除去对象的方法与属性, form 这个标签本身有一些动态类可以使用:

  • ng-valid 当表单验证通过时的设置
  • ng-invalid 当表单验证失败时的设置
  • ng-pristine 表单的未被动之前拥有
  • ng-dirty 表单被动过之后拥有

form 对象的属性有:

  • $pristine 表单是否未被动过
  • $dirty 表单是否被动过
  • $valid 表单是否验证通过
  • $invalid 表单是否验证失败
  • $error 表单的验证错误

其中的 $error 对象包含有所有字段的验证信息,及对相关字段的 NgModelController 实例的引用。它的结构是一个对象, key 是失败信息, requiredminlength 之类的, value 是对应的字段实例列表。

注意,这里的失败信息是按序列取的一个。比如,如果一个字段既要求 required ,也要求 minlength ,那么当它为空时, $error 中只有 required 的失败信息。只输入一个字符之后, required 条件满足了,才可能有 minlength 这个失败信息。

  <form name="test_form" ng-controller="TestCtrl">
    <input type="text" name="a" required ng-model="a"  />
    <input type="text" name="b" required ng-model="b" ng-minlength="2" />
    <span ng-click="see()">{{ test_form.$error }}</span>
  </form>
  
  var TestCtrl = function($scope){
    $scope.see = function(){
      console.log($scope.test_form.$error);
    }
  }

6.5.2. input

input 是数据的最主要入口。 ng 支持 HTML5 中的相关属性,同时对旧浏览器也做了兼容性处理。最重要的, input 的规则定义,是所属表单的相关行为的参照(比如表单是否验证成功)。

input 控件的相关可用属性为:

  • name 名字
  • ng-model 绑定的数据
  • required 是否必填
  • ng-required 是否必填
  • ng-minlength 最小长度
  • ng-maxlength 最大长度
  • ng-pattern 匹配模式
  • ng-change 值变化时的回调
  <form name="test_form" ng-controller="TestCtrl">
    <input type="text" name="a" ng-model="a" required ng-pattern="/abc/" />
    <span ng-click="see()">{{ test_form.$error }}</span>
  </form>

input 控件,它还有一些扩展,这些扩展有些有自己的属性:

  • input type="number" 多了 number 错误类型,多了 maxmin 属性。
  • input type="url" 多了 url 错误类型。
  • input type="email" 多了 email 错误类型。

6.5.3. checkbox

它也算是 input 的扩展,不过,它没有验证相关的东西,只有选中与不选中两个值:

  <form name="test_form" ng-controller="TestCtrl">
    <input type="checkbox" name="a" ng-model="a" ng-true-value="AA" ng-false-value="BB" />
    <span>{{ a }}</span>
  </form>
  
  var TestCtrl = function($scope){
    $scope.a = 'AA';
  }

两点:

  1. controller 要初始化变量值。
  2. controller 中的初始化值会关系到控件状态(双向绑定)。

6.5.4. radio

也是 input 的扩展。和 checkbox 一样,但它只有一个值了:

  <form name="test_form" ng-controller="TestCtrl">
    <input type="radio" name="a" ng-model="a" value="AA" />
    <input type="radio" name="a" ng-model="a" value="BB" />
    <span>{{ a }}</span>
  </form>

6.5.5. textarea

input

6.5.6. select

这是一个比较牛B的控件。它里面的一个叫做 ng-options 的属性用于数据呈现。

对于给定列表时的使用。

最简单的使用方法, x for x in list

  <form name="test_form" ng-controller="TestCtrl" ng-init="o=[0,1,2,3]; a=o[1];">
    <select ng-model="a" ng-options="x for x in o" ng-change="show()">
      <option value="">可以加这个空值</option>
    </select>
  </form>
  
  <script type="text/javascript">
  var TestCtrl = function($scope){
    $scope.show = function(){
      console.log($scope.a);
    }
  }
  
  angular.bootstrap(document.documentElement);
  </script>

$scope 中, select 绑定的变量,其值和普通的 value 无关,可以是一个对象:

  <form name="test_form" ng-controller="TestCtrl"
        ng-init="o=[{name: 'AA'}, {name: 'BB'}]; a=o[1];">
    <select ng-model="a" ng-options="x.name for x in o" ng-change="show()">
    </select>
  </form>

显示与值分别指定, x.v as x.name for x in o

  <form name="test_form" ng-controller="TestCtrl"
        ng-init="o=[{name: 'AA', v: '00'}, {name: 'BB', v: '11'}]; a=o[1].v;">
    <select ng-model="a" ng-options="x.v as x.name for x in o" ng-change="show()">
    </select>
  </form>

加入分组的, x.name group by x.g for x in o

  <form name="test_form" ng-controller="TestCtrl"
        ng-init="o=[{name: 'AA', g: '00'}, {name: 'BB', g: '11'}, {name: 'CC', g: '00'}]; a=o[1];">
    <select ng-model="a" ng-options="x.name group by x.g for x in o" ng-change="show()">
    </select>
  </form>

分组了还分别指定显示与值的, x.v as x.name group by x.g for x in o

  <form name="test_form" ng-controller="TestCtrl" ng-init="o=[{name: 'AA', g: '00', v: '='}, {name: 'BB', g: '11', v: '+'}, {name: 'CC', g: '00', v: '!'}]; a=o[1].v;">
    <select ng-model="a" ng-options="x.v as x.name group by x.g for x in o" ng-change="show()">
    </select>
  </form>

如果参数是对象的话,基本也是一样的,只是把遍历的对象改成 (key, value)

  <form name="test_form" ng-controller="TestCtrl" ng-init="o={a: 0, b: 1}; a=o.a;">
    <select ng-model="a" ng-options="k for (k, v) in o" ng-change="show()">
    </select>
  </form>
  
  <form name="test_form" ng-controller="TestCtrl"
        ng-init="o={a: {name: 'AA', v: '00'}, b: {name: 'BB', v: '11'}}; a=o.a.v;">
    <select ng-model="a" ng-options="v.v as v.name for (k, v) in o" ng-change="show()">
    </select>
  </form>
  
  <form name="test_form" ng-controller="TestCtrl"
        ng-init="o={a: {name: 'AA', v: '00', g: '=='}, b: {name: 'BB', v: '11', g: '=='}}; a=o.a;">
    <select ng-model="a" ng-options="v.name group by v.g for (k, v) in o" ng-change="show()">
    </select>
  </form>
  
  <form name="test_form" ng-controller="TestCtrl"
        ng-init="o={a: {name: 'AA', v: '00', g: '=='}, b: {name: 'BB', v: '11', g: '=='}}; a=o.a.v;">
    <select ng-model="a" ng-options="v.v as v.name group by v.g for (k, v) in o" ng-change="show()">
    </select>
  </form>

7. 模板中的过滤器

这里说的过滤器,是用于对数据的格式化,或者筛选的函数。它们可以直接在模板中通过一种语法使用。对于常用功能来说,是很方便的一种机制。

多个过滤器之间可以直接连续使用。

7.1. 排序 orderBy

orderBy 是一个排序用的过滤器标签。它可以像 sort 函数那样支持一个排序函数,也可以简单地指定一个属性名进行操作:

  <div ng-controller="TestCtrl">
    {{ data | orderBy: 'age' }} <br />
    {{ data | orderBy: '-age' }} <br />
    {{ data | orderBy: '-age' | limitTo: 2 }} <br />
    {{ data | orderBy: ['-age', 'name'] }} <br />
  </div>
  
  
  <script type="text/javascript">
  var TestCtrl = function($scope){
    $scope.data = [
      {name: 'B', age: 4},  
      {name: 'A', age: 1},  
      {name: 'D', age: 3},  
      {name: 'C', age: 3},  
    ];
  }
  
  angular.bootstrap(document.documentElement);
  </script>

7.2. 过滤列表 filter

filter 是一个过滤内容的标签。

如果参数是一个字符串,则列表成员中的任意属性值中有这个字符串,即为满足条件(忽略大小写):

  <div ng-controller="TestCtrl">
    {{ data | filter: 'b' }} <br />
    {{ data | filter: '!B' }} <br />
  </div>
  
  
  <script type="text/javascript">
  var TestCtrl = function($scope){
    $scope.data = [
      {name: 'B', age: 4},  
      {name: 'A', age: 1},  
      {name: 'D', age: 3},  
      {name: 'C', age: 3},  
    ];
  }
  
  angular.bootstrap(document.documentElement);
  </script>

可以使用对象,来指定属性名, $ 表示任意属性:

  {{ data | filter: {name: 'A'} }} <br />
  {{ data | filter: {$: '3'} }} <br />
  {{ data | filter: {$: '!3'} }} <br />

自定义的过滤函数也支持:

  <div ng-controller="TestCtrl">
    {{ data | filter: f }} <br />
  </div>
  
  
  <script type="text/javascript">
  var TestCtrl = function($scope){
    $scope.data = [
      {name: 'B', age: 4},  
      {name: 'A', age: 1},  
      {name: 'D', age: 3},  
      {name: 'C', age: 3},  
    ];
  
    $scope.f = function(e){
      return e.age > 2;
    }
  }
  
  angular.bootstrap(document.documentElement);
  </script>

7.3. 其它

时间戳格式化 date

  <div ng-controller="TestCtrl">
  {{ a | date: 'yyyy-MM-dd HH:mm:ss' }}
  </div>
  
  <script type="text/javascript">
  var TestCtrl = function($scope){
    $scope.a = ((new Date().valueOf()));
  }
  
  angular.bootstrap(document.documentElement);
  </script>

列表截取 limitTo ,支持正负数:

  {{ [1,2,3,4,5] | limitTo: 2 }}
  {{ [1,2,3,4,5] | limitTo: -3 }}

大小写 lowercaseuppercase

  {{ 'abc' | uppercase }}
  {{ 'Abc' | lowercase }}

7.4. 例子:表头排序

 1   <div ng-controller="TestCtrl">
 2     <table>
 3       <tr>
 4         <th ng-click="f='name'; rev=!rev">名字</th>
 5         <th ng-click="f='age'; rev=!rev">年龄</th>
 6       </tr>
 7   
 8       <tr ng-repeat="o in data | orderBy: f : rev">
 9         <td>{{ o.name }}</td>
10         <td>{{ o.age }}</td>
11       </tr>
12     </table>
13   </div>
14   
15   <script type="text/javascript">
16   var TestCtrl = function($scope){
17     $scope.data = [
18       {name: 'B', age: 4},  
19       {name: 'A', age: 1},  
20       {name: 'D', age: 3},  
21       {name: 'C', age: 3},  
22     ];
23   }
24   
25   angular.bootstrap(document.documentElement);
26   </script>

7.5. 例子:搜索

  <div ng-controller="TestCtrl" ng-init="s=data[0].name; q=''">
    <div>
      <span>查找:</span> <input type="text" ng-model="q" />
    </div>
    <select ng-multiple="true" ng-model="s"
            ng-options="o.name as o.name + '(' + o.age + ')' for o in data | filter: {name: q} | orderBy: ['age', 'name'] ">
    </select>
  </div>
  
  <script type="text/javascript">
  var TestCtrl = function($scope){
    $scope.data = [
      {name: 'B', age: 4},  
      {name: 'A', age: 1},  
      {name: 'D', age: 3},  
      {name: 'C', age: 3},  
    ];
  }
  
  angular.bootstrap(document.documentElement);
  </script>

8. 锚点路由

准确地说,这应该叫对 hashchange 事件的处理吧。

就是指 URL 中的锚点部分发生变化时,触发预先定义的业务逻辑。比如现在是 /test#/x ,锚点部分的值为 # 后的 /x ,它就对应了一组处理逻辑。当这部分变化时,比如变成了 /test#/t ,这时页面是不会刷新的,但是它可以触发另外一组处理逻辑,来做一些事,也可以让页面发生变化。

这种机制对于复杂的单页面来说,无疑是一种强大的业务切分手段。就算不是复杂的单页面应用,在普通页面上善用这种机制,也可以让业务逻辑更容易控制。

ng 提供了完善的锚点路由功能,虽然目前我觉得相当重要的一个功能还有待完善(后面会说),但目前这功能的几部分内容,已经让我思考了很多种可能性了。

ng 中的锚点路由功能是由几部分 API 共同完成的一整套方案。这其中包括了路由定义,参数定义,业务处理等。

8.1. 路由定义

要使用锚点路由功能,需要在先定义它。目前,对于定义的方法,我个人只发现在“初始化”阶段可以通过 $routeProvider 这个服务来定义。

在定义一个 app 时可以定义锚点路由:

  <html ng-app="ngView">
    ... ...
  
  <div ng-view></div>
  
  <script type="text/javascript">
  
  angular.module('ngView', [],
    function($routeProvider){
      $routeProvider.when('/test',
        {
          template: 'test',
        }
      );
    }
  );
  
  </script>

首先看 ng-view 这个 directive ,它是一个标记“锚点作用区”的指令。目前页面上只能有一个“锚点作用区”。有人已经提了,“多个可命名”的锚点作用区的代码到官方,但是目前官方还没有接受合并,我觉得多个作用区这个功能是很重要的,希望下个发布版中能有。

锚点作用区的功能,就是让锚点路由定义时的那些模板, controller 等,它们产生的 HTML 代码放在作用区内。

比如上面的代码,当你刚打开页面时,页面是空白的。你手动访问 /#/test 就可以看到页面上出现了 'test' 的字样。

angular.bootstrap() 时也可以定义:

  angular.bootstrap(document.documentElement, [
    function($routeProvider){
      $routeProvider.when('/test',
        {
          template: 'test'
        }
      );
    }
  ]);

8.2. 参数定义

在作路由定义时,可以匹配一个规则,规则中可以定义路径中的某些部分作为参数之用,然后使用 $routeParams 服务获取到指定参数。比如 /#/book/test 中, test 作为参数传入到 controller 中:

  <div ng-view></div>
  
  
  <script type="text/javascript">
  
  angular.module('ngView', [],
    function($routeProvider){
      $routeProvider.when('/book/:title',
        {
          template: '{{ title }}',
          controller: function($scope, $routeParams){
            $scope.title = $routeParams.title;
          }
        }
      );
    }
  );
  
  </script>

访问: /#/book/test

不需要预定义模式,也可以像普通 GET 请求那样获取到相关参数:

  angular.module('ngView', [],
    function($routeProvider){
      $routeProvider.when('/book',
        {
          template: '{{ title }}',
          controller: function($scope, $routeParams){
            $scope.title = $routeParams.title;
          }
        }
      );
    }
  );

访问: /#/book?title=test

8.3. 业务处理

简单来说,当一个锚点路由定义被匹配时,会根据模板生成一个 $scope ,同时相应的一个 controller 就会被触发。最后模板的结果会被填充到 ng-view 中去。

从上面的例子中可以看到,最直接的方式,我们可以在模板中双向绑定数据,而数据的来源,在 controller 中控制。在 controller 中,又可以使用到像 $scope$routeParams 这些服务。

这里先提一下另外一种与锚点路由相关的服务, $route 。这个服务里锚点路由在定义时,及匹配过程中的信息。比如我们搞怪一下:

  angular.module('ngView', [],
    function($routeProvider){
      $routeProvider.when('/a',
        {
          template: '{{ title }}',
          controller: function($scope){
            $scope.title = 'a';
          }
        }
      );
  
      $routeProvider.when('/b',
        {
          template: '{{ title }}',
          controller: function($scope, $route){
            console.log($route);
            $route.routes['/a'].controller($scope);
          }
        }
      );
    }
  );

回到锚点定义的业务处理中来。我们可以以字符串形式写模板,也可以直接引用外部文件作为模板:

  angular.module('ngView', [],
    function($routeProvider){
      $routeProvider.when('/test',
        {
          templateUrl: 'tpl.html',
          controller: function($scope){
            $scope.title = 'a';
          }
        }
      );
    }
  );

tpl.html 中的内容是:

  {{ title }}

这样的话,模板可以预定义,也可以很复杂了。

现在暂时忘了模板吧,因为前面提到的,当前 ng-view 不能有多个的限制,模板的渲染机制局限性还是很大的。不过,反正会触发一个 controller ,那么在函数当中我们可以尽量地干自己喜欢的事:

  angular.module('ngView', [],
    function($routeProvider){
      $routeProvider.when('/test',
        {
          template: '{{}}',
          controller: function(){
            $('div').first().html('<b>OK</b>');
          }
        }
      );
    }
  );

那个空的 template 不能省,否则 controller 不会被触发。

9. 定义模板变量标识标签

由于下面涉及动态内容,所以我打算起一个后端服务来做。但是我发现我使用的 Tornado 框架的模板系统,与 ng 的模板系统,都是使用 {{ }} 这对符号来定义模板表达式的,这太悲剧了,不过幸好 ng 已经提供了修改方法:

  angular.bootstrap(document.documentElement,
    [function($interpolateProvider){
      $interpolateProvider.startSymbol('[[');
      $interpolateProvider.endSymbol(']]');
    }]);

使用 $interpolateProvider 服务即可。

10. AJAX

ng 提供了基本的 AJAX 封装,你直接面对 promise 对象,使用起来还是很方便的。

10.1. HTTP请求

基本的操作由 $http 服务提供。它的使用很简单,提供一些描述请求的参数,请求就出去了,然后返回一个扩充了 success 方法和 error 方法的 promise 对象(下节介绍),你可以在这个对象中添加需要的回调函数。

  var TestCtrl = function($scope, $http){
    var p = $http({
      method: 'GET',
      url: '/json'
    });
    p.success(function(response, status, headers, config){
        $scope.name = response.name;
    });
  }

$http 接受的配置项有:

  • method 方法
  • url 路径
  • params GET请求的参数
  • data post请求的参数
  • headers 头
  • transformRequest 请求预处理函数
  • transformResponse 响应预处理函数
  • cache 缓存
  • timeout 超时毫秒,超时的请求会被取消
  • withCredentials 跨域安全策略的一个东西

其中的 transformRequesttransformResponseheaders 已经有定义的,如果自定义则会覆盖默认定义:

 1   var $config = this.defaults = {
 2     // transform incoming response data
 3     transformResponse: [function(data) {
 4       if (isString(data)) {
 5         // strip json vulnerability protection prefix
 6         data = data.replace(PROTECTION_PREFIX, '');
 7         if (JSON_START.test(data) && JSON_END.test(data))
 8           data = fromJson(data, true);
 9       }
10       return data;
11     }],
12   
13     // transform outgoing request data
14     transformRequest: [function(d) {
15       return isObject(d) && !isFile(d) ? toJson(d) : d;
16     }],
17   
18     // default headers
19     headers: {
20       common: {
21         'Accept': 'application/json, text/plain, */*',
22         'X-Requested-With': 'XMLHttpRequest'
23       },
24       post: {'Content-Type': 'application/json;charset=utf-8'},
25       put:  {'Content-Type': 'application/json;charset=utf-8'}
26     }
27   };

注意它默认的 POST 方法出去的 Content-Type

对于几个标准的 HTTP 方法,有对应的 shortcut :

  • $http.delete(url, config)
  • $http.get(url, config)
  • $http.head(url, config)
  • $http.jsonp(url, config)
  • $http.post(url, data, config)
  • $http.put(url, data, config)

注意其中的 JSONP 方法,在实现上会在页面中添加一个 script 标签,然后放出一个 GET 请求。你自己定义的,匿名回调函数,会被 ng 自已给一个全局变量。在定义请求,作为 GET 参数,你可以使用 JSON_CALLBACK 这个字符串来暂时代替回调函数名,之后 ng 会为你替换成真正的函数名:

  var p = $http({
    method: 'JSONP',
    url: '/json',
    params: {callback: 'JSON_CALLBACK'}
  });
  p.success(function(response, status, headers, config){
      console.log(response);
      $scope.name = response.name;
  });

$http 有两个属性:

  • defaults 请求的全局配置
  • pendingRequests 当前的请求队列状态
  $http.defaults.transformRequest = function(data){console.log('here'); return data;}
  console.log($http.pendingRequests);

10.2. 广义回调管理

和其它框架一样, ng 提供了广义的异步回调管理的机制。 $http 服务是在其之上封装出来的。这个机制就是 ng 的 $q 服务。

不过 ng 的这套机制总的来说实现得比较简单,按官方的说法,够用了。

使用的方法,基本上是:

  • 通过 $q 服务得到一个 deferred 实例
  • 通过 deferred 实例的 promise 属性得到一个 promise 对象
  • promise 对象负责定义回调函数
  • deferred 实例负责触发回调
  var TestCtrl = function($q){
    var defer = $q.defer();
    var promise = defer.promise;
    promise.then(function(data){console.log('ok, ' + data)},
                 function(data){console.log('error, ' + data)});
    //defer.reject('xx');
    defer.resolve('xx');
  }

了解了上面的东西,再分别看 $qdeferredpromise 这三个东西。

10.2.1. $q

$q 有四个方法:

  • $q.all() 合并多个 promise ,得到一个新的 promise
  • $q.defer() 返回一个 deferred 对象
  • $q.reject() 包装一个错误,以使回调链能正确处理下去
  • $q.when() 返回一个 promise 对象

$q.all() 方法适用于并发场景很合适:

  var TestCtrl = function($q, $http){
    var p = $http.get('/json', {params: {a: 1}});
    var p2 = $http.get('/json', {params: {a: 2}});
    var all = $q.all([p, p2]);
    p.success(function(res){console.log('here')});
    all.then(function(res){console.log(res[0])});
  }

$q.reject() 方法是在你捕捉异常之后,又要把这个异常在回调链中传下去时使用:

要理解这东西,先看看 promise 的链式回调是如何运作的,看下面两段代码的区别:

  var defer = $q.defer();
  var p = defer.promise;
  p.then(
    function(data){return 'xxx'}
  );
  p.then(
    function(data){console.log(data)}
  );
  defer.resolve('123');
  var defer = $q.defer();
  var p = defer.promise;
  var p2 = p.then(
    function(data){return 'xxx'}
  );
  p2.then(
    function(data){console.log(data)}
  );
  defer.resolve('123');

从模型上看,前者是“并发”,后者才是“链式”。

$q.reject() 的作用就是触发后链的 error 回调:

  var defer = $q.defer();
  var p = defer.promise;
  p.then(
    function(data){return data},
    function(data){return $q.reject(data)}
  ).
  then(
    function(data){console.log('ok, ' + data)},
    function(data){console.log('error, ' + data)}
  )
  defer.reject('123');

最后的 $q.when() 是把数据封装成 promise 对象:

  var p = $q.when(0, function(data){return data},
                     function(data){return data});
  p.then(
    function(data){console.log('ok, ' + data)},
    function(data){console.log('error, ' + data)}
  );

10.2.2. deferred

deferred 对象有两个方法一个属性。

  • promise 属性就是返回一个 promise 对象的。
  • resolve() 成功回调
  • reject() 失败回调
  var defer = $q.defer();
  var promise = defer.promise;
  promise.then(function(data){console.log('ok, ' + data)},
               function(data){console.log('error, ' + data)});
  //defer.reject('xx');
  defer.resolve('xx');

10.2.3. promise

promise 对象只有 then() 一个方法,注册成功回调函数和失败回调函数,再返回一个 promise 对象,以用于链式调用。

11. 工具函数

11.1. 上下文绑定

angular.bind 是用来进行上下文绑定,参数动态绑定的工具函数。

  var f = angular.bind({a: 'xx'},
    function(){
      console.log(this.a);
    }
  );
  f();

参数动态绑定:

  var f = function(x){console.log(x)}
  angular.bind({}, f, 'x')();

11.2. 对象处理

对象复制: angular.copy()

  var a = {'x': '123'};
  var b = angular.copy(a);
  a.x = '456';
  console.log(b);

对象聚合: angular.extend()

  var a = {'x': '123'};
  var b = {'xx': '456'};
  angular.extend(b, a);
  console.log(b);

空函数: angular.noop()

大小写转换: angular.lowercase()angular.uppercase()

JSON转换: angular.fromJson()angular.toJson()

遍历: angular.forEach() ,支持列表和对象:

  var l = {a: '1', b: '2'};
  angular.forEach(l, function(v, k){console.log(k + ': ' + v)});
  
  var l = ['a', 'b', 'c'];
  angular.forEach(l, function(v, i, o){console.log(v)});
  
  var context = {'t': 'xx'};
  angular.forEach(l, function(v, i, o){console.log(this.t)}, context);

11.3. 类型判定

  • angular.isArray
  • angular.isDate
  • angular.isDefined
  • angular.isElement
  • angular.isFunction
  • angular.isNumber
  • angular.isObject
  • angular.isString
  • angular.isUndefined

12. 其它服务

12.1. 日志

ng 提供 $log 这个服务用于向终端输出相关信息:

  • error()
  • info()
  • log()
  • warn()
  var TestCtrl = function($log){
    $log.error('error');
    $log.info('info');
    $log.log('log');
    $log.warn('warn');
  }

12.2. 缓存

ng 提供了一个简单封装了缓存机制 $cacheFactory ,可以用来作为数据容器:

  var TestCtrl = function($scope, $cacheFactory){
    $scope.cache = $cacheFactory('s_' + $scope.$id, {capacity: 3});
  
    $scope.show = function(){
      console.log($scope.cache.get('a'));
      console.log($scope.cache.info());
    }
  
    $scope.set = function(){
      $scope.cache.put((new Date()).valueOf(), 'ok');
    }
  }

调用时,第一个参数是 id ,第二个参数是配置项,目前支持 capacity 参数,用以设置缓存能容留的最大条目数。超过这个个数,则自动清除较旧的条目。

缓存实例的方法:

  • info() 获取 id , size 信息
  • put(k, v) 设置新条目
  • get(k) 获取条目
  • remove(k) 删除条目
  • removeAll() 删除所有条目
  • destroy() 删除对本实例的引用

$http 的调用当中,有一个 cache 参数,值为 true 时为自动维护的缓存。值也可以设置为一个 cache 实例。

12.3. 计时器

$timeout 服务是 ng 对 window.setTimeout() 的封装,它使用 promise 统一了计时器的回调行为:

  var TestCtrl = function($timeout){
    var p = $timeout(function(){console.log('haha')}, 5000);
    p.then(function(){console.log('x')});
    //$timeout.cancel(p);
  }

使用 $timeout.cancel() 可以取消计时器。

12.4. 表达式函数化

$parse 这个服务,为 js 提供了类似于 Python 中 @property 的能力:

  var TestCtrl = function($scope, $parse){
    $scope.get_name = $parse('name');
    $scope.show = function(){console.log($scope.get_name($scope))}
    $scope.set = function(){$scope.name = '123'}
  }

$parse 返回一个函数,调用这个函数时,可以传两个参数,第一个作用域,第二个是变量集,后者常用于覆盖前者的变量:

  var get_name = $parse('name');
  var r = get_name({name: 'xx'}, {name: 'abc'});
  console.log(r);

$parse 返回的函数,也提供了相应的 assign 功能,可以为表达式赋值(如果可以的话):

  var get_name = $parse('name');
  var set_name = get_name.assign;
  var r = get_name({name: 'xx'}, {name: 'abc'});
  console.log(r);
  
  var s = {}
  set_name(s, '123');
  var r = get_name(s);
  console.log(r);

12.5. 模板单独使用

ng 中的模板是很重要,也很强大的一个机制,自然少不了单独运用它的方法。不过,即使是单独使用,也是和 DOM 紧密相关的程度:

  • 定义时必须是有 HTML 标签包裹的,这样才能创建 DOM 节点
  • 渲染时必须传入 $scope

之后使用 $compile 就可以得到一个渲染好的节点对象了。当然, $compile 还要做其它一些工作,指令处理什么的。

  var TestCtrl = function($scope, $element,$compile){
    $scope.a = '123';
    $scope.set = function(){
      var tpl = $compile('<p>hello {{ a }}</p>');
      var e = tpl($scope);
      $element.append(e);
    }
  }

13. 自定义模块和服务

13.1. 模块和服务的概念与关系

总的来说,模块是组织业务的一个框框,在一个模块当中定义多个服务。当你引入了一个模块的时候,就可以使用这个模块提供的一种或多种服务了。

比如 AngularJS 本身的一个默认模块叫做 ng ,它提供了 $http$q 等等服务。

服务只是模块提供的多种机制中的一种,其它的还有命令( directive ),过滤器( filter ),及其它配置信息。

然后在额外的 js 文件中有一个附加的模块叫做 ngResource , 它提供了一个 $resource 服务。

定义时,我们可以在已有的模块中新定义一个服务,也可以先新定义一个模块,然后在新模块中定义新服务。

使用时,服务是需要显式地的声明依赖(引入)关系的,而服务则可以让 ng 自动地做注入,然后直接使用。

13.2. 定义模块

定义模块的方法是使用 angular.module 。调用时声明了对其它模块的依赖,并定义了“初始化”函数。

  var my_module = angular.module('MyModule', [], function(){
      console.log('here');
  });

这段代码定义了一个叫做 MyModule 的模块, my_module 这个引用可以在接下来做其它的一些事,比如定义服务。

13.3. 定义服务

服务本身是一个任意的对象。但是 ng 提供服务的过程涉及它的依赖注入机制。在这里呢,就要先介绍一下叫 provider 的东西。

简单来说, provider 是被“注入控制器”使用的一个对象,注入机制通过调用一个 provider$get() 方法,把得到的东西作为参数进行相关调用(比如把得到的服务作为一个 Controller 的参数)。

在这里“服务”的概念就比较不明确,对使用而言,服务仅指 $get() 方法返回的东西,但是在整体机制上,服务又要指提供了 $get() 方法的整个对象。

  //这是一个provider
  var pp = function(){
    this.$get = function(){
      return {'haha': '123'};
    }
  }
  
  //我在模块的初始化过程当中, 定义了一个叫 PP 的服务
  var app = angular.module('Demo', [], function($provide){
    $provide.provider('PP', pp);
  });
  
  //PP服务实际上就是 pp 这个 provider 的 $get() 方法返回的东西
  app.controller('TestCtrl',
    function($scope, PP){
      console.log(PP);
    }
  );

上面的代码是一种定义服务的方法,当然, ng 还有相关的 shortcut, ng 总有很多 shortcut 。

第一个是 factory 方法,由 $provide 提供, modulefactory 是一个引用,作用一样。这个方法直接把一个函数当成是一个对象的 $get() 方法,这样你就不用显式地定义一个 provider 了:

  var app = angular.module('Demo', [], function($provide){
    $provide.factory('PP', function(){
      return {'hello': '123'};
    });
  });
  app.controller('TestCtrl', function($scope, PP){ console.log(PP) });

module 中使用:

  var app = angular.module('Demo', [], function(){ });
  app.factory('PP', function(){return {'abc': '123'}});
  app.controller('TestCtrl', function($scope, PP){ console.log(PP) });

第二个是 service 方法,也是由 $provide 提供, module 中有对它的同名引用。 servicefactory 的区别在于,前者是要求提供一个“构造方法”,后者是要求提供 $get() 方法。意思就是,前者一定是得到一个 object ,后者可以是一个数字或字符串。它们的关系大概是:

  var app = angular.module('Demo', [], function(){ });
  app.service = function(name, constructor){
    app.factory(name, function(){
      return (new constructor());
    });
  }

这里插一句,js 中 new 的作用,以 new a() 为例,过程相当于:

  1. 创建一个空对象 obj
  2. 把 obj 绑定到 a 函数的上下文当中(即 a 中的 this 现在指向 obj )
  3. 执行 a 函数
  4. 返回 obj

service 方法的使用就很简单了:

  var app = angular.module('Demo', [], function(){ });
  app.service('PP', function(){
    this.abc = '123';
  });
  app.controller('TestCtrl', function($scope, PP){ console.log(PP) });

13.4. 引入模块并使用服务

结合上面的“定义模块”和“定义服务”,我们可以方便地组织自己的额外代码:

  angular.module('MyModule', [], function($provide){
    $provide.factory('S1', function(){
      return 'I am S1';
    });
    $provide.factory('S2', function(){
      return {see: function(){return 'I am S2'}}
    });
  });
  
  var app = angular.module('Demo', ['MyModule'], angular.noop);
  app.controller('TestCtrl', function($scope, S1, S2){
    console.log(S1)
    console.log(S2.see())
  });

14. 附加模块 ngResource

14.1. 使用引入与整体概念

ngResource 这个是 ng 官方提供的一个附加模块。附加的意思就是,如果你打算用它,那么你需要引入一人单独的 js 文件,然后在声明“根模块”时注明依赖的 ngResource 模块,接着就可以使用它提供的 $resource 服务了。完整的过程形如:

  <!DOCTYPE html>
  <html ng-app="Demo">
  <head>
  <meta charset="utf-8" />
  <title>AngularJS</title>
  <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/angularjs/1.0.3/angular.min.js"></script>
  <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/angularjs/1.0.3/angular-resource.js"></script>
  </head>
  <body>
  
    <div ng-controller="TestCtrl"></div>
  
  
  <script type="text/javascript" charset="utf-8">
  
  var app = angular.module('Demo', ['ngResource'], angular.noop);
  app.controller('TestCtrl', function($scope, $resource){
    console.log($resource);
  });
  
  </script>
  </body>
  </html>

$resource 服务,整体上来说,比较像是使用类似 ORM 的方式来包装了 AJAX 调用。区别就是 ORM 是操作数据库,即拼出 SQL 语句之后,作 execute 方法调用。而 $resource 的方式是构造出 AJAX 请求,然后发出请求。同时,AJAX 请求是需要回调处理的,这方面, $resource 的机制可以使你在一些时候省掉回调处理,当然,是否作回调处理在于业务情形及容错需求了。

使用上 $resource 分成了“类”与“实例”这两个层面。一般地,类的方法调用就是直观的调用形式,通常会返回一个对象,这个对象即为“实例”。

“实例”贯穿整个服务的使用过程。“实例”的数据是填充方式,即因为异步关系,回调函数没有执行时,实例已经存在,只是可能它还没有相关数据,回调执行之后,相关数据被填充到实例对象当中。实例的方法一般就是在类方法名前加一个 $ ,调用上,根据定义,实例数据可能会做一些自动的参数填充,这点是区别实例与类的调用上的不同。

好吧,上面这些话可能需要在看了接下来的内容之后再回过来理解。

14.2. 基本定义

就像使用 ORM 一般要先定义 Model 一样,使用 $resource 需要先定义“资源”,也就是先定义一些 HTTP 请求。

在业务场景上,我们假设为,我们需要操作“书”这个实体,包括创建create,获取详情read,修改update,删除delete,批量获取multi,共五个操作方法。实体属性有:唯一标识id,标题title,作者author。

我们把这些操作定义成 $resource 的资源:

  var app = angular.module('Demo', ['ngResource'], angular.noop);
  app.controller('BookCtrl', function($scope, $resource){
    var actions = {
      create: {method: 'POST', params: {_method: 'create'}},
      read: {method: 'POST', params: {_method: 'read'}},
      update: {method: 'POST', params: {_method: 'update'}},
      delete: {method: 'POST', params: {_method: 'delete'}},
      multi: {method: 'POST', params: {_method: 'multi'}}
    }
    var Book = $resource('/book', {}, actions);
  });

定义是使用使用 $resource 这个函数就可以了,它接受三个参数:

  • url
  • 默认的params(这里的 params 即是 GET 请求的参数,POST 的参数单独叫做“postData”)
  • 方法映射

方法映射是以方法名为 key ,以一个对象为 value ,这个 value 可以有三个成员:

  • method, 请求方法,'GET', 'POST', 'PUT', 'DELETE' 这些
  • params, 默认的 GET 参数
  • isArray, 返回的数据是不是一个列表

14.3. 基本使用

在定义了资源之后,我们看如果使用这些资源,发出请求:

  var book = Book.read({id: '123'}, function(response){
    console.log(response);
  });

这里我们进行 Book 的“类”方法调用。在方法的使用上,根据官方文档:

  HTTP GET "class" actions: Resource.action([parameters], [success], [error])
  non-GET "class" actions: Resource.action([parameters], postData, [success], [error])
  non-GET instance actions: instance.$action([parameters], [success], [error])

我们这里是第二种形式,即类方法的非 GET 请求。我们给的参数会作为 postData 传递。如果我们需要 GET 参数,并且还需要一个错误回调,那么:

  var book = Book.read({get: 'haha'}, {id: '123'},
    function(response){
      console.log(response);
    },
    function(error){
      console.log(error);
    }
  );

调用之后,我们会立即得到的 book ,它是 Book 类的一个实例。这里所谓的实例,实际上就是先把所有的 action 加一个 $ 前缀放到一个空对象里,然后把发出的参数填充进去。等请求返回了,把除 action 以外的成员删除掉,再把请求返回的数据填充到这个对象当中。所以,如果我们这样:

  var book = Book.read({id: '123'}, function(response){
    console.log(book);
  });
  console.log(book)

就能看到 book 实例的变化过程了。

现在我们得到一个真实的实例,看一下实例的调用过程:

  //响应的数据是 {result: 0, msg: '', obj: {id: 'xxx'}}
  var book = Book.create({title: '测试标题', author: '测试作者'}, function(response){
    console.log(book);
  });

可以看到,在请求回调之后, book 这个实例的成员已经被响应内容填充了。但是这里有一个问题,我们返回的数据,并不适合一个 book 实例。格式先不说,它把 titleauthor 这些信息都丢了(因为响应只返回了 id )。

如果仅仅是格式问题,我们可以通过配置 $http 服务来解决( AJAX 请求都要使用 $http 服务的):

  $http.defaults.transformResponse = function(data){return angular.fromJson(data).obj};

当然,我们也可以自己来解决一下丢信息的问题:

  var p = {title: '测试标题', author: '测试作者'};
  var book = Book.create(p, function(response){
    angular.extend(book, p);
    console.log(book);
  });

不过,始终会有一些不方便了。比较正统的方式应该是调节服务器端的响应,让服务器端也具有和前端一样的实例概念,返回的是完整的实例信息。即使这样,你也还要考虑格式的事。

现在我们得到了一个真实的 book 实例了,带有 id 信息。我们尝试一下实例的方法调用,先回过去头看一下那三种调用形式,对于实例只有第三种形式:

  non-GET instance actions: instance.$action([parameters], [success], [error])

首先解决一个疑问,如果一个实例是进行一个 GET 的调用会怎么样?没有任何问题,这当然没有任何问题的,形式和上面一样。

如何实例是做 POST 请求的话,从形式上看,我们无法控制请求的 postData ?是的,所有的 POST 请求,其 postData 都会被实例数据自动填充,形式上我们只能控制 params

所以,如果是在做修改调用的话:

  book.$update({title: '新标题', author: '测试作者'}, function(response){
    console.log(book);
  });

这样是没有意义的并且错误的。因为要修改的数据只是作为 GET 参数传递了,而 postData 传递的数据就是当前实例的数据,并没有任何修改。

正确的做法:

  book.title = '新标题'
  book.$update(function(response){
    console.log(book);
  });

显然,这种情况下,回调都可以省了:

  book.title = '新标题'
  book.$update();

14.4. 定义和使用时的占位量

两方面。一是在定义时,在其 URL 中可以使用变量引用的形式(类型于定义锚点路由时那样)。第二时定义默认 params ,即 GET 参数时,可以定义为引用 postData 中的某变量。比如我们这样改一下:

  var Book = $resource('/book/:id', {}, actions);
  var book = Book.read({id: '123'}, {}, function(response){
    console.log(response);
  });

在 URL 中有一个 :id ,表示对 paramsid 这个变量的引用。因为 read 是一个 POST 请求,根据调用形式,第一个参数是 params ,第二个参数是 postData 。这样的调用结果就是,我们会发一个 POST 请求到如下地址, postData 为空:

  /book/123?_method=read

再看默认的 params 中引用 postData 变量的形式:

  var Book = $resource('/book', {id: '@id'}, actions);
  var book = Book.read({title: 'xx'}, {id: '123'}, function(response){
    console.log(response);
  });

这样会出一个 POST 请求, postData 内容中有一个 id 数据,访问的 URL 是:

  /book?_method=read&id=123&title=xx

这两个机制也可以联合使用:

  var Book = $resource('/book/:id', {id: '@id'}, actions);
  var book = Book.read({title: 'xx'}, {id: '123'}, function(response){
    console.log(response);
  });

结果就是出一个 POST 请求, postData 内容中有一个 id 数据,访问的 URL 是:

  /book/123?_method=read&title=xx

14.5. 实例

ngResource 要举一个实例是比较麻烦的事。因为它必须要一个后端来支持,这里如果我用 Python 写一个简单的后端,估计要让这个后端跑起来对很多人来说都是问题。所以,我在几套公共服务的 API 中纠结考察了一番,最后使用 www.rememberthemilk.com 的 API 来做了一个简单的,可用的例子。

例子见: http://zouyesheng.com/demo/ng-resource-demo.html (可以直接下载看源码)

先说一下 API 的情况。这里的请求调用全是跨域的,所以交互上全部是使用了 JSONP 的形式。 API 的使用有使用签名认证机制,嗯, js 中直接算 md5 是可行的,我用了一个现成的库(但是好像不能处理中文吧)。

这个例子中的 LoginCtrl 大家就不用太关心了,参见官方的文档,走完流程拿到 token 完事。与 ngResource 相关的是 MainCtrl 中的东西。

其实从这个例子中就可以看出,目前 ngResource 的机制对于服务端返回的数据的格式是严重依赖的,同时也可以反映出 $http 对一些场景根本无法应对的局限。所以,我现在的想法是理解 ngResource 的思想,真正需要的人自己使用 jQuery 重新实现一遍也许更好。这应该也花不了多少时间, ngResource 的代码本来不多。

我为什么说 $http 在一些场景中有局限呢。在这个例子当中,所有的请求都需要带一个签名,签名值是由请求中带的参数根据规则使用 md5 方法计算出的值。我找不到一个 hook 可以让我在请求出去之前修改这个请求(添加上签名)。所以在这个例子当中,我的做法是根据 ngResource 的请求最后会使用 $httpBackend 这个底层服务,在 module 定义时我自己复制官方的相关代码,重新定义 $httpBackend 服务,在需要的地方做我自己的修改:

  script.src = sign_url(url);

不错,我就改了这一句,但我不得不复制了 50 行官方源码到我的例子中。

另外一个需要说的是对返回数据的处理。因为 ngResource 会使用返回的数据直接填充实例,所以这个数据格式就很重要。

首先,我们可以使用 $http.defaults.transformResponse 来统一处理一下返回的数据,但是这并不能解决所有问题,可目前 ngResource 并不提供对每一个 action 的单独的后处理回调函数项。除非你的服务端是经过专门的适应性设计的,否则你用 ngResource 不可能爽。例子中,我为了获取当前列表的结果,我不得不自己去封装结果:

  var list_list = List.getList(function(){
    var res = list_list[1];
    while(list_list.length > 0){list_list.pop()};
    angular.forEach(res.list, function(v){
      list_list.push(new List({list: v}));
    });
    $scope.list_list = list_list;
    $scope.show_add = true;
    return;
  });

15. AngularJS与其它框架的混用(jQuery, Dojo)

这个问题似乎很多人都关心,但是事实是,如果了解了 ng 的工作方式,这本来就不是一个问题了。

在我自己使用 ng 的过程当中,一直是混用 jQuery 的,以前还要加上一个 Dojo 。只要了解每种框架的工作方式,在具体的代码中每个框架都做了什么事,那么整体上控制起来就不会有问题。

回到 ng 上来看,首先对于 jQuery 来说,最开始说提到过,在 DOM 操作部分, ng 与 jQuery 是兼容的,如果没有 jQuery , ng 自己也实现了兼容的部分 API 。

同时,最开始也提到过, ng 的使用最忌讳的一点就是修改 DOM 结构——你应该使用 ng 的模板机制进行数据绑定,以此来控制 DOM 结构,而不是直接操作。换句话来说,在不动 DOM 结构的这个前提之下,你的数据随便怎么改,随便使用哪个框架来控制都是没问题的,到时如有必要使用 $scope.$digest() 来通知 ng 一下即可。

下面这个例子,我们使用了 jQuery 中的 Deferred ( $.ajax 就是返回一个 Deferred ),还使用了 ng 的 $timeout ,当然是在 ng 的结构之下:

 1   <!DOCTYPE html>
 2   <html ng-app="Demo">
 3   <head>
 4   <meta charset="utf-8" />
 5   <title>AngularJS</title>
 6   </head>
 7   <body>
 8   
 9   <div ng-controller="TestCtrl">
10     <span ng-click="go()">{{ a }}</span>
11   </div>
12   
13   <script type="text/javascript"
14     src="http://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js">
15   </script>
16   <script type="text/javascript"
17     src="http://ajax.googleapis.com/ajax/libs/angularjs/1.0.3/angular.min.js">
18   </script>
19   
20   <script type="text/javascript">
21   var app = angular.module('Demo', [], angular.noop);
22   app.controller('TestCtrl', function($scope, $timeout){
23     $scope.a = '点击我开始';
24   
25     var defer = $.Deferred();
26     var f = function(){
27       if($scope.a == ''){$scope.a = '已停止'; return}
28       defer.done(function(){
29         $scope.a.length < 10 ? $scope.a += '>' : $scope.a = '>';
30         $timeout(f, 100);
31       });
32     }
33     defer.done(function(){$scope.a = '>'; f()});
34   
35     $scope.go = function(){
36       defer.resolve();
37       $timeout(function(){$scope.a = ''}, 5000);
38     }
39   });
40   </script>
41   </body>
42   </html>

再把 Dojo 加进来看与 DOM 结构相关的例子。之前说过,使用 ng 就最好不要手动修改 DOM 结构,但这里说两点:

  1. 对于整个页面,你可以只在局部使用 ng ,不使用 ng 的地方你可以随意控制 DOM 。
  2. 如果 DOM 结构有变动,你可以在 DOM 结构定下来之后再初始化 ng 。

下面这个例子使用了 AngularJSjQueryDojo

 1   <!DOCTYPE html>
 2   <html>
 3   <head>
 4   <meta charset="utf-8" />
 5   <title>AngularJS</title>
 6   <link rel="stylesheet"
 7     href="http://ajax.googleapis.com/ajax/libs/dojo/1.9.1/dijit/themes/claro/claro.css" media="screen" />
 8   </head>
 9   <body class="claro">
10   
11   <div ng-controller="TestCtrl" id="test_ctrl">
12   
13     <p ng-show="!btn_disable">
14       <button ng-click="change()">调用dojo修改按钮</button>
15     </p>
16   
17     <p id="btn_wrapper">
18       <button data-dojo-type="dijit/form/Button" type="button">{{ a }}</button>
19     </p>
20   
21     <p>
22       <input ng-model="dialog_text" ng-init="dialog_text='对话框内容'" />
23       <button ng-click="dialog(dialog_text)">显示对话框</button>
24     </p>
25   
26     <p ng-show="show_edit_text" style="display: none;">
27       <span>需要编辑的内容:</span>
28       <input ng-model="text" />
29     </p>
30   
31     <div id="editor_wrapper">
32       <div data-dojo-type="dijit/Editor" id="editor"></div>
33     </div>
34   
35   </div>
36   
37   
38   <script type="text/javascript"
39     src="http://ajax.googleapis.com/ajax/libs/dojo/1.9.1/dojo/dojo.js">
40   </script>
41   <script type="text/javascript"
42     src="http://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js">
43   </script>
44   <script type="text/javascript"
45     src="http://ajax.googleapis.com/ajax/libs/angularjs/1.0.3/angular.min.js">
46   </script>
47   
48   <script type="text/javascript">
49   
50   require(['dojo/parser', 'dijit/Editor'], function(parser){
51     parser.parse($('#editor_wrapper')[0]).then(function(){
52       var app = angular.module('Demo', [], angular.noop);
53   
54       app.controller('TestCtrl', function($scope, $timeout){
55         $scope.a = '我是ng, 也是dojo';
56         $scope.show_edit_text = true;
57   
58         $scope.change = function(){
59           $scope.a = 'DOM结构已经改变(不建议这样做)';
60           require(['dojo/parser', 'dijit/form/Button', 'dojo/domReady!'],
61             function(parser){
62               parser.parse($('#btn_wrapper')[0]);
63               $scope.btn_disable = true;
64             }
65           );
66         }
67   
68         $scope.dialog = function(text){
69           require(["dijit/Dialog", "dojo/domReady!"], function(Dialog){
70             var dialog = new Dialog({
71                 title: "对话框哦",
72                 content: text,
73                 style: "width: 300px"
74             });
75             dialog.show();
76           });
77         }
78   
79         require(['dijit/registry'], function(registry){
80           var editor = registry.byId('editor');
81           $scope.$watch('text', function(new_v){
82             editor.setValue(new_v);
83           });
84         });
85   
86       });
87   
88       angular.bootstrap(document, ['Demo']);
89     });
90   
91   });
92   
93   </script>
94   </body>
95   </html>
评论
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics