[关闭]
@Belinda 2015-05-15T03:15:48.000000Z 字数 13584 阅读 1658

anjular js(v3.0)

学习笔记


找不到的API

AngularJS提供了一些功能的封装,但是当你试图通过全局对象angular去访问这些 功能时,却发现与以往遇到的库大不相同。

比如,在jQuery中,我们知道它的API通过一个全局对象:ajax使.ajax()就可以了。API很符合思维的预期。

AngularJS也暴露了一个全局对象:angular,也对ajax调用进行封装提供了一个 http沿访angular.http时,发现不是 那么回事。

仔细地查阅http线http拿到。

事实上,AngularJS把所有的功能组件都以依赖注入的方式组织起来,这导致了你 必须通过一个中介才能获得某个组件的实例对象:

    var injector = angular.injector(['ng']);
    injector.invoke(function($http){
        //do sth. with $http
    });

这个中介,就是依赖注入模式中的容器,在AngularJS中,被称为注入器

注入器/injector

注入器是AngularJS框架的关键,这是一个DI/IoC容器的实现。

AngularJS将功能分成了不同类型的组件分别实现,这些组件有一个统称: 供给者/provider。组件之间不可以互相直接调用,一个组件必须通过注入器才 可以调用另一个组件。这样的好处是组件之间相互解耦,脏活留给注入器。

注入器实现了两个重要的功能:

集中存储所有provider的配方(名称+实例化方法),就是说,它知道整个系统 都有哪些功能组件。
按需提供功能组件的实例。

注册服务组件

在AngularJS中,从injector的角度看,组件就是一个功能提供者,因此被称为供给者/Provider。

很显然地,每个组件需要在injector中注册自己,这需要两部分信息:

标识符:用来区别自己
创建方法:用来告诉injector如何实例化自己
标识符通常使用一个字符串标识,比如"http"http"scope"代表作用域对象、"$compile" 代表编译服务...

创建方法通常是一个具有指定接口的构造函数,injector通过调用该函数,就可以实例化组件。

标识符和创建方法的组合信息,被称为配方。injector中将维护一个集中的配方库,用来按需创建 不同的组件。

获得注入器对象

要使用AngularJS的功能,必须首先获取注入器。有两种方法取得注入器。

1.创建一个新的注入器

可以使用angular.injector()创建一个新的注入器

2.使用已经创建的注入器

使用angular.element().injector()获得已经创建的注入器。

调用AngularJS的API

使用注入器的invoke()方法,可以直接调用一个用户自定义的函数体,并通过函数参数 注入所以来的对象:

    angular.injector(['ng'])
    .invoke(function($http){
        //do sth. with $http
    });

也可以使用注入器的get()方法,获得指定名称的服务实例:

    var my$http = angular.injector(['ng']).get('$http');
    //do sth. with my$http

怎么确定注入什么?

当采用invoke方式调用组件服务时,AngularJS通过检查函数的参数名确定需要注 入什么对象,比如:

    angular.injector(['ng'])
    .invoke(function($http){
        //do sth. with $http
    });

AngularJS在执行invoke时,检查函数的参数表,发现并注入$http对象。

这样有一个问题,就是当我们对JavaScript代码进行压缩处理时,$http可能会被 变更成其他名称,这将导致注入失败。

显式指定注入项

AngularJS采用依赖项数组的方法解决代码压缩混淆产生的问题:

    angular.injector(['ng'])
    .invoke(["$http","$compile",function($http,$compile){
        //do sth. with $http,$compile
    }]);

这是传入invoke的是一个数组,数组的最后一项是实际要执行的函数,数组的其他项 则指明需要向该函数注入的对象。invoke将按照数组中的顺序,依次向函数注入依赖 对象。

库 vs. 框架

和jQuery不同,AngularJS是一个框架。

lib vs. framework

jQuery是一个库,库总是被动的,就像工具,应用的开发逻辑是你的,在 某一点上需要用一下工具,就用好了。

框架则非常不同,这意味着AngularJS为应用已经搭起了一个架子,约定了 一些组成部分,并且实现了这些部分的拼装运行。换句话说,应用的开发 逻辑是AngularJS的,你得跟着它走。

所以,AngularJS难学一些,因为它有一个架子在那,你不了解这个架子, 基本没法下手。

引入AngularJS库

在右边的示例中,我们定义了一个指令ez-duang, 它应该会展开成一个动画 显示出来。

但是,看起来没有什么动画显示出来。AngularJS似乎没有工作,为什么?

有点像操作系统,AngularJs也有一个启动引导的概念。

当你在HTML文件中引入angular.min.js时,AngularJS只是建立了一个全局的 angular对象,这个对象有一些方法可供开发者调用,但应用的框架还没有建立。 在这个阶段,AngularJS还只是一个库。

只有通过启动引导,那些指令才能够生效,比如,我们的ez-duang。

像下面这样,试着给html元素增加一个ng-app指令,再重新运行!

    <html ng-app>
    ....
    </html>
    ng-app="ezstuff"

自动引导启动AngularJS框架

如果HTML文件中有某个标签有ng-app属性,那么当DOM树建立成功后,AngularJS 就会自动进入引导过程,启动整个框架:

手工引导启动AngularJS框架

在大多数情况下,我们都使用ng-app指令来进行自动引导启动,但是如果一个HTML文件中 有多个ng-app,AngularJS只会自动引导它找到的第一个ng-app应用,这是需要手工引导 的一个应用场景。

我们可以利用angular.bootstrap()进行手动引导:

    angular.bootstrap(element, [modules], [config]);

bootstrap方法有三个参数:

element : 一个DOM元素,以这个元素为Angular应用的根。就是自动引导时ng-app所在 的元素,这个参数是必须的。比如:document、document.body等。
modules : 引导时需要载入的模块数组。比如:[]、["ezstuff"]等。由于我们的HTML中引用 了ezstuff模块中定义的ez-duang指令,所以,我们需要指定载入ezstuff模块。
config :引导配置项,可选。我们先忽略。
最终,我们使用如下的形式进行手动引导:

    angular.bootstrap(document,["ezstuff"]);

引导第1步:创建注入器

引导过程使AngularJS从库转变成了一个框架。

回忆我们之前提到,AngularJS深入骨髓地使用着依赖注入,那么,在引导过程之初,首先创建一个 注入器就毫不奇怪了。

注入器是通向AngularJS所有功能的入口,而AngularJS的功能实现,是通过模块的方式组织的。所以, 在创建注入器的时候,需要告诉AngularJS载入哪些模块(ng模块是内置载入的,不需要显式指定)。

在自动启动引导的场景下,可以给ng-app赋值以指定一个需要载入的模块,比如:

    ng-app = "ezstuff"

在手动启动引导的场景下,通过bootstrap方法的第二个参数指定需要载入的模块,比如:

    angular.bootstrap(document,["ezstuff"]);

引导第2步:创建根作用域

scope对象是AngularJS实现数据绑定的重要服务,所以,在引导启动建立了注入器之后, AngularJS马上在应用的根节点上创建一个根作用域:$rootScope对象。

$rootScope

如果是自动引导启动,那么ng-app所在的DOM节点对应着根作用域。如果是手工引导启动, 那么在bootstrap方法中指定的第一个参数就对应着根作用域。

无论哪一种情况,一旦$rootScope对象创建成功,AngularJS就将这个对象存储到根节点 的data中,我们可以使用如下的方法查看这个对象:

    angular.element(approot).data("$rootScope");

引导第3步:编译DOM子树

以ng-app所在DOM节点为根节点,对这棵DOM子树进行编译。

编译过程通常借助于指令,完成这几种操作:

对DOM对象进行变换。

在DOM对象上挂接事件监听。
在DOM对象对应的scope对象上挂接数据监听。
编译过程是AngularJS相当有特点的一个存在,我们将在下一节继续深入。
编译的目的:实现声明式前端开发
HTML可以声明式地格式化静态文档。例如,如果你需要居中,那么你不需要明确地告诉 浏览器将窗口尺寸二分以获得中心点,并将该中心点与文本中心点对齐。简单地加一个 "align=center"属性就可以获得期望的效果。这就是声明式的威力。

但是,声明式语言也有其局限性。例如,没有简单的办法将文本对器到1/3的位置。我们 需要一个办法教会浏览器新的语法。

AngularJS的HTML编译器使开发者可以创建新的HTML语法。可以将AngularJS的编译器 理解为浏览器引擎的前级解释器,开发者定制的指令,首先被AngularJS的编译器解释为 浏览器引擎能够理解的指令,再送往浏览器引擎进行渲染生成。

指令/directive

笼统地说,指令是DOM元素(例如属性、元素、CSS类等)上的标识符,用来告诉AngularJS的HTML编译器 ($compile服务)将特定的行为绑定到DOM元素,或者改变DOM元素及其子元素。

指令可以放置在元素名、属性、CSS类名称及备注中。下面是一些等效的触发"ng-bind"指令的格式:

    <span ng-bind="exp"></span>
    <span class="ng-bind: exp;"></span>
    <ng-bind></ng-bind>
    <!-- directive: ng-bind exp -->

指令本质上就是一个函数,当编译器在DOM中匹配它时,AngularJS将执行这个函数。

问题是,HTML中的ez-duang,怎么就匹配到了JavaScript中的ezDuang?

指令的规范化

AngularJS在进行匹配检测之前,首先对HTML元素的标签和属性名转化成规范的驼峰式字符串:

去除名称前缀的x-和data-
以: , - 或 _ 为分割符,将字符串切分成单词,除第一个单词外,其余单词首字母大写
重新拼接各单词
例如,下面的写法都等效地匹配ngBind指令:

    <span ng-bind="name"></span> <br>
    <span ng:bind="name"></span> <br>
    <span ng_bind="name"></span> <br>
    <span data-ng-bind="name"></span> <br>
    <span x-ng-bind="name"></span> <br>

所以,在前面的课程中,我们在HTML中使用的ez-duang指令,将被规范为ezDuang,编译器使用 这个规范化的名称与注册的指令进行匹配。

编译器/$compile

编译器是一个AngularJS的内置服务,它负责遍历DOM树来查找匹配指令,并根据指令进行 处理。

HTML编译包括3个步骤:

匹配指令。compileDOMDOMDOMDOMprioritycompilecompilelinklinklinkcompile通过执行link函数,将模板和scope链接起来。结果就是 一个DOM和SCOPE之间的动态绑定。
为何将compile和link分开?

简单说,当模型变化会导致DOM结构变化时,需要compile和link分开。例如,ng-repeat指令 需要为集合中的每个成员克隆DOM元素。将编译和链接过程分开可以有效地提高性能,因为 模板克隆仅需要编译一次,但链接则发生在每个克隆的DOM元素上。

指令很少需要compile函数,这是很少见的,因为大多数指令考虑的是作用于特定的DOM元素实例, 而不是改变DOM的结构。

指令通常需要link函数,link函数允许指令在DOM元素上注册监听器,

理解控制器

我们知道,在AngularJS中,实现数据绑定的核心是scope对象。那么控制器又有什么用呢?

简单地说,没有控制器,我们没有地方定义业务模型。

回忆下ng-init指令,我们可以使用ng-init指令在scope对象上定义数据,比如:

    <div ng-init="sb={name:'somebody',gender:'male',age:28}">
    </div>

但是,ng-init的值是一个AngularJS表达式,在这个表达式里,没有办法定义方法。

控制器/controller让我们有机会在scope上定义我们的业务逻辑,具体说,可以使用控制器:

对scope对象上的数据模型进行初始化
向$scope对象上的数据模型添加方法
在一个HTML元素上使用ng-controller指令,就可以引入一个控制器对象:

    <div ng-controller="myController">...</div>

名为myController的控制器实际上就是一个JavaScript的构造函数:

        //控制器类定义
        var myControllerClass = function($scope){
        //模型属性定义
        $scope.text = "...";
        //模型方法定义
        $scope.do = function(){...};
    };
    //在模块中注册控制器
    angular.module('someModule',[])
    .controller("myController",myControllerClass);

控制器构造函数仅在AngularJS对HTML文档进行编译时被执行一次,从这个角度看,就更容易理解 为何将控制器称为对scope对象的增强:一旦控制器创建完毕,就意味着scope对象上的业务模型 构造完毕,此后就不再需要控制器了- scope对象接管了一切。

控制器对scope的影响

ng-controller指令将在该DOM对象上创建一个新的scope对象,这个scope对象的原型就是父scope。 在下图中,我们看到:

ng-app指令导致rootScopebodyscoperootScope,此时,通过ng-init指令,将sb对象挂在了rootScopedivngcontrollerscoperootScope。在do函数 中对sb的引用是有效的,在当前scope中没有sb的定义,因此这个引用指向原型对象的sb,最终 $rootScope上的sb得到了修改。

初始化$scope对象

通常在应用启动时,需要初始化scope对象上的数据模型。我们之前曾使用ng-init指令进行初始化, 而使用控制器则是更为规范的做法。

右边的示例定义了一个ezController,利用这个控制器,我们对业务模型进行了初始化赋值:

请注意,控制器仅仅负责在编译时在scope对象上建立视图对象vm,视图对象和模板的绑定则是由 scope负责管理的。

向cope对象添加方法

业务模型是动态的,在数据之外,我们需要给业务模型添加动作。

在之前建立的业务模型上,我们增加一个随机挑选的方法:shuffle,这个方法负责 从一个小型的名人库中随机的选择一个名人来更新模型的sb属性:

通过在button上使用ng-click指令,我们将模型的shuffle方法绑定到了鼠标点击 事件上。试着用鼠标点击【shuffle】按钮,我们的模型将从库里随机的选出一个 名人,显示在视图里。

DON'T DO IT

控制器的设计出发点是封装单个视图的业务逻辑,因此,不要进行以下操作:

DOM操作
应当将DOM操作使用指令directive进行封装。

变换输出形式
应当使用过滤器/filter对输出显示进行转化。

跨控制器共享代码
对于需要复用的基础代码,应当使用服务/service进行封装

创建服务组件

在AngularJS中创建一个服务组件很简单,只需要定义一个具有$get方法的构造函数, 然后使用模块的provider方法进行登记:

    //定义构造函数
    var myServiceProvider = function(){
        this.$get = function(){
            return ....
        };
    };
    //在模块中登记
    angular.module("myModule",[])
    .provider("myService",myServiceProvider);

服务定义语法糖

使用模块的provider方法定义服务组件,在有些场景下显得有些笨重。AngularJS友好 地提供了一些简化的定义方法,这些方法只是对provider方法的封装,分别适用于 不同的场景。

我们简单介绍其中的factory方法和service方法:

factory方法

    factory方法要求提供一个类工厂,调用该类工厂将返回服务实例。
    var myServiceFactory = function(){
        return ...
    };
    angular.module("myModule",[])
    .factory("myService",myServiceFactory);

service方法
service方法要求提供一个构造函数,AngularJS使用这个构造函数创建服务实

    var myServiceClass = function(){
        this.method1 = function(){...}
    };
    angular.module("myModule",[])
    .service("myService",myServiceClass);

创建指令

指令也是一种服务,只是这种服务的定义有几个特殊要求:

  1. 必须使用模块的directive方法定义
  2. 必须提供factory方法
  3. factory方法返回的对象必须返回一个指令定义对象

    //定义指令的类工厂
    var directiveFactory = function(injectables){
        //指令定义对象
        var directiveDefinationObject = {
            ...
        };
        return directiveDefinationObject;
    };
    //在模块上注册指令
    angular.module("someModule",[])
    .directive("directiveName",directiveFactory);
    

    右边的示例定义一个简单的指令ez-hoverable,这个指令被限制只能 出现在属性的位置,每个具有这个指令的HTML元素,将在鼠标移入 时以虚线边框突出显示。

指令定义对象

每个指令定义的工厂函数,需要返回一个指令定义对象,编译器/$compile 在编译时就根据这个定义对象对指令进行展开。

指令定义对象的常用属性如下:

link函数负责实现DOM和scope的数据绑定,通常在link里执行DOM事件监听和数据变化监听。 link函数在template执行后被调用。link是最常用的属性,一个指令的逻辑通常在link函数 里实现。

link函数的形式如下:

    function link(scope,iElement,iAttrs,controller,transcludeFn){...}

restrict

可以是EACM这四个字母的任意组合,用来限定指令的应用场景。如果不指定这个属性, 默认情况下,指令将仅允许被用作元素名和属性名:

    * E - 元素名,例如:<my-directive></my-directive>
    * A - 属性名,例如:<div my-directive="exp"></div>
    * C - 类,例如:<div class="my-directive: exp;"></div>
    * M - 注释,例如:<!-- directive:my-directive exp -->

template

template是一个HTML片段,可以用来:

    * 替换指令的内容。这是默认的行为,可以使用replace属性更改。
    * 替换指令本身(如果replace属性设为TRUE的话)。
    * 包裹指令的内容(如果transclue属性设为TRUE的话)。

replace

指明是否使用template替换指令元素。

    * true - 编译时,将使用template替换指令元素
    * false - 编译时,将使用template替换指令元素的内容

template:定义替换模板

template是一个HTML片段,可以用来:

替换指令的内容。这是默认的行为,可以使用replace属性更改。
替换指令本身(如果replace属性设为TRUE的话)。
包裹指令的内容(如果transclue属性设为TRUE的话)。
最简单的指令只需要进行模板替换就可以实现,右边的示例实现了一个ezCustomer 指令,这个指令只是简单的使用template指定的模板替换ez-customer的内容。

限制指令地址

可以使用restrict属性,限定指令允许出现的位置:

'A' : 只匹配属性
'E' : 只匹配元素
'C' : 只匹配CSS类

    angular.module('demo',[])
    .directive('myCustomer', function() {
      return {
        restrict: 'E',
        template: 'Name: {{customer.name}} Address: {{customer.address}}'
      };

这时,以下的使用方法就不行了:

        <div ng-controller="Controller">
          <div my-customer=""></div>
        </div>

这样是合法的:

<div ng-controller="Controller">
  <my-customer></my-customer>
</div>

使用隔离作用域

默认情况下,指令没有自己的作用域,当然,可以使用一个控制器构造独立 的作用域,但更好的方法,是让指令有自己隔离的作用域。

这通过增加scope选项获得:

angular.module('docsIsolationExample', [])
.controller('Controller', ['$scope', function($scope) {
  $scope.naomi = { name: 'Naomi', address: '1600 Amphitheatre' };
  $scope.vojta = { name: 'Vojta', address: '3456 Somewhere Else' };
}])
.directive('myCustomer', function() {
  return {
    restrict: 'E',
    scope: {
      customerInfo: '=info'
    },
    template: 'Name: {{customer.name}} Address: {{customer.address}}'
  };
});

使用:

<div ng-controller="Controller">
  <my-customer info="naomi"></my-customer>
   <my-customer info="vojta"></my-customer>
</div>

scope选项指定了每一个隔离作用域绑定的属性:

customerInfo对应隔离作用域中的属性
=info 用来告诉$compile绑定到info属性上

如果需要在指令中操作DOM,我们可以在选项中使用link,link需要指定一个函数, AngularJS将在编译时调用这个函数,并传递scope、element和attrs这几个参数进去。

在指令中操作DOM

scope 是SCOPE对象
element 是jq对象
attrs 是规范化后的属性名/值哈希表

    angular.module('docsTimeDirective', [])
    .controller('Controller', ['$scope', function($scope) {
      $scope.format = 'M/d/yy h:mm:ss a';
    }])
    .directive('myCurrentTime', ['$interval', 'dateFilter', function($interval, dateFilter) {

      function link(scope, element, attrs) {
        var format,
            timeoutId;

        function updateTime() {
          element.text(dateFilter(new Date(), format));
        }

        scope.$watch(attrs.myCurrentTime, function(value) {
          format = value;
          updateTime();
        });

        element.on('$destroy', function() {
          $interval.cancel(timeoutId);
        });

        // start the UI update process; save the timeoutId for canceling
        timeoutId = $interval(function() {
          updateTime(); // update DOM
        }, 1000);
      }

      return {
        link: link
      };
    }]);

使用:

<div ng-controller="Controller">
  Date format: <input ng-model="format"> <hr>
  Current time is: <span my-current-time="format"></span>
</div>

创建包含其他元素的指令

有些指令需要能够包含其他未知的元素,比如一个对话框,我们不知道会有什么元素 需要放在对话框里。如果要设计一个对话框指令,我们需要使用transclude。

angular.module('docsTransclusionDirective', [])
.controller('Controller', ['$scope', function($scope) {
  $scope.name = 'Tobias';
}])
.directive('myDialog', function() {
  return {
    restrict: 'E',
    transclude: true,
    templateUrl: <div ng-transclude=""></div>
  };
});

使用:

<div ng-controller="Controller">
  <my-dialog>Check out the contents, {{name}}!</my-dialog>
</div>

创建响应事件指令

angular.module('dragModule', [])
.directive('myDraggable', ['$document', function($document) {
  return function(scope, element, attr) {
    var startX = 0, startY = 0, x = 0, y = 0;

        element.css({
         position: 'relative',
         border: '1px solid red',
         backgroundColor: 'lightgrey',
         cursor: 'pointer'
        });

    element.on('mousedown', function(event) {
      // Prevent default dragging of selected content
      event.preventDefault();
      startX = event.pageX - x;
      startY = event.pageY - y;
      $document.on('mousemove', mousemove);
      $document.on('mouseup', mouseup);
    });

    function mousemove(event) {
      y = event.pageY - startY;
      x = event.pageX - startX;
      element.css({
        top: y + 'px',
        left:  x + 'px'
      });
    }

    function mouseup() {
      $document.off('mousemove', mousemove);
      $document.off('mouseup', mouseup);
    }
  };
}]);

在视图模板中使用过滤器

过滤器也是一种服务,负责对输入的内容进行处理转换,以便更好地向用户显示。

过滤器可以在模板中的{{}}标记中使用:

    {{ expression | filter:arg1:arg2}}

预置的过滤器
AngularJS的ng模块实现了一些预置的过滤器,如:currency、number等等,可以直接 使用。例如下面的示例将对数字12使用currency过滤器,结果将是"$12.00":

    {{12|currency}}

带参数的过滤器
过滤器也可以有参数,例如下面的示例将数字格式化为"1,234.00":

    {{1234|number:2}}

过滤器流水线
过滤器可以应用于另一个过滤器的输出,可称之为链式调用,语法如下:

    {{expression|filter1|filter2|...}}

在代码中使用过滤器

别忘了过滤器也是一种服务,所以你可以将它注入你的代码中。

和普通的服务不同,过滤器在注入器中注册时,名称被加了一个后缀:Filter。例如, number过滤器的服务名称是:numberFilter,而currency过滤器的服务名称是: currencyFilter。

通常我们的代码被封装在三个地方:控制器、服务、指令。这些地方都支持服务的直接 注入,例如:

    angular.module('myModule',[])
    .controller(function($scope,numberFilter){
        //...
    })

有时你需要显式的通过注入器调用过滤器,那么使用注入器的invoke方法:

    angular.injector(['ng'])
    .invoke(function(numberFilter){
        //...
    })

总之,记住过滤器是一种服务,除了名字需要追加Filter后缀,和其他服务的调用方法没 什么区别

创建过滤器

过滤器也是一种特殊的服务,与创建一个普通的服务相比较:

必须使用模块的filter()接口定义
必须提供factory方法
factory方法必须返回一个过滤器函数,其第一个参数为输入变量

    //定义过滤器类工厂
    var filterFactory = function(){
        //定义过滤器函数
        var filter = function(input){
            //process input and generate output
            return output
        }
    };
    //在模块上注册过滤器
    angular.module("someModule",[])
    .filter("filterName",filterFactory);

右边的示例定义了一个将字符串格式化为大写的过滤器。

为过滤器增加参数

过滤器的行为可以通过额外的参数来调整。比如,我们希望改进上一节的示例,使其可以 支持仅大写每个单词的首字母。

实现阶段
通过在过滤器类工厂返回的过滤器函数中传入额外的参数,就可以实现这个功能。
var filter = function(input,argument1,argument2){...}
调用阶段
在使用过滤器时,额外的参数通过前缀:引入,比如
{{expression|filter:argument1:argument2}}

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注